一句话版本
学术界和业界已经"发现"了数以百计的因子,有人戏称为因子动物园。问题是:当所有人都在同一份历史数据上反复寻找规律,按常规的"t 值大于 2 就算显著"的标准,纯靠运气也会冒出大量"显著"的因子。 应对的办法是承认自己做了多少次尝试,并相应地提高门槛:Harvey、Liu、Zhu(2016)建议新因子的 t 值至少要超过 3;Bailey 与 López de Prado 提出的 Deflated Sharpe Ratio 则把"试了多少次"直接纳入夏普比率的显著性检验。
免责
本文仅为技术科普,不构成任何投资建议。文中代码使用模拟数据。
第一层:通俗理解
1.1 抛硬币冠军
让 1000 个人每人抛 10 次硬币,统计正面次数。几乎一定会有人抛出 9 次或 10 次正面。他是"抛硬币天才"吗?显然不是,只是人数够多,总会有人碰巧很极端。
量化研究中:
- 一个研究员测试 100 个因子,每个都用 5% 的显著性水平;
- 即使所有因子都毫无作用,平均也会有 5 个"显著";
- 如果只报告这 5 个,就制造出了 5 个虚假的发现。
1.2 因子动物园
| 现象 | 说明 |
|---|---|
| 数量爆炸 | Harvey、Liu、Zhu 在 2016 年的论文中整理了三百多个已发表的因子,此后还在增加 |
| 复现危机 | 后续大规模复现研究发现,相当一部分已发表的因子在更严格的检验、更长的样本或更新的数据中无法复现 |
| 发表偏差 | 显著的结果更容易发表,不显著的结果"进了抽屉",没人知道全世界一共试了多少次 |
| 发表后衰减 | 研究发现,因子在论文发表后平均收益明显下降,部分原因是原本的结果有运气成分,部分原因是被市场套利 |
第二层:校正方法
2.1 提高 t 值门槛
| 方法 | 思路 | 特点 |
|---|---|---|
| Bonferroni | 做了 次检验,显著性水平改为 | 最保守,控制"至少一次误报"的概率 |
| Holm | Bonferroni 的逐步改进版 | 稍宽松,同样控制误报 |
| BHY / FDR | 控制"被判为显著的结果中,假的比例" | 更适合探索性研究 |
| Harvey–Liu–Zhu 门槛 | 综合考虑整个领域的检验次数 | 建议新因子 t > 3.0 |
t 值从 2 提高到 3,听起来不多,但对应的 p 值从约 0.05 降到约 0.003,要求严格了十几倍。
2.2 Deflated Sharpe Ratio(DSR)
夏普比率的显著性检验需要考虑三件事:
- 试了多少次:试的次数越多,噪声中能找到的最大夏普越高;
- 样本长度:样本越短,夏普估计越不可靠;
- 收益分布的形状:偏度为负、尾部厚的策略,夏普比率被高估。
DSR 的思路是:先算出"在 次纯噪声尝试中,夏普比率的最大值期望是多少"(),再检验观测到的夏普是否显著高于 :
其中 是各次尝试之间夏普比率的方差, 是欧拉常数, 是收益的偏度和峰度, 是样本长度。DSR 可以理解为"这个策略的真实夏普大于零的概率"。
第三层:代码实现
import numpy as np
from scipy.stats import norm, skew, kurtosis
EULER = 0.5772156649
def expected_max_sharpe(n_trials, var_sr):
"""N 次独立尝试中,纯噪声下夏普比率最大值的期望(Bailey & López de Prado)"""
return np.sqrt(var_sr) * ((1 - EULER) * norm.ppf(1 - 1 / n_trials)
+ EULER * norm.ppf(1 - 1 / (n_trials * np.e)))
def deflated_sharpe(returns, n_trials, var_sr):
T = len(returns)
sr = returns.mean() / returns.std() # 非年化(每期)夏普
sr0 = expected_max_sharpe(n_trials, var_sr)
g3, g4 = skew(returns), kurtosis(returns, fisher=False)
z = (sr - sr0) * np.sqrt(T - 1) / np.sqrt(1 - g3 * sr + (g4 - 1) / 4 * sr ** 2)
return sr, sr0, norm.cdf(z)
rng = np.random.default_rng(3)
T = 252 * 3
r = rng.normal(0.0008, 0.01, T) # 一个"看起来不错"的策略
print("年化夏普:", round(r.mean() / r.std() * np.sqrt(252), 2))
for n in [2, 10, 100, 1000]:
sr, sr0, dsr = deflated_sharpe(r, n, var_sr=1 / T)
print(f"尝试次数 {n:>5}: 噪声最大夏普(年化)={sr0*np.sqrt(252):.2f} DSR={dsr:.3f}")
输出:
年化夏普: 2.1
尝试次数 2: 噪声最大夏普(年化)=0.30 DSR=0.999
尝试次数 10: 噪声最大夏普(年化)=0.91 DSR=0.979
尝试次数 100: 噪声最大夏普(年化)=1.46 DSR=0.862
尝试次数 1000: 噪声最大夏普(年化)=1.88 DSR=0.645
(这里 var_sr = 1/T 是一个简化假设:各次尝试的夏普估计只有抽样误差的方差。实际使用时应该用你真实尝试过的那些策略的夏普比率的样本方差。)
3.1 解读
同一个三年年化夏普 2.1 的策略:
| 如果它是…… | 结论 |
|---|---|
| 唯一一次尝试 | 几乎肯定是真的(DSR ≈ 1) |
| 100 次尝试中最好的 | 仍然大概率真实,但纯噪声也能产生 1.46 的最大夏普 |
| 1000 次尝试中最好的 | 噪声最大值期望已达 1.88,DSR 只有 0.65,相当可疑 |
同一个数字,可信度取决于你找它的过程。这就是为什么严肃的量化团队要求记录每一次尝试。
3.2 实务建议
| 建议 | 说明 |
|---|---|
| 记录尝试次数 | 包括所有参数组合、所有放弃的想法 |
| 预先注册 | 先写下假设、检验方法和通过标准,再看数据 |
| 提高门槛 | 新因子 t > 3;用 DSR 而不是原始夏普做决定 |
| 要求经济逻辑 | 有合理解释的因子,比纯数据挖掘出来的更可能持续 |
| 用新数据检验 | 新市场、新时间段、新资产类别中是否同样有效 |
| 警惕自动化挖掘 | 机器和 AI Agent 可以在一夜之间尝试上万次,问题会被成千倍放大(见《Agent挖因子》) |
总结
| 层次 | 要点 |
|---|---|
| 问题 | 试的次数多了,噪声里也能找到"显著"的因子 |
| 现象 | 因子动物园:数百个因子,大量无法复现,发表后衰减 |
| 校正 | Bonferroni / Holm / FDR;t > 3;Deflated Sharpe Ratio |
| 原则 | 同一个数字的可信度取决于找到它的过程;记录每一次尝试 |