Caloogy 文章
English

多重检验与因子动物园

日期
2025 年 3 月 4 日
系列
量化
阅读
4 分钟
主题
QuantMultiple testingFactor zooDeflated sharpe ratio

一句话版本

学术界和业界已经"发现"了数以百计的因子,有人戏称为因子动物园。问题是:当所有人都在同一份历史数据上反复寻找规律,按常规的"t 值大于 2 就算显著"的标准,纯靠运气也会冒出大量"显著"的因子。 应对的办法是承认自己做了多少次尝试,并相应地提高门槛:Harvey、Liu、Zhu(2016)建议新因子的 t 值至少要超过 3;Bailey 与 López de Prado 提出的 Deflated Sharpe Ratio 则把"试了多少次"直接纳入夏普比率的显著性检验。

免责

本文仅为技术科普,不构成任何投资建议。文中代码使用模拟数据。


第一层:通俗理解

1.1 抛硬币冠军

让 1000 个人每人抛 10 次硬币,统计正面次数。几乎一定会有人抛出 9 次或 10 次正面。他是"抛硬币天才"吗?显然不是,只是人数够多,总会有人碰巧很极端。

量化研究中:

1.2 因子动物园

现象 说明
数量爆炸 Harvey、Liu、Zhu 在 2016 年的论文中整理了三百多个已发表的因子,此后还在增加
复现危机 后续大规模复现研究发现,相当一部分已发表的因子在更严格的检验、更长的样本或更新的数据中无法复现
发表偏差 显著的结果更容易发表,不显著的结果"进了抽屉",没人知道全世界一共试了多少次
发表后衰减 研究发现,因子在论文发表后平均收益明显下降,部分原因是原本的结果有运气成分,部分原因是被市场套利

第二层:校正方法

2.1 提高 t 值门槛

方法 思路 特点
Bonferroni 做了 MM 次检验,显著性水平改为 α/M\alpha / M 最保守,控制"至少一次误报"的概率
Holm Bonferroni 的逐步改进版 稍宽松,同样控制误报
BHY / FDR 控制"被判为显著的结果中,假的比例" 更适合探索性研究
Harvey–Liu–Zhu 门槛 综合考虑整个领域的检验次数 建议新因子 t > 3.0

t 值从 2 提高到 3,听起来不多,但对应的 p 值从约 0.05 降到约 0.003,要求严格了十几倍。

2.2 Deflated Sharpe Ratio(DSR)

夏普比率的显著性检验需要考虑三件事:

  1. 试了多少次:试的次数越多,噪声中能找到的最大夏普越高;
  2. 样本长度:样本越短,夏普估计越不可靠;
  3. 收益分布的形状:偏度为负、尾部厚的策略,夏普比率被高估。

DSR 的思路是:先算出"在 NN 次纯噪声尝试中,夏普比率的最大值期望是多少"(SR0SR_0),再检验观测到的夏普是否显著高于 SR0SR_0:

SR0=V[(1−γ) Φ−1 ⁣(1−1N)+γ Φ−1 ⁣(1−1Ne)],SR_0 = \sqrt{V}\left[(1-\gamma)\,\Phi^{-1}\!\Big(1 - \frac{1}{N}\Big) + \gamma\,\Phi^{-1}\!\Big(1 - \frac{1}{N e}\Big)\right],

DSR=Φ ⁣((SR^−SR0)T−11−γ^3SR^+γ^4−14SR^2),\text{DSR} = \Phi\!\left(\frac{(\widehat{SR} - SR_0)\sqrt{T-1}}{\sqrt{1 - \hat\gamma_3 \widehat{SR} + \frac{\hat\gamma_4 - 1}{4}\widehat{SR}^2}}\right),

其中 VV 是各次尝试之间夏普比率的方差,γ≈0.5772\gamma \approx 0.5772 是欧拉常数,γ^3,γ^4\hat\gamma_3, \hat\gamma_4 是收益的偏度和峰度,TT 是样本长度。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
原则 同一个数字的可信度取决于找到它的过程;记录每一次尝试