3. 盘口快照频率与插值偏差

盘口量化回测里有一类偏差非常隐蔽:策略代码没有错,成交逻辑也没有错,但回测出来的收益曲线就是比实盘"顺滑"得多。追查下去,问题往往出在数据层——快照间隔过大,而回测引擎为了让价格序列"连续",在相邻快照之间做了线性插值。插值本身不是错,错在把插值出来的价格当成了真实可成交的价格路径。

一、快照间隔为什么会产生偏差

盘口数据(Level-1 或 Level-2 快照)本质上是离散采样。交易所或行情源按固定频率推送快照,常见的有 3 秒一笔(部分 A 股 Level-1 快照)、500 毫秒一笔(部分期货行情)、甚至逐笔重建的 tick 数据。无论频率多高,两次快照之间发生的一切——价格跳动、挂单撤单、瞬时成交——都没有被记录。

回测引擎面对两个快照之间的时间空档,通常有三种处理方式:

  • 前值填充(ffill):用上一笔快照的价格一直延续到下一笔快照。价格路径是阶梯状的,不会凭空产生中间价,但会低估波动、延迟信号触发。
  • 线性插值(linear interpolation):在相邻快照的中间价之间画一条直线,按时间比例生成中间价格。价格路径变成斜线,看起来"连续",但这条斜线上的每一个点都是虚构的。
  • 不填充:只在快照时刻判断信号,中间不产生任何价格点。最保守,但可能漏掉快照之间的真实机会。

偏差主要来自第二种。线性插值假设价格在两次快照之间匀速变化,而真实市场是跳跃的、有方向的、带噪声的。插值出来的斜线会系统性地"平滑"掉价格的真实波动结构,导致回测中基于价格穿越、均线交叉、止损触发的判断出现两类错误:虚假触发(插值路径穿过了某个阈值,但真实价格从未到达)和触发延迟或漏触发(真实价格在快照间隙穿过了阈值又回来,插值路径没有体现)。

二、识别插值偏差的三个检查点

要判断一份回测结果是否被插值污染,可以从数据层和信号层分别检查。

检查点一:快照间隔分布。统计相邻快照的时间差,画出直方图。如果存在大量间隔超过策略信号周期的情况(例如策略用 1 分钟均线,但快照间隔达到 5 秒以上且不均匀),插值的影响就会被放大。特别要关注间隔的不均匀性——行情活跃时间隔短、清淡时间隔长,插值在清淡时段生成的虚假路径占比更高。

检查点二:插值点占比。在回测的价格序列中,标记哪些点是原始快照、哪些是插值生成。如果插值点占比超过 50%,说明大部分价格路径是虚构的,任何基于逐点价格的计算(收益率、波动率、最大回撤)都不可信。

检查点三:信号触发点与快照时刻的对齐。把回测中所有开平仓信号的触发时间列出来,检查它们落在原始快照时刻还是插值区间内。如果大量信号触发在插值区间,且这些信号在"仅用快照时刻判断"的对照回测中消失,就基本可以确认是插值引入的虚假信号。

快照间隔与线性插值:真实路径 vs 插值路径 时间 价格 信号阈值 插值路径穿越阈值 (真实快照未穿越) 快照间隔 Δt 真实快照路径(阶梯) 线性插值路径(虚构) 信号阈值

上图展示了一个典型场景:真实价格在快照之间是阶梯跳跃的(深蓝实线),而线性插值把它拉成了斜线(红色虚线)。在标注位置,插值路径穿过了信号阈值,但真实快照从未到达该价位——这就是一次虚假触发。如果策略是"价格上穿阈值开多",回测会记录一笔盈利或亏损交易,而实盘中这笔交易根本不会发生。

三、处理方案:从"填平"转向"尊重离散"

处理插值偏差的核心原则是:回测的决策时刻必须与真实可观测时刻对齐。具体落地有四种做法,按保守程度递增。

方案 A:关闭插值,仅用快照时刻决策。把回测引擎的插值开关关掉,信号判断只在原始快照时间戳上进行。这是最直接、最不容易出错的做法。代价是如果快照间隔较大,策略可能错过快照之间的机会,回测结果偏保守。对于中低频策略(持仓周期分钟级以上),这个代价通常可以接受。

方案 B:前值填充 + 快照时刻触发。价格序列用 ffill 延续,但信号判断和成交撮合都锚定在快照时刻。这样既保持了时间轴的连续性(便于计算持仓时间、资金曲线),又不会在虚构价格上做决策。注意:用 ffill 后的价格计算浮动盈亏是可以的,但不能用它触发交易信号。

# 示例:仅用原始快照时刻判断信号(伪代码)
snapshots = load_snapshots("level1_20240102.csv")  # 含 timestamp, bid, ask, last
snapshots = snapshots.sort_values("timestamp").reset_index(drop=True)

signals = []
for i in range(1, len(snapshots)):
    prev_mid = (snapshots.bid[i-1] + snapshots.ask[i-1]) / 2
    curr_mid = (snapshots.bid[i] + snapshots.ask[i]) / 2
    # 只在快照时刻比较,不生成中间点
    if prev_mid < threshold <= curr_mid:
        signals.append({
            "time": snapshots.timestamp[i],
            "type": "cross_up",
            "price": snapshots.ask[i]  # 用卖一价模拟买入
        })

方案 C:提高数据频率或改用逐笔数据。如果策略确实依赖快照之间的价格变化(例如日内高频、盘口套利),3 秒快照本身就不够用,应该换用逐笔成交(tick-by-tick)或 Level-2 逐笔委托数据重建盘口。这是从数据源层面解决问题,成本最高但最彻底。

方案 D:插值仅用于可视化,不用于回测。如果团队需要平滑的价格曲线做展示或报告,可以保留插值,但必须在回测引擎中把插值序列和决策序列分离——决策用原始快照,展示用插值曲线,两者不混用。

四、一个可复用的排查清单

在实际排障中,可以按下面的顺序快速定位问题,避免在错误的层面上反复调参:

排查项 观察方法 异常信号 处理动作
快照间隔分布 统计相邻时间差,看 P50 / P95 / 最大值 P95 远大于策略信号周期 换更高频数据,或降低策略频率
插值点占比 标记原始点与插值点,计算比例 插值点超过 50% 关闭插值,改用快照时刻决策
信号触发位置 列出信号时间,对照快照时间戳 大量信号落在插值区间 用方案 A 做对照回测,比较差异
阈值穿越验证 对每个穿越信号,检查前后两笔快照是否真的跨越阈值 插值穿越但快照未穿越 剔除该信号,或改用快照确认机制
成交价合理性 检查成交价是否落在当时买卖价之间 成交价使用了插值中间价 成交撮合改用快照的 bid/ask
一个实用的对照实验:同一份策略、同一段行情,分别用"插值回测"和"仅快照回测"跑两遍,比较交易笔数、胜率、最大回撤。如果两者差异显著(例如交易笔数相差 30% 以上),说明策略对价格路径的细节高度敏感,插值偏差已经实质性影响结论,必须以仅快照回测为准,并重新评估策略在真实数据频率下的可行性。

需要强调的是,插值偏差不是"数据质量问题"这么简单,它会系统性地改变策略的收益分布。线性插值倾向于让价格路径更平滑、更"趋势化",这会利好趋势跟踪类策略、利空均值回归类策略——因为平滑后的路径减少了假突破,也减少了回归机会。所以同一份插值数据,对不同类型策略的偏差方向可能相反。排障时不能只看"回测收益是否偏高",而要看"信号触发结构是否被改变"。把快照时刻、插值区间、信号触发点三者的对齐关系画出来,偏差就无处藏身了。