【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 固定比例滑点

最简单、最常用的模型:假设每次成交都损失固定的百分比。

def fixed_slippage(price, direction, pct=0.001):
"""固定比例滑点: 买入加价、卖出减价。"""
if direction == "buy":
return round(price * (1 + pct), 2)
return round(price * (1 - pct), 2)

  • 买入时成交价 = 目标价 × 1.001
  • 卖出时成交价 = 目标价 × 0.999
  • pct=0.001 即 0.1%,适合流动性好的大盘股

3.2 成交量比例滑点

固定滑点的问题在于:大盘股和小盘股用同一个数不合理。成交量比例滑点让滑点随流动性动态调整——成交量低于中位数时滑点放大,高于时缩小。

def volume_slippage(price, vol, median_vol, direction, base_pct=0.001):
"""成交量比例滑点: 成交量低于中位数时滑点放大。"""
ratio = median_vol / max(vol, 1)
adj_pct = base_pct * min(ratio, 3.0)
if direction == "buy":
return round(price * (1 + adj_pct), 2)
return round(price * (1 - adj_pct), 2)

3.3 冲击成本(大额订单)

当你一次性买入 10 万股,市场没有那么厚的买盘承接。冲击成本用平方根法则近似:

import numpy as np

def impact_cost(price, qty, avg_daily_vol, direction, gamma=1.0):
"""冲击成本: 大额订单对价格的影响(平方根法则简化版)。"""
if avg_daily_vol <= 0:
return price
impact = gamma * np.sqrt(qty / avg_daily_vol) * 0.01
if direction == "buy":
return round(price * (1 + impact), 2)
return round(price * (1 - impact), 2)

  • qty / avg_daily_vol 是你的委托量占日均成交量的比例
  • gamma 是市场冲击系数,A 股活跃标的通常取 0.5–1.5

四、TWAP / VWAP:拆单执行的两种思路

大单不拆 = 裸奔。两种主流拆单策略:

4.1 TWAP(时间加权平均价格)

把订单平均拆到 N 个时间切片,每片按当时的市场价成交。

def twap_exec(prices, qty, direction, slippage_fn):
"""TWAP 执行: 等时间切片,每片用开盘价+滑点成交。"""
slices = 5
qty_per = qty // slices
fills = []
for i in range(slices):
p = prices[i] if i < len(prices) else prices[-1]
p_fill = slippage_fn(p, direction)
fills.append({"slice": i + 1, "price": p_fill, "qty": qty_per})
return fills

4.2 VWAP(成交量加权平均价格)

按历史成交量比例分配每个切片的委托量,尽量贴近市场真实的成交量分布。

def vwap_exec(bars, qty, direction, slippage_fn):
"""VWAP 执行: 按成交量比例分配,以 VWAP+滑点成交。"""
total_vol = sum(float(b.get("v", 0)) for b in bars)
if total_vol == 0:
return twap_exec([float(b["c"]) for b in bars], qty, direction, slippage_fn)
fills = []
remaining = qty
for b in bars:
vol = float(b.get("v", 0))
alloc = int(qty * vol / total_vol)
if alloc <= 0:
continue
alloc = 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 -= alloc
if remaining <= 0:
break
if remaining > 0 and fills:
fills[-1]["qty"] += remaining
return fills


五、完整代码实战

下面用麦蕊行情 API 拉取 600519 的 50 条日线,对比六种执行方案的成交结果。

from mairui import Client
import pandas as pd
import numpy as np

演示证书(非真实账户,仅限演示;实际项目请换成自己的证书)

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 = 10000
direction = "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.01
p_impact = round(latest_price * (1 + impact), 2)

4) TWAP(5 切片)

twap_fills = []
for i in range(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_fixedqty:,.0f}")
print(f"成交量滑点: {p_vol:.2f} | 成本 {p_vol
qty:,.0f}")
print(f"冲击成本: {p_impact:.2f} | 成本 {p_impactqty:,.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 条收盘价恰好处于局部低位,体现了"时间分散"带来的均价波动,而非成本优势。


七、常见坑与注意

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

八、小结 + 下篇预告

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

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


免责声明

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


了解更多

posted @ 2026-10-13 08:55  量化分析码农  阅读(2)  评论(0)    收藏  举报