4. 回测框架问题:滑点、手续费与停牌处理
回测跑出来的曲线再漂亮,扔到实盘里一塌糊涂——这种故事我听过太多次了。多半问题出在回测引擎本身。你想想看,引擎对市场环境的模拟如果太理想化,策略就像在玻璃瓶里游泳,看着挺美,一放进大海就淹死。
我个人习惯把回测引擎的漏洞分成三大类:滑点模拟缺陷、手续费计算失真、停牌处理不当。下面逐个拆。
4.1 滑点——那个吃你利润的隐形黑洞
滑点这东西,说白了就是你的挂单价格和实际成交价格之间的差值。为什么会有滑点?市场深度不够、流动性差、大单冲击、交易延迟。回测时忽略它,等于默认你能以理想价格买卖。嗯,这很危险。
常见的滑点模型有哪些?
| 模型 | 定义 | 典型问题 |
|---|---|---|
| 固定滑点 | 每笔成交价偏移固定值(如0.01元) | 对小市值股票严重低估滑点,对大市值又过度惩罚 |
| 百分比滑点 | 成交价偏移成交金额的固定比例(如0.1%) | 忽略价格波动和流动性差异,日内不同时段滑点实际差异巨大 |
| 基于成交量的滑点 | 滑点与订单量/市场深度挂钩 | 计算复杂,但更接近真实——我偏向这种 |
| 随机滑点 | 从历史滑点分布中抽样 | 需要大量数据支撑,否则噪声过大 |
我踩过的坑:早些年做高频动量策略,回测只用0.5‰固定滑点。实盘第一天就亏了2%,后来查出来是因为那支股流动性极差,固定滑点根本覆盖不了盘口跳价。后来我改成基于过去20笔tick成交均价计算滑点,虽然慢了点,但靠谱多了。
一个简单的基于历史成交滑点的伪代码:
def calc_slippage(ticker, order_qty, timestamp):
# 取近20笔tick的买一卖一价差中位数
tick_data = get_recent_ticks(ticker, timestamp, 20)
spread_median = np.median([t['ask'] - t['bid'] for t in tick_data])
# 订单量占5档深度的比例
depth = get_order_book_depth(ticker, timestamp, 5)
ratio = order_qty / max(depth, 1)
# 衰减系数
return spread_median * 0.5 * (1 + np.tanh(3 * ratio))
为什么用 tanh?你看,当订单量远小于市场深度时,滑点趋近 spread/2;当订单量猛增时,滑点被非线性放大——现实中大单就是会额外推高成本。
4.2 手续费——别被“万二点五”骗了
很多回测框架只在成交时扣一个固定费率。但真实的手续费结构比这复杂。
以A股为例:
- 佣金:默认万2.5,但每笔最低5元。如果你做小单高频,最低5元这一条就把你的成本翻了几倍。
- 印花税:卖出时收千分之一(目前单边)。注意卖出。
- 过户费:万0.2,买卖都收。盘子大的策略一年下来也能吃掉不少。
还可能有平台费、结算费、VIP通道费。我见过一个团队的回测引擎只写了“手续费=成交额×0.0003”,结果实盘后实际手续费是回测的3倍——因为他们没处理最低5元。
注意:有些期货品种还有日内平今仓优惠/惩罚。比如股指期货平今手续费是开仓的10倍。回测时不区分开平方向,等于白送钱。
我建议至少实现一个可配置的阶梯费率函数:
def calc_commission(order_type, price, qty, market='A'):
turnover = price * qty
if market == 'A':
comm = max(turnover * 0.00025, 5) # 佣金,最低5元
stamp = turnover * 0.001 if order_type == 'sell' else 0
transfer = turnover * 0.00002
return comm + stamp + transfer
elif market == 'future':
# 区分平今
...
4.3 停牌与涨跌停——策略的“隐身”杀手
停牌是回测中最容易被忽视的漏洞之一。为什么重要?
- 停牌期间你的持仓无法卖出,风险敞口被锁死。
- 复牌后通常伴随剧烈波动,你的滑点模型可能完全失效。
- 有些策略在停牌日频繁产生买卖信号,但实际无法成交。
我曾经排查一个低频均值回归策略,回测年化22%,实盘只有7%。最后定位到bug:回测引擎在处理停牌时,把停牌日的bar自动跳过,但策略在下一天继续用停牌前的价格生成信号。等于说策略“预知”了复牌后的价格——这明显是前视偏差。
正确的做法:
- 停牌期间,策略应保持挂单状态(或取消挂单),不产生新的买卖单。
- 复牌当日,以实际集合竞价或连续竞价价格处理积压订单。
- 如果停牌超过N天,策略需要做风控处理(例如强制减仓或对冲)。
涨跌停的情况更微妙。很多框架简单地让涨停后的买单和跌停后的卖单“成交不了”,但没考虑封单量和排队顺序。我向来建议:
- 对于趋势策略:涨跌停封单量超过某个阈值时,判定成交概率为0。
- 对于套利策略:涨跌停可能导致连续多日无法平仓,必须模拟这种流动性枯竭。
4.4 回测引擎的核心架构缺陷
除了上述三个具体漏洞,我还想提一点引擎架构上的常见问题:
| 漏洞类型 | 现象 | 我的处理方法 |
|---|---|---|
| 撮合引擎过于简单 | 按收盘价撮合,无视盘口 | 至少用Bar内的VWAP或开盘/收盘价加权 |
| 订单簿状态不全 | 不维护挂单队列,止盈止损单失效 | 实现一个简易订单簿,跟踪未成交单 |
| 数据污染 | 停牌期间数据填充了前后收盘价 | 停牌期间价格/成交量为NaN,不填充 |
| 回填时间戳错误 | 异步数据到达顺序乱掉 | 严格按时间戳排序,拒绝未来数据 |
一句话总结:回测框架不是“跑个函数”那么简单。它本质上是市场微观结构的压缩模型。你压缩得越粗糙,策略偏差越大。
下面我把本章的三大漏洞逻辑画成一张图,方便你对照排查:
自查清单:你可以在回测报告里加一个“成本偏离率”指标——(回测总成本 - 模拟实盘总成本) / 模拟实盘总成本。如果偏离超过10%,说明你的引擎模型有系统性问题。