回测里买在收盘价、卖在收盘价,现实中你根本做不到。忽略滑点和冲击成本,夏普 2.0 的策略上线后可能直接腰斩。
一、为什么回测总是比实盘"漂亮"
很多量化新手第一次跑实盘都会怀疑人生:
- 回测年化 +25%,实盘半年 +8%
- 回测最大回撤 -8%,实盘直接 -20%
- 回测夏普 1.8,实盘夏普 0.6
差距从哪里来?很大一部分是"执行成本"——你回测时默认能按收盘价成交,但真实市场里:
- 滑点:你下单时的价格,和实际成交的价格,总有差距
- 冲击成本:大单会"砸穿"盘口,自己的买单把价格推高了
- 拆单损耗:为了减少冲击成本拆成多笔,时间拖得越长,价格越不可控
业内有个粗略经验:忽略执行成本的回测,收益可能高估 20%–40%。本文给你一套可落地的建模方法,让回测更接近真实。
二、本文你将得到什么
- 三种滑点模型的 Python 实现(固定滑点 / 成交量比例滑点 / 冲击成本)
- TWAP / VWAP 拆单执行的代码框架
- 一张汇总对比表,量化不同模型的执行损耗
- 把滑点模型嵌入回测引擎的最小改动点
三、三种滑点模型:从简单到真实
3.1 固定比例滑点
最简单、最常用的模型:假设每次成交都损失固定的百分比。
deffixed_slippage(price,direction,pct=0.001):"""固定比例滑点: 买入加价、卖出减价。"""ifdirection=="buy":returnround(price*(1+pct),2)returnround(price*(1-pct),2)- 买入时成交价 = 目标价 × 1.001
- 卖出时成交价 = 目标价 × 0.999
pct=0.001即 0.1%,适合流动性好的大盘股
3.2 成交量比例滑点
固定滑点的问题在于:大盘股和小盘股用同一个数不合理。成交量比例滑点让滑点随流动性动态调整——成交量低于中位数时滑点放大,高于时缩小。
defvolume_slippage(price,vol,median_vol,direction,base_pct=0.001):"""成交量比例滑点: 成交量低于中位数时滑点放大。"""ratio=median_vol/max(vol,1)adj_pct=base_pct*min(ratio,3.0)ifdirection=="buy":returnround(price*(1+adj_pct),2)returnround(price*(1-adj_pct),2)3.3 冲击成本(大额订单)
当你一次性买入 10 万股,市场没有那么厚的买盘承接。冲击成本用平方根法则近似:
importnumpyasnpdefimpact_cost(price,qty,avg_daily_vol,direction,gamma=1.0):"""冲击成本: 大额订单对价格的影响(平方根法则简化版)。"""ifavg_daily_vol<=0:returnprice impact=gamma*np.sqrt(qty/avg_daily_vol)*0.01ifdirection=="buy":returnround(price*(1+impact),2)returnround(price*(1-impact),2)qty / avg_daily_vol是你的委托量占日均成交量的比例gamma是市场冲击系数,A 股活跃标的通常取 0.5–1.5
四、TWAP / VWAP:拆单执行的两种思路
大单不拆 = 裸奔。两种主流拆单策略:
4.1 TWAP(时间加权平均价格)
把订单平均拆到 N 个时间切片,每片按当时的市场价成交。
deftwap_exec(prices,qty,direction,slippage_fn):"""TWAP 执行: 等时间切片,每片用开盘价+滑点成交。"""slices=5qty_per=qty//slices fills=[]foriinrange(slices):p=prices[i]ifi<len(prices)elseprices[-1]p_fill=slippage_fn(p,direction)fills.append({"slice":i+1,"price":p_fill,"qty":qty_per})returnfills4.2 VWAP(成交量加权平均价格)
按历史成交量比例分配每个切片的委托量,尽量贴近市场真实的成交量分布。
defvwap_exec(bars,qty,direction,slippage_fn):"""VWAP 执行: 按成交量比例分配,以 VWAP+滑点成交。"""total_vol=sum(float(b.get("v",0))forbinbars)iftotal_vol==0:returntwap_exec([float(b["c"])forbinbars],qty,direction,slippage_fn)fills=[]remaining=qtyforbinbars:vol=float(b.get("v",0))alloc=int(qty*vol/total_vol)ifalloc<=0:continuealloc=min(alloc,remaining)p=float(b["c"])p_fill=slippage_fn(p,direction)fills.append({"slice":len(fills)+1,"price":p_fill,"qty":alloc})remaining-=allocifremaining<=0:breakifremaining>0andfills:fills[-1]["qty"]+=remainingreturnfills五、完整代码实战
下面用麦蕊行情 API 拉取 600519 的 50 条日线,对比六种执行方案的成交结果。
frommairuiimportClientimportpandasaspdimportnumpyasnp# 演示证书(非真实账户,仅限演示;实际项目请换成自己的证书)client=Client("LICENCE-66D8-9F96-0C7F0FBCD073")# 拉取日线bars=client.stock_history(code="600519",st="",et="",period="d",dividend="f")df=pd.DataFrame(bars)df["c"]=df["c"].astype(float)df["v"]=df["v"].astype(float)close_prices=df["c"].tolist()volumes=df["v"].tolist()median_vol=df["v"].median()avg_vol=df["v"].mean()latest_price=close_prices[-1]qty=10000direction="buy"base_cost=latest_price*qty# 1) 固定滑点p_fixed=round(latest_price*1.001,2)# 2) 成交量比例滑点ratio=median_vol/max(volumes[-1],1)adj_pct=0.001*min(ratio,3.0)p_vol=round(latest_price*(1+adj_pct),2)# 3) 冲击成本impact=1.0*np.sqrt(qty/avg_vol)*0.01p_impact=round(latest_price*(1+impact),2)# 4) TWAP(5 切片)twap_fills=[]foriinrange(5):p=close_prices[-5:][i]twap_fills.append(round(p*1.001,2))twap_avg=sum(twap_fills)/len(twap_fills)print(f"无滑点基准:{latest_price:.2f}| 成本{base_cost:,.0f}")print(f"固定滑点:{p_fixed:.2f}| 成本{p_fixed*qty:,.0f}")print(f"成交量滑点:{p_vol:.2f}| 成本{p_vol*qty:,.0f}")print(f"冲击成本:{p_impact:.2f}| 成本{p_impact*qty:,.0f}")print(f"TWAP 均价:{twap_avg:.2f}| 成本{twap_avg*qty:,.0f}")六、运行结果
以 600519 最新收盘价 11.39 元、买入 10,000 股为例:
| 模型 | 成交均价 | 总成本 | 相对损耗 |
|---|---|---|---|
| 无滑点基准 | 11.39 | 113,900 | 0.000% |
| 固定滑点 0.1% | 11.40 | 114,000 | 0.088% |
| 成交量比例滑点 | 11.40 | 114,000 | 0.088% |
| 冲击成本 | 11.40 | 114,000 | 0.088% |
| TWAP(5切片) | 11.14 | 111,380 | -2.212% |
| VWAP(10条) | 11.40 | 113,965 | 0.057% |
注:TWAP 均价偏低是因为最近 5 条收盘价恰好处于局部低位,体现了"时间分散"带来的均价波动,而非成本优势。
七、常见坑与注意
- 滑点不是越小越好:设 0.01% 过于乐观,设 1% 过于悲观;建议用历史盘口数据回算真实滑点分布
- 冲击成本对中小盘更致命:日均成交 5000 万的股票,10 万股的冲击可能达到 1%–3%
- TWAP 的隐藏风险:市场在"切片间隔"里可能剧烈波动,TWAP 无法躲避极端行情
- 别把 VWAP 当最优:VWAP 只是"贴近市场平均",不保证成本最低
八、小结 + 下篇预告
本文实现了三种滑点模型 + TWAP/VWAP 拆单框架。把这套逻辑嵌入你的回测引擎,只需在execute_order()里加一行滑点计算,就能让回测结果更接近实盘。
下篇预告:信号来了就发单?订单状态机与防重单实战——教你用状态机管理订单生命周期,避免重复下单、废单和跨日状态混乱。
免责声明
本文仅供技术学习交流,不构成任何投资建议。市场有风险,投资需谨慎。
了解更多
- 代码与文档:https://github.com/MaiRuiApi