过拟合识别 — Deflated Sharpe / PBO / Multiple Testing
多重比较问题在量化里的具体形态、Deflated Sharpe Ratio 推导直觉、PBO 框架、IS/OOS 的常见误用、AQR 风格的 robust check 清单
日期: 2026-06-02 方向: 回测严谨性 / 过拟合 阶段: Phase 1: 基础与工具链 标签: #Overfitting #DeflatedSharpe #PBO #MultipleTesting #InSample #OutOfSample #LopezDePrado
今日目标
| 类型 | 内容 |
|---|---|
| 学习 | 多重比较问题在量化里的具体形态、Deflated Sharpe Ratio 推导直觉、PBO 框架、IS/OOS 的常见误用、AQR 风格的 robust check 清单 |
| 实操 | 给 Day 6 的 SMA 参数扫描结果套上 DSR 校正;写一个简化版 PBO 计算函数;对当前两条策略做「试错次数」诚实清点 |
| 反思 | 过去 23 天有没有不自觉做 multiple testing、有没有「悄悄换 OOS 边界」的行为 |
| 产出 | TR-DAY24 笔记 + tr_lib/diagnostics/dsr.py + tr_lib/diagnostics/pbo.py + 一张 SMA 扫描结果的 DSR 校正前后对比表 |
一、过拟合在量化里的本质:试错和挑选才是真问题
新手以为过拟合是「模型参数太多」。在量化里,过拟合最常见的来源根本不是模型本身,而是研究者:
一个研究者跑了 1,000 个策略变种,挑出回测 Sharpe 最高的那一个写进 paper,剩下 999 个被悄悄删了 — 这才是真正的过拟合机制。
机器学习领域有个对应概念叫「researcher degree of freedom」,量化领域称为 backtest overfitting。它的恶毒之处在于:
- 每一步都看起来很合理:换个均线窗口、加一个止损、试一下不同 universe — 任何一步都不是「作弊」
- 没有任何人能审计你试过多少次:你的脑子里跑过的策略不会进 git log
- 样本外验证看起来没问题:因为「样本外」是你挑赢家之后才被划出来的
López de Prado 在 Advances in Financial Machine Learning 里有一句话直接到刺人:
"Most published results in finance are likely false."(金融领域大部分已发表结果可能都是假的。)
他不是在说大家造假,他在说没有人对 multiple testing 做修正。这和心理学领域 2015 年的「replication crisis」是同一类问题。
1.1 量化研究者每天都在做的 multiple testing(自检清单)
| 行为 | 是不是 multiple testing | 一般试了几次 |
|---|---|---|
| 参数扫描(slow=20,30,40,50,60) | 是 | 5-20 |
| 换一个 universe(SPY → QQQ → IWM) | 是 | 3-5 |
| 试不同 holding period(5d/10d/20d) | 是 | 3-5 |
| 加 / 不加止损 | 是 | 2 |
| 换一个 entry signal(cross vs threshold) | 是 | 2-3 |
| 试不同 transaction cost 假设 | 是 | 2-3 |
| 看回测崩了换个时段重新看 | 是 | 不确定(最危险) |
| 改了 bug 重新跑 | 是(即便正当) | 多次 |
把这些维度乘起来:5 × 3 × 3 × 2 × 2 × 2 × 多次重跑 = 几百次。这就是「我只试了几个参数」这句话背后的真相。
二、多重比较问题:N 个噪声里挑最好的会很「亮」
2.1 玩具实验
假设你有 N 个策略,每一个真实 Sharpe = 0(纯噪声,没有任何 alpha)。每一个策略的样本 Sharpe 估计有标准误 σ_SR ≈ 1/√T(T 个独立样本)。
你挑「这 N 个里最高的那个」,它的样本 Sharpe 期望是多少?
对正态分布的 N 个独立采样取最大值,期望约等于:
$$ \mathbb{E}[\max_{i=1..N} Z_i] \approx \sqrt{2 \log N} $$
| N | E[max Sharpe](年化, T 充分大) | 含义 |
|---|---|---|
| 10 | ~2.15 / √T × √252 | 试 10 个噪声策略,最好那个看起来「不错」 |
| 100 | ~3.03 / √T × √252 | 看起来像「优秀策略」 |
| 1000 | ~3.72 / √T × √252 | 看起来像「世界级策略」 |
| 10000 | ~4.29 / √T × √252 | 看起来像「Renaissance」 |
注意:这些 Sharpe 全部来自 alpha = 0 的噪声。你只是挑了里面最幸运的一个。
2.2 这条公式的实战含义
如果你试了 100 个参数组合,至少有一个会得到 Sharpe ≈ 3 而它的真实 Sharpe 是 0。如果你不做 multiple testing 修正,你会把这个全噪声策略当作 winner 部署到实盘 — 然后亏钱。
经验法则:试 N 个组合后挑最高的,真实 Sharpe ≈ 样本 Sharpe − √(2 log N) / √T × √252
这就是 Deflated Sharpe Ratio 的核心直觉。
三、Deflated Sharpe Ratio (DSR):把试错次数 paste 回去
3.1 公式直觉
López de Prado (2014) 的 DSR 干的事是:
原始问题:观察到 SR_hat = 2.5,这够好吗?
DSR 问的问题:考虑到我试过 N 次、收益分布有偏度/峰度、样本长度 T,
SR_hat 真的显著大于 E[最大噪声 Sharpe] 吗?
公式(保留可读形式,详见 López de Prado 论文):
$$ \text{DSR} = \Phi\left( \frac{(\hat{SR} - \mathbb{E}[\max SR_{\text{noise}}]) \cdot \sqrt{T-1}}{\sqrt{1 - \gamma_3 \hat{SR} + \frac{\gamma_4 - 1}{4}\hat{SR}^2}} \right) $$
其中:
Φ= 标准正态 CDFSR_hat= 样本 Sharpe(年化)γ_3= 收益序列的偏度(skewness)γ_4= 收益序列的峰度(kurtosis,正态 = 3)T= 样本数(一般用日数)E[max SR_noise]= 在 N 次试错下的「最大噪声 Sharpe」期望,约等于(1 - γ_em) Φ^{-1}(1 - 1/N) + γ_em Φ^{-1}(1 - 1/(N·e)),其中 γ_em ≈ 0.5772(Euler-Mascheroni)
3.2 关键 takeaway
| 输入 | 对 DSR 的影响 |
|---|---|
| N 增大 | DSR 减小(试得越多越要打折) |
| T 增大 | DSR 增大(样本越长越可信) |
| 正偏度 γ_3 > 0 | DSR 增大(好策略) |
| 高峰度 γ_4 > 3 | DSR 减小(fat tail 不利) |
| SR_hat 增大 | DSR 增大(前提是其他条件不变) |
3.3 实操判定
| DSR | 判定 |
|---|---|
| < 0.5 | 极可能是 noise,不要部署 |
| 0.5 - 0.8 | 可疑,至少做 walk-forward |
| 0.8 - 0.95 | 边缘,谨慎小仓位 |
| > 0.95 | 大概率真实 edge(仍需 OOS 验证) |
注意 DSR 是个概率(0 到 1),不是 Sharpe,所以 ">0.95" 是说「Sharpe 显著大于噪声峰值的概率 > 95%」。
3.4 N 该怎么填?这是 DSR 的最大争议点
理论上 N 应该是「你为了得到这个 SR 试过的所有独立配置数」。实际操作:
- 诚实下限:参数 grid size × universe 数 × holding period 数 × ...
- 更诚实:再乘以「重跑次数」(每次发现 bug 重跑算一次新的 try)
- 极其诚实:再加上「同事 / KOL 给我看过的策略」(如果你受其启发)
经验:把你「以为的 N」乘 3 比较接近真相。这是因为人脑会自动忘记「失败的尝试」(survivorship bias of your own research)。
四、Probability of Backtest Overfitting (PBO)
4.1 框架(Bailey-Borwin-López de Prado 2014)
DSR 解决「单个策略 SR 在 N 次试错下是否显著」,PBO 解决一个更直接的问题:
我从 M 个策略里挑「In-Sample 最好」的那个 — 它在 Out-of-Sample 也最好的概率是多少?
如果策略是真的有 edge,IS 最好的那个 OOS 也大概率好。如果都是噪声,IS 最好的那个 OOS 排名是随机的 — 50% 概率会落到下半区。
PBO = P(选中策略的 OOS 排名跌进下半)
4.2 Combinatorial Symmetric Cross-Validation (CSCV)
具体计算流程:
1. 把 T 期数据切成 S 个等长 sub-period(S 通常 = 16)
2. 从 S 个中选一半作为 IS,另一半作为 OOS — 共 C(S, S/2) 种切法
3. 对每种切法:
a. 在 IS 上跑所有 M 个策略,找出 IS Sharpe 最高的策略 i*
b. 看策略 i* 在 OOS 上的排名 r*
c. 计算 logit: λ = log(r* / (M - r*))
4. PBO = P(λ < 0) = OOS 排名 < median 的比例
直觉:如果 PBO = 0.5,那 IS winner 在 OOS 的位置就是抛硬币 — 完全过拟合。
4.3 判定阈值
| PBO | 判定 |
|---|---|
| < 0.1 | 几乎不过拟合,IS winner 大概率真 |
| 0.2 - 0.4 | 中度过拟合 |
| > 0.5 | 显著过拟合,IS 选出来的不可信 |
实际工业界很多「Sharpe 2-3」的策略做完 PBO 是 0.6-0.8 — 这正是为什么大部分发表的因子无法 replicate。
五、In-Sample / Out-of-Sample:被滥用最多的概念
5.1 经典做法(不够用)
[----------------- 全部数据 -----------------]
[------- 70% IS(参数调优) -------][- 30% OOS -]
- 在 IS 上找最佳参数
- 把这套参数搬到 OOS 跑一次
- 看 OOS Sharpe 是否「也不错」
5.2 这种做法的三个常见 bug
Bug 1: Peek at OOS(偷看样本外)
- 你看了一眼 OOS 表现不好
- 回去改 IS 上的参数 / 加 feature
- 重跑 OOS
- 这时 OOS 已经被你间接 fit 了,不再是「样本外」
一旦你看过 OOS 任何一眼,OOS 就被污染了。
Bug 2: OOS 太短
- 30% 数据可能只有 1-2 年
- 单次划分,运气因素仍然巨大
- 解法:k-fold CV / walk-forward(Day 25)
Bug 3: OOS 不代表未来
- 即便严格不偷看,OOS 也是历史
- 真正的「样本外」是未来还没发生的市场
- Paper trading 才是真 OOS(虽然没成本)
5.3 进阶做法预告
| 方法 | 简介 | 何时学 |
|---|---|---|
| k-fold CV | 数据切 k 份,轮流当 OOS | Day 25 |
| Walk-forward | 滚动窗口,模拟真实部署 | Day 25 重点 |
| CSCV / PBO | 组合式 CV,专门对抗 multiple testing | 今天讲了 |
| Purged / Embargoed CV | 解决金融数据 leakage | Day 26-27 |
六、过拟合的具体「症状」(怎么用眼睛看出来)
不一定每个症状都要算 DSR/PBO — 有些过拟合是视觉上就能识别的。
6.1 症状 1:参数对窗口高度敏感
slow=50: Sharpe = 1.8
slow=45: Sharpe = 0.3
slow=55: Sharpe = -0.2
健康策略的参数响应应该是平台型(plateau):参数 ±20% 都能 work。如果只有最大值附近能 work,那个最大值就是噪声峰。
6.2 症状 2:Sharpe 平面陡峭
把双参数(slow, fast)画成 heatmap:
| 图形 | 含义 |
|---|---|
| 缓慢起伏的山地 | 健康,参数稳健 |
| 单点突起 + 周围全是水 | 过拟合,唯一的「峰」是 noise |
| 多个独立峰 | 通常也是 overfit |
Day 6 SMA 扫描的 heatmap 我应该再翻出来用这个视角重看。
6.3 症状 3:IS Sharpe >> OOS Sharpe
| IS Sharpe | OOS Sharpe | 解读 |
|---|---|---|
| 2.5 | 2.2 | 健康(小幅 deflation 正常) |
| 2.5 | 1.5 | 中度 overfit |
| 2.5 | 0.4 | 重度 overfit,IS 是 noise |
| 2.5 | -0.3 | 灾难,反向 fit |
经验:OOS Sharpe ≈ 0.5 × IS Sharpe 是常态(这就是为什么实盘普遍低于回测)。
6.4 症状 4:策略「故事」过于复杂
入场:
- MA10 > MA50
- 且 RSI < 70
- 且 VIX < 25
- 且非财报前 3 天
- 且当日 volume > 20d avg × 1.2
- 且非月末
- 且... (继续加 7 个 filter)
每加一个 filter 都让 backtest 看起来更好,但本质上你在找一组凑巧匹配历史的过滤器。
好策略的 rule 数 ≤ 3。Renaissance 的策略据传也只有少量核心信号,剩下都是工程实现。
6.5 症状 5:自由参数 / 数据量 比例过高
经验法则:
$$ \frac{\text{free parameters}}{\text{independent observations}} < 0.001 $$
如果你的策略有 10 个参数(5 个均线窗口 + 3 个阈值 + 2 个 stop),跑在 5 年日线 = 1,260 天。比例 10/1260 ≈ 0.008,已经过高。
「独立观察」≠ 日线数。如果策略持仓 20 天,独立观察更接近 1260/20 = 63 — 比例变成 10/63 = 0.16,完全过拟合。
七、三种 anti-overfitting 工具(之上的层级)
7.1 Walk-forward analysis(Day 25 详细学)
时间 →
[--- train 252d ---][test 63d]
[--- train 252d ---][test 63d]
[--- train 252d ---][test 63d]
...
每个 test 段都是真正的样本外。串起来就是「如果我每季度重新优化参数,实盘会发生什么」。
7.2 减少 free parameters
Day 6 教训重提:SMA 双参数扫描,固定 fast=10 + 只调 slow,比 fast/slow 双扫描更不容易 overfit。每砍掉一个参数,N 是几何级数降低。
7.3 Regularization(feature selection 阶段)
如果你用机器学习选 feature:
- Lasso (L1):自动让大部分系数变成 0,强制 sparsity
- Ridge (L2):把系数压向 0 但不为 0,平滑
- Elastic Net:两者混合
对应到非 ML 的策略:用「贝叶斯先验」给极端参数赋低概率,等同 regularization。
八、Python 实现:DSR + 简化版 PBO
8.1 DSR 计算
# tr_lib/diagnostics/dsr.py
import numpy as np
from scipy.stats import norm
EULER_MASCHERONI = 0.5772156649
def expected_max_sr(N: int) -> float:
"""
López de Prado expected max Sharpe under null (alpha=0).
N = number of independent trials.
"""
if N <= 1:
return 0.0
gamma = EULER_MASCHERONI
# E[max] ≈ (1-γ) Φ^-1(1 - 1/N) + γ Φ^-1(1 - 1/(N·e))
term1 = (1 - gamma) * norm.ppf(1 - 1.0 / N)
term2 = gamma * norm.ppf(1 - 1.0 / (N * np.e))
return term1 + term2
def deflated_sharpe(returns: np.ndarray, n_trials: int,
annualization: int = 252) -> dict:
"""
Compute Deflated Sharpe Ratio.
Parameters
----------
returns : daily returns of the selected strategy
n_trials : how many strategies you tried (be honest!)
annualization : 252 for daily, 12 for monthly
Returns
-------
dict with sr_hat, sr_expected_max, dsr (probability)
"""
T = len(returns)
if T < 30:
raise ValueError("Need at least 30 observations")
mu = returns.mean()
sigma = returns.std(ddof=1)
sr_hat_daily = mu / sigma
sr_hat = sr_hat_daily * np.sqrt(annualization)
# higher moments
centered = returns - mu
skew = (centered ** 3).mean() / (sigma ** 3)
kurt = (centered ** 4).mean() / (sigma ** 4) # 3 for normal
# Expected max under null, scaled to annualized SR
# (the null is daily SR ~ N(0, 1/sqrt(T)); annualize at the end)
e_max_daily = expected_max_sr(n_trials) / np.sqrt(T)
e_max = e_max_daily * np.sqrt(annualization)
# DSR formula
numerator = (sr_hat_daily - e_max_daily) * np.sqrt(T - 1)
denom = np.sqrt(1 - skew * sr_hat_daily +
(kurt - 1) / 4.0 * sr_hat_daily ** 2)
z = numerator / denom
dsr = norm.cdf(z)
return {
"sr_hat": sr_hat,
"sr_expected_max": e_max,
"skew": skew,
"kurt": kurt,
"T": T,
"n_trials": n_trials,
"dsr": dsr,
}
if __name__ == "__main__":
# Sanity check: pure noise, N=100 trials, pick best — DSR 应该很低
np.random.seed(42)
best_sr = -np.inf
best_returns = None
for _ in range(100):
r = np.random.normal(0, 0.01, 1000) # 1000 days, σ=1%
sr = r.mean() / r.std() * np.sqrt(252)
if sr > best_sr:
best_sr = sr
best_returns = r
print(f"Best of 100 noise strategies, naive SR = {best_sr:.2f}")
result = deflated_sharpe(best_returns, n_trials=100)
print(result)
# 期望输出:naive SR 看起来不错,DSR 接近 0.5(噪声)
8.2 简化版 PBO(CSCV)
# tr_lib/diagnostics/pbo.py
import numpy as np
from itertools import combinations
def pbo_cscv(returns_matrix: np.ndarray, S: int = 16) -> dict:
"""
Combinatorial Symmetric Cross-Validation PBO.
Parameters
----------
returns_matrix : (T, M) ndarray of daily returns
M strategies (e.g. M=21 if you tested slow=20,25,...,120)
S : number of sub-periods (must be even, default 16)
Returns
-------
dict with pbo, logits, ...
"""
T, M = returns_matrix.shape
assert S % 2 == 0, "S must be even"
chunk_size = T // S
# Truncate to clean S chunks
returns_matrix = returns_matrix[:S * chunk_size]
# Reshape to (S, chunk_size, M)
chunks = returns_matrix.reshape(S, chunk_size, M)
logits = []
half = S // 2
for is_idx in combinations(range(S), half):
is_set = set(is_idx)
oos_idx = [i for i in range(S) if i not in is_set]
is_returns = chunks[list(is_idx)].reshape(-1, M)
oos_returns = chunks[oos_idx].reshape(-1, M)
is_sr = is_returns.mean(axis=0) / (is_returns.std(axis=0) + 1e-12)
oos_sr = oos_returns.mean(axis=0) / (oos_returns.std(axis=0) + 1e-12)
i_star = np.argmax(is_sr) # IS winner
rank_oos = (oos_sr.argsort().argsort()[i_star] + 1) # 1-indexed
# logit
lam = np.log(rank_oos / (M + 1 - rank_oos))
logits.append(lam)
logits = np.array(logits)
pbo = (logits < 0).mean()
return {"pbo": pbo, "logits": logits, "n_splits": len(logits)}
if __name__ == "__main__":
# Sanity: 21 个 SMA 参数的日收益矩阵(假数据演示)
np.random.seed(0)
T, M = 2000, 21
fake = np.random.normal(0, 0.01, (T, M)) # 全噪声
result = pbo_cscv(fake, S=16)
print(f"PBO on noise = {result['pbo']:.2%}")
# 期望 ≈ 50%(纯噪声 = 完全过拟合)
8.3 把它套到 Day 6 SMA 扫描上
Day 6 SMA 双参数扫描的输出大致结构是 (T, n_fast, n_slow) 三维收益。把它 reshape 成 (T, n_fast * n_slow),每一列就是一个策略,再喂给 pbo_cscv 即可。
from tr_lib.diagnostics.dsr import deflated_sharpe
from tr_lib.diagnostics.pbo import pbo_cscv
import numpy as np
# 假设 Day 6 已经保存了
sma_returns = np.load("artifacts/day6_sma_grid_returns.npy") # (T, n_fast, n_slow)
T, nf, ns = sma_returns.shape
flat = sma_returns.reshape(T, nf * ns) # (T, M)
M = nf * ns
# 找到 IS Sharpe 最高的那组
sr_each = flat.mean(axis=0) / flat.std(axis=0) * np.sqrt(252)
best_i = np.argmax(sr_each)
print(f"Best naive SR among {M} configs = {sr_each[best_i]:.2f}")
# DSR 校正
result = deflated_sharpe(flat[:, best_i], n_trials=M)
print(f"DSR after deflating by N={M}: {result['dsr']:.2%}")
# PBO
pbo = pbo_cscv(flat, S=16)
print(f"PBO (overfit probability): {pbo['pbo']:.2%}")
预期结果(粗略推测,等明天实跑):
- Naive SR 可能 1.5-2.0
- E[max SR_noise] for N=49 ≈ 0.4 年化
- 实际 DSR 可能落在 0.6-0.9(说明 SMA 的 edge 真假参半)
- PBO 可能 0.3-0.5(边缘过拟合)
九、经验法则(值得贴墙上)
| 法则 | 内容 |
|---|---|
| 1 | 如果回测 Sharpe > 2,乘 0.5 是大概率会发生的实盘表现 |
| 2 | 试过 > 50 个参数组合,至少一个会假阳性 |
| 3 | 简单策略 + 经济学故事 >> 复杂策略 + 数据驱动 |
| 4 | "Less is more" 在量化是字面意义上的 |
| 5 | OOS 一旦被你看过一次,它就不再是 OOS |
| 6 | 真正的 OOS 只有 paper trade + live,回测里没有 |
| 7 | 自由参数 / 独立观察 比例必须 < 0.001 |
| 8 | 报告策略时同时报告 N(试过的次数) 和 DSR,否则数字没意义 |
十、AQR 的「robust check」清单
AQR 等老牌量化基金内部对策略的尽调有一套机械化的稳健性测试。我把核心的列出来:
| 维度 | 测试 | 通过标准 |
|---|---|---|
| 时间稳健 | 在 2007-2009 (GFC) 表现 | 不爆仓 / Sharpe 不为负 |
| 时间稳健 | 在 2020 (COVID) 表现 | 不爆仓 |
| 时间稳健 | 在 2022 (rate hike) 表现 | 不爆仓 |
| 时间稳健 | 在 2024-2025 (AI bubble) 表现 | 没靠这个时段拉高 |
| Universe 稳健 | 大盘 (S&P 500) vs 中盘 (Russell 2000) | 方向一致,幅度可不同 |
| 国家稳健 | 美股 vs 欧股 (STOXX 600) vs 日股 (TOPIX) | 至少 2/3 同号 |
| 参数稳健 | 参数 ±30% | Sharpe 下降幅度 < 50% |
| 频率稳健 | 5min vs 1h vs 1d bar | 关键信号在多个频率都有 |
| transaction cost 稳健 | 把假设 cost 翻倍 | 仍然 Sharpe > 1 |
| shorting friction | 加上 borrow cost 50bp | 仍然 Sharpe > 0.5 |
| 存活偏差 | universe 是否包括退市股 | 是 |
| 数据修正 | 用 point-in-time data 而非 latest | 是(避免 lookahead) |
判定标准:以上 12 项里通过 ≥ 10 项才考虑实盘。我自己当前的「双 SMA」策略大概只过 6-7 项 — 这就是为什么 Day 6 后我没急着实盘。
十一、当前两条策略的诚实清点
把过去 23 天我(用 IBKR Paper / 回测框架)触碰过的策略和试错次数清一遍:
| 策略 | 试错次数 N(保守估计) | 当前 IS Sharpe | DSR 预估 |
|---|---|---|---|
| SMA cross (双均线) | 49 (7×7 grid) | ~1.5 | 0.7 (待校正后确认) |
| Momentum factor (Day 10) | ~8(不同 lookback × universe) | ~0.9 | 0.6 |
| Mean reversion (Day 11) | ~6 | ~0.6 | 0.5 |
| Quality factor (Day 12) | ~5 | ~0.4 | 0.4 |
诚实说,没有一个达到 DSR > 0.95。这是预期的,说明 Phase 1 任务正确 — 现在是学方法论而不是部署。Phase 2 (Day 26-60) 才该出现 DSR > 0.95 的候选。
关键防火墙:Phase 1 结束前不动真金。
十二、PM 视角:把 multiple testing 迁移到产品工作
这套思维方式对 PM 工作有直接迁移:
12.1 A/B test 的 multiple testing 问题
PM 跑 1 个 A/B test,p=0.04 → "stat sig,部署!"
PM 同时跑 20 个 A/B test,至少 1 个 p<0.05 是偶然 (5% × 20 = 100%)
→ 那个 "winner" 上线后 doesn't replicate
这就是「我们做了 A/B,数据显示有提升,但上线后没看到效果」的常见原因。
12.2 修正方法(PM 版本)
| 方法 | 实施 |
|---|---|
| Bonferroni 修正 | 同时跑 N 个 test,要求 p < 0.05/N |
| FDR (Benjamini-Hochberg) | 控制 false discovery rate,比 Bonferroni 宽松 |
| Pre-registration | 提前写下「我要 test 哪些 metric」,事后只看这些 |
| Hold-out group | 永远留一个 group 不接触任何 treatment,作为 sanity baseline |
12.3 工作场景里我会立刻应用的两条
- 季度 review 时问产品经理:「你这个赢得 A/B 的 feature,背后 team 总共试了几个 variant?」如果是 20+,他的 winner 大概率不 replicate。
- 决定 feature 上线时:除了「stat sig 且 effect size 足够」外,再加一条「未来 4 周不看数据,4 周后再看一次 hold-out group」— 这就是 walk-forward 思想在产品工作里的应用。
12.4 招聘里也用得上
面试候选人:「告诉我一个你做过的 successful 决策」vs「告诉我你做过的所有相关决策,包括失败的」。第二种问法在过滤 multiple testing 偏差 — 只听 winner 永远会高估对方能力。
十三、明日预告
Day 25: Walk-forward Analysis 与 Purged K-Fold CV
- Walk-forward 的 3 种 schedule:rolling / anchored / expanding
- 数据切分时的 embargo period 设计
- 金融数据特有的 leakage:label overlap、feature lookahead
- López de Prado 的 Purged K-Fold CV(解决标签重叠)
- 把今天的 SMA 例子改造成 walk-forward 版本,看 OOS 性能
- 产出:
tr_lib/diagnostics/walk_forward.py
把今天的 DSR / PBO 当作「截面工具」(一次性评估),Day 25 的 walk-forward 是「时序工具」(模拟真实部署)。两者配合才是完整 anti-overfitting 工具链。
实际执行记录
启动一项填一项,时间戳 + 卡点。
- [hh:mm] 读 López de Prado Pseudo-mathematics and Financial Charlatanism (2014) — 至少摘要和 Section 3
- [hh:mm] 实现
dsr.py+ sanity check(纯噪声 + N=100 → 看 DSR) - [hh:mm] 实现
pbo.py+ sanity check(纯噪声 → PBO ≈ 50%) - [hh:mm] 把 Day 6 SMA 扫描结果套上 DSR / PBO,记录数值
- [hh:mm] 写「过去 23 天 multiple testing 诚实清点」(本笔记第十一节)
- 卡点 / 学到的:
- …
总字数:约 6,900 字 今日完成度:理论 ✓ / 实操(待跑)/ 笔记 ✓