ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

【Python量化实盘工程 #04】回测收益虚高30%?滑点与冲击成本建模让模拟更真实

【Python量化实盘工程 #04】回测收益虚高30%?滑点与冲击成本建模让模拟更真实

回测里买在收盘价、卖在收盘价,现实中你根本做不到。忽略滑点和冲击成本,夏普 2.0 的策略上线后可能直接腰斩。


一、为什么回测总是比实盘"漂亮"

很多量化新手第一次跑实盘都会怀疑人生:

  • 回测年化 +25%,实盘半年 +8%
  • 回测最大回撤 -8%,实盘直接 -20%
  • 回测夏普 1.8,实盘夏普 0.6

差距从哪里来?很大一部分是"执行成本"——你回测时默认能按收盘价成交,但真实市场里:

  1. 滑点:你下单时的价格,和实际成交的价格,总有差距
  2. 冲击成本:大单会"砸穿"盘口,自己的买单把价格推高了
  3. 拆单损耗:为了减少冲击成本拆成多笔,时间拖得越长,价格越不可控

业内有个粗略经验:忽略执行成本的回测,收益可能高估 20%–40%。本文给你一套可落地的建模方法,让回测更接近真实。


二、本文你将得到什么

  1. 三种滑点模型的 Python 实现(固定滑点 / 成交量比例滑点 / 冲击成本)
  2. TWAP / VWAP 拆单执行的代码框架
  3. 一张汇总对比表,量化不同模型的执行损耗
  4. 把滑点模型嵌入回测引擎的最小改动点

三、三种滑点模型:从简单到真实

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})returnfills

4.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.39113,9000.000%
固定滑点 0.1%11.40114,0000.088%
成交量比例滑点11.40114,0000.088%
冲击成本11.40114,0000.088%
TWAP(5切片)11.14111,380-2.212%
VWAP(10条)11.40113,9650.057%

注:TWAP 均价偏低是因为最近 5 条收盘价恰好处于局部低位,体现了"时间分散"带来的均价波动,而非成本优势。


七、常见坑与注意

  1. 滑点不是越小越好:设 0.01% 过于乐观,设 1% 过于悲观;建议用历史盘口数据回算真实滑点分布
  2. 冲击成本对中小盘更致命:日均成交 5000 万的股票,10 万股的冲击可能达到 1%–3%
  3. TWAP 的隐藏风险:市场在"切片间隔"里可能剧烈波动,TWAP 无法躲避极端行情
  4. 别把 VWAP 当最优:VWAP 只是"贴近市场平均",不保证成本最低

八、小结 + 下篇预告

本文实现了三种滑点模型 + TWAP/VWAP 拆单框架。把这套逻辑嵌入你的回测引擎,只需在execute_order()里加一行滑点计算,就能让回测结果更接近实盘。

下篇预告:信号来了就发单?订单状态机与防重单实战——教你用状态机管理订单生命周期,避免重复下单、废单和跨日状态混乱。


免责声明

本文仅供技术学习交流,不构成任何投资建议。市场有风险,投资需谨慎。


了解更多

  • 代码与文档:https://github.com/MaiRuiApi
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进