2. 策略回测中的时间泄漏场景剖析

时间泄漏是盘口策略回测中最隐蔽、也最具破坏性的一类错误。它不会让程序报错,也不会让回测曲线出现明显异常——恰恰相反,泄漏往往让回测结果变得"过于漂亮",从而在实盘中造成远超预期的落差。本节围绕四类典型泄漏场景展开:未来函数、快照对齐错误、特征计算跨期与标签泄漏,逐一剖析其成因、表现与后果。

一、未来函数:在决策时刻使用了尚未产生的信息

未来函数指策略在时刻 t 做决策时,引用了时间戳晚于 t 的数据。盘口策略中最常见的形态是"用当前快照的收盘价成交"。例如,策略在 10:00:00.000 的盘口快照上计算信号,却假设以该快照的买一价立即成交。问题在于:这份快照本身是 10:00:00.000 时刻的截面,而信号计算需要读取整份快照,等信号算完,真实时间已经推进到下一个快照周期。若回测仍按当前快照价格成交,就等于让策略"预知"了本周期内的价格路径。

更隐蔽的一种是滚动窗口越界。假设用最近 20 个快照的成交量均值作为特征,代码写成 df['vol'].rolling(20).mean() 后直接与同一行的价格对齐,看似无误;但如果数据按时间倒序排列,或 rolling 的 center=True,窗口就会包含未来快照。这类错误在数据预处理阶段引入,往往到复盘时才发现。

判断未来函数的一个实用方法是"截断测试":把数据在任意时刻 T 截断,只保留 t ≤ T 的记录重新回测,若 T 之前的信号或成交结果发生变化,说明存在未来信息依赖。正确的做法是让信号在 t 时刻生成、在 t+1 个快照(或明确的延迟)成交,并在代码中显式写出 signal.shift(1) 这类位移。

二、快照对齐错误:时间戳语义不一致

盘口数据通常以固定频率(如每 500ms 或每 3s)采样,但不同数据源的时间戳语义可能完全不同:有的是快照生成时刻,有的是交易所推送时刻,有的是本地接收时刻。若把 A 源的信号时间戳与 B 源的价格时间戳直接按行号对齐,就会产生系统性错位。

典型后果是"偷看一个周期"。假设信号源时间戳比价格源晚 200ms,按行对齐后,第 i 行的信号实际对应第 i 行价格之后 200ms 的信息,回测中却当作同一时刻处理,等于每个决策都提前看到了未来 200ms 的盘口变化。在高频盘口策略中,200ms 足以覆盖数次报价跳动,回测收益会被显著高估。

正确的对齐方式是使用 merge_asof 按时间戳做最近邻匹配,并明确指定 direction='backward',保证每个决策只使用不晚于决策时刻的最近一份数据。对齐后还应抽查若干时间点,人工核对信号时间戳与所用价格时间戳的先后关系。

三、特征计算跨期:窗口内混入决策时刻之后的数据

特征计算跨期与未来函数有交集,但更强调"窗口边界"问题。盘口特征如买卖压力比、订单簿斜率、成交量加权价等,常需要在一个时间窗口内聚合。若窗口定义为 [t-5s, t+5s] 这类对称区间,或聚合时未按时间戳过滤而按行数切片,就会把 t 之后的数据纳入特征。

一个易被忽视的场景是"当日统计特征":用当日累计成交量、当日已实现波动率作为特征。若在盘中 t 时刻使用"当日全天"的统计量,就引入了未来信息。正确做法是使用截至 t 的累计值,即扩展窗口(expanding window)而非全样本窗口。下面给出一个可复用的对齐与特征构造片段:

# 假设 snap 已按 timestamp 升序排列
snap = snap.sort_values('timestamp').reset_index(drop=True)

# 1) 信号位移:t 时刻信号,t+1 快照成交
snap['signal'] = compute_signal(snap)          # 仅使用 t 及之前数据
snap['exec_price'] = snap['bid1'].shift(-1)    # 下一快照买一价成交
snap['ret'] = snap['exec_price'] / snap['bid1'] - 1

# 2) 扩展窗口特征:只用截至当前时刻的数据
snap['cum_vol'] = snap['volume'].expanding().sum()
snap['vol_ma20'] = snap['volume'].rolling(20, min_periods=20).mean()

# 3) 跨源对齐:信号时间戳匹配不晚于它的最近价格
merged = pd.merge_asof(
    signals.sort_values('ts'),
    prices.sort_values('ts'),
    on='ts', direction='backward'
)

四、标签泄漏:训练标签中混入了决策时不可得的信息

标签泄漏发生在监督学习类策略中。标签通常定义为"未来 N 个快照的收益方向",这本身合理;问题出在标签的构造方式上。若标签使用未来窗口内的最高价或最低价(如"未来 10 个快照内是否触及 +2 跳"),而特征窗口与标签窗口在时间上重叠,模型就会学到"未来信息"与"当前特征"的虚假关联。

更严重的是样本重叠:相邻样本的标签窗口大量重叠,导致训练集与验证集之间信息串通。例如每 500ms 一个样本、标签窗口 10s,则相邻 20 个样本共享同一段未来价格,随机划分训练/验证集时,验证集的信息几乎必然出现在训练集中,验证指标虚高。正确做法是按时间切分(如前 70% 训练、后 30% 验证),并在标签窗口与特征窗口之间留出隔离带(purge gap),隔离带长度不小于标签窗口长度。

四类时间泄漏的时间轴对照 过去 未来 决策时刻 t ① 未来函数 t 时刻信号 用 t 快照价成交 ← 越界使用 ② 快照对齐错误 信号源 ts 价格源 ts ← 错位 200ms,偷看未来 ③ 特征计算跨期 窗口 [t-5s, t+5s] 含未来段 ← 对称窗口越界 ④ 标签泄漏 特征窗口 隔离带 标签窗口(未来) ← 窗口重叠/样本串通

五、后果分析与排查优先级

四类泄漏的共同后果是回测收益被系统性高估,但表现形态不同。未来函数通常表现为"胜率异常高、单笔收益分布过于集中";快照对齐错误表现为"换手率极高但收益稳定为正",因为策略实际上在利用错位信息做无风险套利;特征跨期表现为"特征重要性排名中,某些窗口统计量异常靠前";标签泄漏则表现为"训练集与验证集指标几乎一致,但实盘迅速衰减"。

排查时建议按以下优先级:先做截断测试排除未来函数,再用 merge_asof 复核跨源对齐,然后检查所有滚动窗口的边界方向,最后审查标签构造与训练/验证切分方式。每一步都应保留可复现的最小样例,便于定位问题引入的具体代码行。需要强调的是,泄漏排查不是一次性工作,而应固化为回测流水线中的自动检查项——例如在每次回测前断言"所有特征时间戳 ≤ 决策时间戳",把隐性错误变成显性失败。

回测结果显著优于同类策略时,应优先怀疑时间泄漏而非策略优势。建议在策略上线前,用一段与训练期不重叠的样本外数据做独立验证,并对比样本内外的收益衰减幅度;若衰减超过合理范围,需回到本节四类场景逐一排查。