数据快照与避免前视偏差(Point-in-Time)
直觉
模块 0 提过前视偏差——这里从数据角度再讲一遍:很多数据在「事后」会被修订(财报先出预披露、后修正;公司被并购后从数据库消失)。如果回测时直接用「今天看到的数据」,等于偷看了未来。Point-in-Time(PIT)数据就是给每个数据点打上「何时可得」的时间戳,回测只用当时已公开的版本。
两类前视
- 特征前视:用「包含当日」的信息生成当日特征(如用当日收盘算当日均线再当日交易);
- 修订/幸存者前视:用了后来才修订或后来才筛选过的数据。
「人话」解释:特征前视怎么躲?
记住一条铁律:当日只能用「昨日及之前」的信息。生成特征后,再整体 .shift(1) 一下,确保「今日用昨日特征」。
我们的回测引擎 quant.vector_backtest 内部正是用 持仓 = 信号.shift(1) 实现这点(见 引擎验证)。
可运行案例:前视特征 vs 安全特征
下面演示两种「20 日均线」特征:一种泄漏(用了当日收盘),一种安全(shift(1))。泄漏版「预测」当期收益时相关性强得离谱——这正是它不可信的原因。
import numpy as np
import pandas as pd
df = pd.read_csv('/data/spy_daily.csv', parse_dates=['date']).set_index('date')
close = df['close']
ret = np.log(close / close.shift(1))
ma20 = close.rolling(20).mean()
# 特征 = 价格偏离均线的程度
feature_leaky = (close / ma20 - 1) # 含当日收盘 → 泄漏
feature_safe = feature_leaky.shift(1) # 只用到昨日的均线 → 安全
d = pd.DataFrame({'ret': ret, 'leaky': feature_leaky, 'safe': feature_safe}).dropna()
print("当期收益 与 特征 的同期相关(越高越'像能预测'):")
print(f" 泄漏版特征 corr = {d['ret'].corr(d['leaky']):+.3f} ← 虚高, 不可信")
print(f" 安全版特征 corr = {d['ret'].corr(d['safe']):+.3f} ← 真实可用信号强度")
print("\n→ 差距来自: 泄漏版用'今日收盘'算'今日特征', 再与'今日收益'对比, 当然高相关。")
print(" shift(1) 后, 用'昨日特征'预测'今日收益', 才是诚实的评估。")
动手改一改
把均线窗口从 20 改成 5,再对比泄漏版与安全版的相关——窗口越短,泄漏版的「虚假预测力」往往越夸张,更能凸显 shift(1) 的必要。
小结
- 前视分特征前视(当日用了当日信息)和修订/幸存者前视;
- 防特征前视:生成特征后
.shift(1),只用「昨日的」信息; - PIT 数据给每个数据点打「可得时间戳」,是严肃回测的基础设施。