3. 盘口快照频率与插值偏差
盘口量化回测里有一类偏差非常隐蔽:策略代码没有错,成交逻辑也没有错,但回测出来的收益曲线就是比实盘"顺滑"得多。追查下去,问题往往出在数据层——快照间隔过大,而回测引擎为了让价格序列"连续",在相邻快照之间做了线性插值。插值本身不是错,错在把插值出来的价格当成了真实可成交的价格路径。
一、快照间隔为什么会产生偏差
盘口数据(Level-1 或 Level-2 快照)本质上是离散采样。交易所或行情源按固定频率推送快照,常见的有 3 秒一笔(部分 A 股 Level-1 快照)、500 毫秒一笔(部分期货行情)、甚至逐笔重建的 tick 数据。无论频率多高,两次快照之间发生的一切——价格跳动、挂单撤单、瞬时成交——都没有被记录。
回测引擎面对两个快照之间的时间空档,通常有三种处理方式:
- 前值填充(ffill):用上一笔快照的价格一直延续到下一笔快照。价格路径是阶梯状的,不会凭空产生中间价,但会低估波动、延迟信号触发。
- 线性插值(linear interpolation):在相邻快照的中间价之间画一条直线,按时间比例生成中间价格。价格路径变成斜线,看起来"连续",但这条斜线上的每一个点都是虚构的。
- 不填充:只在快照时刻判断信号,中间不产生任何价格点。最保守,但可能漏掉快照之间的真实机会。
偏差主要来自第二种。线性插值假设价格在两次快照之间匀速变化,而真实市场是跳跃的、有方向的、带噪声的。插值出来的斜线会系统性地"平滑"掉价格的真实波动结构,导致回测中基于价格穿越、均线交叉、止损触发的判断出现两类错误:虚假触发(插值路径穿过了某个阈值,但真实价格从未到达)和触发延迟或漏触发(真实价格在快照间隙穿过了阈值又回来,插值路径没有体现)。
二、识别插值偏差的三个检查点
要判断一份回测结果是否被插值污染,可以从数据层和信号层分别检查。
检查点一:快照间隔分布。统计相邻快照的时间差,画出直方图。如果存在大量间隔超过策略信号周期的情况(例如策略用 1 分钟均线,但快照间隔达到 5 秒以上且不均匀),插值的影响就会被放大。特别要关注间隔的不均匀性——行情活跃时间隔短、清淡时间隔长,插值在清淡时段生成的虚假路径占比更高。
检查点二:插值点占比。在回测的价格序列中,标记哪些点是原始快照、哪些是插值生成。如果插值点占比超过 50%,说明大部分价格路径是虚构的,任何基于逐点价格的计算(收益率、波动率、最大回撤)都不可信。
检查点三:信号触发点与快照时刻的对齐。把回测中所有开平仓信号的触发时间列出来,检查它们落在原始快照时刻还是插值区间内。如果大量信号触发在插值区间,且这些信号在"仅用快照时刻判断"的对照回测中消失,就基本可以确认是插值引入的虚假信号。
上图展示了一个典型场景:真实价格在快照之间是阶梯跳跃的(深蓝实线),而线性插值把它拉成了斜线(红色虚线)。在标注位置,插值路径穿过了信号阈值,但真实快照从未到达该价位——这就是一次虚假触发。如果策略是"价格上穿阈值开多",回测会记录一笔盈利或亏损交易,而实盘中这笔交易根本不会发生。
三、处理方案:从"填平"转向"尊重离散"
处理插值偏差的核心原则是:回测的决策时刻必须与真实可观测时刻对齐。具体落地有四种做法,按保守程度递增。
方案 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 |
需要强调的是,插值偏差不是"数据质量问题"这么简单,它会系统性地改变策略的收益分布。线性插值倾向于让价格路径更平滑、更"趋势化",这会利好趋势跟踪类策略、利空均值回归类策略——因为平滑后的路径减少了假突破,也减少了回归机会。所以同一份插值数据,对不同类型策略的偏差方向可能相反。排障时不能只看"回测收益是否偏高",而要看"信号触发结构是否被改变"。把快照时刻、插值区间、信号触发点三者的对齐关系画出来,偏差就无处藏身了。