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自动跳过,但策略在下一天继续用停牌前的价格生成信号。等于说策略“预知”了复牌后的价格——这明显是前视偏差。

正确的做法:

  1. 停牌期间,策略应保持挂单状态(或取消挂单),不产生新的买卖单。
  2. 复牌当日,以实际集合竞价或连续竞价价格处理积压订单。
  3. 如果停牌超过N天,策略需要做风控处理(例如强制减仓或对冲)。

涨跌停的情况更微妙。很多框架简单地让涨停后的买单和跌停后的卖单“成交不了”,但没考虑封单量和排队顺序。我向来建议:

  • 对于趋势策略:涨跌停封单量超过某个阈值时,判定成交概率为0。
  • 对于套利策略:涨跌停可能导致连续多日无法平仓,必须模拟这种流动性枯竭。

4.4 回测引擎的核心架构缺陷

除了上述三个具体漏洞,我还想提一点引擎架构上的常见问题:

漏洞类型现象我的处理方法
撮合引擎过于简单按收盘价撮合,无视盘口至少用Bar内的VWAP或开盘/收盘价加权
订单簿状态不全不维护挂单队列,止盈止损单失效实现一个简易订单簿,跟踪未成交单
数据污染停牌期间数据填充了前后收盘价停牌期间价格/成交量为NaN,不填充
回填时间戳错误异步数据到达顺序乱掉严格按时间戳排序,拒绝未来数据

一句话总结:回测框架不是“跑个函数”那么简单。它本质上是市场微观结构的压缩模型。你压缩得越粗糙,策略偏差越大。

下面我把本章的三大漏洞逻辑画成一张图,方便你对照排查:

回测框架三大漏洞链路 回测引擎漏洞 滑点模拟缺陷 手续费失真 停牌处理不当 固定模型 忽略深度 随机噪声 最低佣金 单向印花税 平今惩罚 跳过Bar 预知复牌 涨跌停队列 后果:回测曲线上扬,实盘曲线下跪 夏普比虚高30%-200%

自查清单:你可以在回测报告里加一个“成本偏离率”指标——(回测总成本 - 模拟实盘总成本) / 模拟实盘总成本。如果偏离超过10%,说明你的引擎模型有系统性问题。