2. 数据窥探偏差:你看到的只是噪音
做量化回测这么多年,我见过最坑的事,就是有人拿着历史数据反复调参,最后搞出一条完美曲线。然后实盘一跑,直接崩盘。
为什么会这样?说白了,你看到的那些「规律」,很可能只是噪音。
这就是我们今天要聊的——数据窥探偏差。我个人习惯叫它「偷看答案式回测」。你想想看,考试前先看了答案,再去做题,能不准吗?但真上了考场,原形毕露。
2.1 什么是数据窥探偏差
数据窥探偏差,英文叫 data snooping bias。它指的是你在回测过程中,有意或无意地使用了未来信息,或者过度拟合了历史数据中的随机模式。
我在项目中遇到过一位同事,他花了三个月调参,回测年化收益做到 80%。我当时就觉得不对劲。后来一查,他的策略里有一个条件:if close > ma20 and volume > mean(volume, 50)。问题出在哪?他用的 mean(volume, 50) 是未来 50 天的均值。嗯,这相当于开着上帝视角做交易。
数据窥探偏差主要有两种表现形式:
- 显性窥探:直接使用了未来数据,比如用当天的收盘价去预测当天的涨跌。
- 隐性窥探:反复在同一个数据集上测试、调参,直到找到「最优」参数。这其实也是一种过拟合。
2.2 一个真实的教训
我曾经帮一个团队 review 策略。他们的策略逻辑很简单:当价格突破布林带上轨时做多,跌破下轨时做空。回测结果非常漂亮,夏普比率 3.5。
我多问了一句:「你们回测了多少次?」
对方回答:「大概试了 200 组参数吧。」
问题就在这里。200 组参数里,总有一组能跑出好结果。这就像你扔 200 次骰子,总有一次能扔出六个六。但你不能说「我扔骰子的技术很好」。
后来我让他们用样本外数据验证,结果夏普直接掉到 0.3。嗯,这才是真实水平。
2.3 如何识别数据窥探
我个人总结了几个判断标准,你可以对照着检查自己的回测:
| 特征 | 正常回测 | 存在数据窥探 |
|---|---|---|
| 参数数量 | 3-5 个以内 | 10 个以上 |
| 回测次数 | 明确记录,有限次 | 反复调参,无记录 |
| 样本外表现 | 与样本内接近 | 大幅下降 |
| 策略逻辑 | 简单、可解释 | 复杂、难以理解 |
你想想看,如果一个策略需要 20 个参数才能赚钱,那它大概率是在拟合噪音。真正的市场规律,往往可以用简单的逻辑描述。
2.4 数据窥探的核心逻辑
为了让你更直观地理解,我画了一张图。这张图展示了数据窥探偏差是如何一步步侵蚀回测结果的:
这张图想表达的是:数据窥探偏差不是一步到位的,它是一个循环。你调一次参数,发现效果不错,再调一次,效果更好。直到你调出一个「完美」的结果。但这个过程,其实是在不断学习噪音。
2.5 如何避免数据窥探
说完了问题,咱们聊聊怎么解决。我总结了几个实战方法:
方法一:严格划分样本内和样本外
这是最基础的做法。把数据分成两部分:
- 样本内(训练集):用于开发和调参
- 样本外(测试集):用于最终验证
我个人习惯按时间划分,比如前 70% 做样本内,后 30% 做样本外。注意,样本外数据在调参阶段绝对不能碰。
方法二:限制调参次数
我建议你在开始回测前,先定好规则:最多调参多少次?比如 50 次。超过这个次数,不管结果如何,都停下来。
为什么要这么做?因为调参次数越多,你找到「假规律」的概率就越大。这就像做统计检验,你测 100 次,总有一次 p 值小于 0.05。
方法三:使用交叉验证
对于时间序列数据,传统的 K 折交叉验证不太适用。我推荐用滚动窗口验证:
# 滚动窗口验证示例
import pandas as pd
import numpy as np
def rolling_window_validation(data, window_size=500, step=100):
"""
滚动窗口验证
window_size: 训练窗口大小
step: 滚动步长
"""
results = []
for start in range(0, len(data) - window_size, step):
train = data.iloc[start:start + window_size]
test = data.iloc[start + window_size:start + window_size + step]
# 在 train 上训练策略
# 在 test 上验证
# 记录结果
results.append({
'train_start': start,
'train_end': start + window_size,
'test_start': start + window_size,
'test_end': start + window_size + step,
'sharpe': np.random.uniform(0.5, 2.0) # 实际计算替换这里
})
return pd.DataFrame(results)
# 使用示例
# df_results = rolling_window_validation(price_data)
# print(f"平均夏普比率: {df_results['sharpe'].mean():.2f}")
这种方法的好处是,你能看到策略在不同市场环境下的表现。如果某个窗口表现特别差,那说明策略可能不稳定。
方法四:保持策略简单
嗯,这一点很重要。我见过太多人把策略搞得很复杂,加了一堆条件、一堆过滤器。结果呢?回测好看,实盘就崩。
我个人有个原则:如果一个策略不能用三句话讲清楚,那它大概率有问题。
2.6 一个简单的自查清单
每次做完回测,我都会问自己几个问题。你也可以试试:
- 我有没有在回测过程中看过样本外数据?
- 我调了多少次参数?有没有记录?
- 策略逻辑是否简单可解释?
- 如果换一段数据,策略还能赚钱吗?
- 我是不是在追求「完美」的回测曲线?
如果以上任何一个问题的答案是「是」,那你就要小心了。数据窥探偏差可能已经在侵蚀你的回测结果。
2.7 写在最后
数据窥探偏差,说白了就是「自己骗自己」。你看到的那些漂亮的回测曲线,可能只是噪音的幻影。
我在刚开始做量化的时候也犯过这个错。那时候调了一个策略,回测年化 120%,兴奋得睡不着觉。结果实盘一个月亏了 15%。嗯,从那以后,我再也不敢相信「完美」的回测了。
记住一句话:回测的唯一目的,是发现策略的弱点,而不是证明它有多强。
公众号:蓝海资料掘金营,微信deep3321