学习路线 04:回测过拟合 — PBO 框架
7步学习路线:从理解为什么回测会撒谎,到测量过拟合概率,到做出 go/no-go 决策。
目标:完成后,你将理解回测过拟合概率(PBO)如何计算,如何解读,以及如何基于此做出决策。
前置知识:熟悉 CPCV(学习路线 02)和多重检验概念。
第 1 步:这是什么?
你优化了一个交易策略。经过数月工作,你找到一组参数,产生漂亮的回测:Sharpe 2.1,最大回撤 8%,月度收益稳定。
然后你上实盘。三个月后,Sharpe 是 0.4。六个月后,是负的。
发生了什么?
你过拟合了。你测试了如此多的参数组合,找到一个在历史数据上恰好看起来很棒的组合——但那个组合不能泛化到未来数据。
PBO(回测过拟合概率) 是这个风险的正式度量。它问:“考虑到你测试了多少组合,你最佳的组合是假阳性的概率是多少?”
第 2 步:为什么这很重要?
不舒服的数学。假设你测试了 1,000 个参数组合。每个是随机游走(没有真实优势)。在 α=0.05,你会期望 50 个显示”显著”正收益。
其中最佳的 Sharpe 可能是 1.8。
PBO 回答:在所有看起来很棒的组合中,有多少比例在样本外表现糟糕?
如果 PBO = 30%,那么 30% 的情况下,一个在回测中看起来惊艳的参数组合在真实交易中表现不佳。这是 1/3 的失败机会。你会乘坐有 1/3 引擎故障概率的飞机吗?
第 3 步:核心机制
PBO 使用 CPCV(组合清仓交叉验证)作为引擎。想法:
- 枚举所有 2^k - 1 种将数据划分为 train/test 的方式
- 对于每个划分,按训练 Sharpe 和测试 Sharpe 对策略排名
- 计数:高训练排名变成低测试排名的频率是多少?
“高训练、低测试”是过拟合的标志。一个只在特定数据分割上看起来好的策略——不稳健。
第 4 步:关键公式
PBO 定义:
决策阈值:
| PBO | 解读 | 行动 |
|---|---|---|
| < 5% | 低过拟合风险 | 谨慎行事 |
| 5-25% | 中等风险 | 需要额外验证 |
| > 25% | 高风险 | 可能过拟合——拒绝或简化 |
第 5 步:实现
import numpy as np
from itertools import combinations
def compute_pbo_detailed(returns_matrix, k=6, top_pct=0.10, bottom_pct=0.50):
"""
Compute PBO with full analysis output.
"""
n_samples, n_strategies = returns_matrix.shape
samples_per_fold = n_samples // k
full_sharpes = np.array([sharpe_ratio(returns_matrix[:, i])
for i in range(n_strategies)])
train_sharpes = []
test_sharpes = []
train_ranks = []
test_ranks = []
for r in range(1, k + 1):
for train_folds in combinations(range(k), r):
train_mask = np.zeros(k, dtype=bool)
train_mask[list(train_folds)] = True
test_mask = ~train_mask
train_samples = np.zeros(n_samples, dtype=bool)
test_samples = np.zeros(n_samples, dtype=bool)
for fold_idx in range(k):
start = fold_idx * samples_per_fold
end = start + samples_per_fold
if train_mask[fold_idx]:
train_samples[start:end] = True
if test_mask[fold_idx]:
test_samples[start:end] = True
if train_samples.sum() > 0 and test_samples.sum() > 0:
for s in range(n_strategies):
train_ret = returns_matrix[train_samples, s]
test_ret = returns_matrix[test_samples, s]
tsr = sharpe_ratio(train_ret)
esr = sharpe_ratio(test_ret)
train_sharpes.append(tsr)
test_sharpes.append(esr)
train_ranks.append(np.sum(full_sharpes > full_sharpes[s]))
test_ranks.append(np.sum(test_sharpes[-1] > np.array(test_sharpes)))
train_sharpes = np.array(train_sharpes)
test_sharpes = np.array(test_sharpes)
train_ranks = np.array(train_ranks)
test_ranks = np.array(test_ranks)
n_comb = len(train_sharpes)
top_threshold = int(top_pct * n_comb)
bottom_threshold = int(bottom_pct * n_comb)
overfitted = (train_ranks < top_threshold) & (test_ranks >= bottom_threshold)
pbo = np.mean(overfitted)
train_test_corr = np.corrcoef(train_sharpes, test_sharpes)[0, 1]
return {
'pbo': pbo,
'n_combinations': n_comb,
'n_overfitted': np.sum(overfitted),
'train_test_correlation': train_test_corr
}
def sharpe_ratio(returns, periods_per_year=252):
if np.std(returns) == 0:
return 0.0
return np.mean(returns) / np.std(returns) * np.sqrt(periods_per_year)
第 6 步:如何解读结果
PBO < 5%:策略表现稳健。仅有 5% 看起来很棒的策略在样本外看起来很糟糕。按标准风险管理进行。
PBO 5-25%:中等关注。策略可能过拟合。上实盘前:
- 尝试更简单的版本(更少参数)
- 要求更多数据(如果可能)
- 留出验证集不用于任何优化
PBO > 25%:高过拟合概率。策略有 >1/4 机会是假阳性。拒绝或回到设计阶段。
训练-测试相关性也重要。 如果 train_sharpe 和 test_sharpes 高度相关(例如 > 0.7),策略是稳健的。如果相关性低或为负,策略是脆弱的。
第 7 步:常见陷阱 + 在知识体系中的位置
陷阱:使用的 k 太小。 k=4 给 15 种组合。对于可靠的 PBO 估计太少。使用 k≥6(63+ 组合)。
陷阱:不清仓。 不清仓的 CPCV 给出有偏 PBO(过于乐观低)。总是使用根据你信号校准的 purge 间隙。
陷阱:把 PBO 解释为”策略无价值的概率”。 PBO 是有条件的:假设你测试了许多组合,“看起来很棒的策略实际上是假阳性的概率”。它不告诉你策略有零技能的概率。
在知识体系中的位置:
- 上实盘前任何优化策略的最终验证
- 比较策略稳健性(更低 PBO = 更稳健)
- 向投资者传达过拟合风险
PBO 是验证管道的一部分:purged CV → CPCV → PBO → DSR。每个步骤增加严谨性。
下一步
准备好下一级了吗?→ 学习路线 05:概率化回测
想要百科参考卡片?→ 回测过拟合:PBO(百科词条)