4、数据窥探偏差:多次回测导致的信息泄露
数据窥探偏差,英文叫 Data Snooping Bias。这名字听着挺学术,说白了就是——你偷偷看了答案,然后假装自己猜对了。
我刚开始做量化那会儿,就犯过这个错。一个策略回测跑出来年化 80%,夏普 3.5,我激动得差点把咖啡洒键盘上。结果实盘一周,亏得亲妈都不认识。后来复盘才发现,我在回测里「不小心」用了未来数据。
嗯,今天我们就来聊聊这个坑。怎么掉进去的,以及怎么爬出来。
4.1 什么是数据窥探偏差?
数据窥探偏差,指的是你在回测过程中,有意或无意地利用了「未来信息」来做决策。这会导致回测结果异常漂亮,但实盘一塌糊涂。
最常见的几种情况:
- 多次回测调参:同一个数据集上跑了几百次,挑出最好的一组参数。这组参数其实已经「过拟合」到历史噪声上了。
- 选择偏差:只报告表现好的策略,隐藏失败的尝试。我见过有人做了 50 个策略,只拿出那个夏普 5.0 的来吹。
- 幸存者偏差:回测时只用了当前还在交易的股票,忽略了那些已经退市的。结果就是回测收益虚高。
- 前视偏差:在计算指标时,用了未来才有的数据。比如用今天的收盘价去计算昨天的信号。
4.2 多次回测导致的信息泄露
假设你有一个策略,参数是 N 日均线。你在历史数据上试了 N=5, 10, 20, 30, 60,发现 N=20 表现最好。于是你宣布:「我的 20 日均线策略年化 25%!」
问题在哪?
你实际上做了 5 次回测,选了最好的一次。这 5 次回测之间不是独立的——它们共享同一段历史数据。你相当于在同一个考试上考了 5 次,然后拿着最高分说「我水平就是 100 分」。
我有个朋友,曾经在一个数据集上跑了 2000 次参数优化。最后选出的参数在样本内夏普 4.2,样本外直接变成 -0.3。他后来跟我说:「那 2000 次回测,其实就是在拟合历史数据的随机噪声。」
为什么会这样?
因为金融数据本身信噪比极低。你跑 100 次回测,总有几个参数组合能「恰好」拟合历史走势。但这不代表它们有预测能力。
4.3 如何避免「偷看」未来数据?
避免数据窥探偏差,说白了就是管住自己的手和脑子。下面是我个人习惯的几个方法:
4.3.1 严格的时间序列分割
回测时,训练集和测试集必须按时间顺序严格分开。绝对不能混用。
# 错误做法:随机分割
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3)
# 正确做法:按时间分割
train_end = int(len(data) * 0.7)
train_data = data[:train_end]
test_data = data[train_end:]
我曾经见过有人用随机分割做回测,结果训练集里包含了 2023 年的数据,测试集里却有 2020 年的。这不就等于让模型穿越回去交易吗?
4.3.2 限制回测次数
给自己定个规矩:每个策略最多跑 10 次参数组合。超过这个数,就必须换一段全新的数据来验证。
我现在的做法是:
- 第一轮:用 60% 数据做初步筛选,最多试 20 组参数
- 第二轮:用 20% 数据做验证,只测试第一轮选出的前 3 组
- 第三轮:用最后 20% 数据做最终测试,只跑 1 次
这样层层过滤,能大幅降低过拟合风险。
4.3.3 使用 Walk-Forward 分析
Walk-Forward 分析,说白了就是滚动回测。每次用过去一段数据训练,然后预测下一段。这样能模拟实盘的真实情况。
def walk_forward(data, window_size=252, step_size=63):
results = []
for i in range(0, len(data) - window_size, step_size):
train = data[i:i+window_size]
test = data[i+window_size:i+window_size+step_size]
# 在 train 上训练,在 test 上测试
result = backtest(train, test)
results.append(result)
return results
这个方法的好处是:你永远不会在测试时用到未来的数据。每次预测都是基于「当时已知」的信息。
4.3.4 保持一个「黑箱测试集」
我建议你留出一段数据,永远不碰它。直到策略开发完成、参数固定之后,才拿出来做一次最终测试。
这个数据集我称之为「黑箱测试集」。它就像高考的最后一题——你之前完全没见过,考的就是真实水平。
4.4 知识体系总览
下面这张图,是我自己总结的数据窥探偏差的完整知识结构。你可以把它当作一个检查清单:
4.5 避坑指南
最后,分享几个我踩过的坑,希望能帮你省点学费:
- 不要用未来函数:比如在回测中用了「当天收盘价」来计算开盘信号。我见过有人用 pandas 的 shift(-1) 不小心把未来数据拉回来了。
- 不要在同一个数据集上反复优化:每次优化都会增加过拟合风险。如果你忍不住,至少用交叉验证来评估。
- 记录所有尝试:包括失败的。这样你才能知道自己的策略到底是在「发现规律」还是在「拟合噪声」。
- 实盘前做一次「盲测」:把策略代码交给同事,让他用他不知道的数据跑一次。结果出来之前,你俩都不知道好坏。
我曾经花三个月优化一个高频策略,回测曲线漂亮得像教科书。结果实盘第一天就亏了 2%。后来才发现,我在回测里用了未来 tick 数据来计算买卖点。嗯,从那以后,我每次回测前都会问自己一句:「这个数据,我当时真的能拿到吗?」
记住:回测的目的是发现策略的弱点,而不是证明策略有多牛。如果你发现回测结果好得不像真的——那它很可能就是假的。