偏差的工程防范:前视 / 幸存者 / 过拟合
:::info 学习目标 你将能把三类偏差转换成可自动检查的数据和代码规则,并识别修订数据、股票池构造和反复调参中的隐蔽泄漏。 :::
直觉
模块 0.3 介绍过三大偏差。这一节把它们落到工程层:每个偏差长什么样、怎么用代码自检、怎么防范。三者的共同症状都是——回测表现好得不真实。
三大偏差与防范
| 偏差 | 表现 | 工程防范 |
|---|---|---|
| 前视 | 用了当时不可知的信息 | 信号 shift(1);引擎内置 持仓=信号.shift(1) |
| 幸存者 | 只回测了「活下来」的标的 | 用含退市标的的 PIT 数据库 |
| 过拟合 | 参数调到完美贴合历史 | 样本外验证、参数稀疏化(见 8.5) |
可运行案例:前视泄漏让夏普「好得离谱」
构造一个偷看明天涨跌的信号( blatant 前视),与真实双均线信号对比——泄漏信号的夏普会荒唐地高,这正是前视的「红牌」。
import quant
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
df = pd.read_csv('/data/spy_daily.csv', parse_dates=['date']).set_index('date')
close = df['adj_close']
ret = close.pct_change()
cost = 2.0
# —— 泄漏信号: 用"明天"的涨跌方向(赤裸裸的前视) ——
leaky = np.sign(ret.shift(-1)).fillna(0.0) # 1=明天涨, -1=明天跌
# —— 真实信号: 双均线(无未来信息) ——
raw = (close.rolling(20).mean() > close.rolling(60).mean()).astype(float)
safe = raw.fillna(0.0)
r_l = quant.vector_backtest(close, leaky, cost_bps=cost, freq=252)
r_s = quant.vector_backtest(close, safe, cost_bps=cost, freq=252)
print(f"泄漏信号 夏普 = {quant.sharpe(r_l['returns'], freq=252):6.2f} ← 好得不真实(偷看未来)")
print(f"真实信号 夏普 = {quant.sharpe(r_s['returns'], freq=252):6.2f} ← 正常水平")
print("\n→ 铁律: 任何回测夏普好得离谱, 第一反应就是查前视泄漏。")
plt.figure(figsize=(8, 3.4))
plt.plot(r_l['equity'], color='crimson', label='泄漏(前视)')
plt.plot(r_s['equity'], color='#2563eb', label='真实(双均线)')
plt.legend(); plt.title('前视泄漏 vs 真实信号'); plt.tight_layout(); plt.show()
自动化防线
仅靠“记得 shift”不够。应在数据层保存发布时间和成分有效期,在特征层断言每个输入的最大时间不晚于决策时刻,在回测层加入“截断未来数据不应改变过去结果”的测试,并维护完整实验日志。幸存者偏差也不能靠最终股票列表修补,必须从源头保留退市与历史成分。
实践与延伸
- 写截断测试:删除某日期后的数据,确认此前所有信号完全不变。
- 在股票池中加入一只中途退市且大亏的标的,衡量幸存者偏差。
延伸阅读:backtest overfitting、PIT 数据库与实验追踪方法。
小结
- 前视:信号
shift(1)、引擎内置防前视; - 幸存者:用含退市标的的 PIT 数据;
- 过拟合:样本外验证、少参数;
- 回测夏普「好得不真实」≈ 一定有偏差,先排查再相信。
下一节用 Walk-Forward 把「样本外」做成可执行的验证流程。
章末自测
检查核心概念
1. 关于本章主题,哪项说法更准确?