回测过拟合:PBO 与组合清仓交叉验证
回测过拟合概率(PBO)衡量你'最佳'策略是统计幻觉的可能性。组合清仓交叉验证是计算 PBO 的工具。
一家对冲基金推出一个回测 3.2 Sharpe 比率的策略。三年后,实际只有 0.5。投资者起诉。基金经理说「市场结构变了」。真正的原因更简单:这个策略对回测过拟合了。它从来没有看起来那么好。
回测过拟合是量化策略开发的核心风险。你在历史数据上优化策略,选择表现最佳的变体,而最佳表现者——顾名思义——是你测试集中最幸运的那个。PBO(回测过拟合概率)量化了这种运气。
直觉
想象你有 100 个真正无价值的交易策略。你回测所有 100 个。按 α=0.05,5 个会显示「显著」的正收益。其中最好的可能显示 1.5 的 Sharpe。
这就是 PBO。给定你搜索的强度(你测试了多少变体),你最佳策略是假阳性的概率是多少?
正式答案需要知道两件事:你测试了多少策略,以及在真正的空假设下回测结果在这些策略之间有多大差异。
组合清仓交叉验证(CPCV)
CPCV 是 Bailey 和 López de Prado(2012)计算 PBO 的方法。核心思想:不是用 k 折,而是枚举你的数据所有可能的训练和测试分割方式。对于每个分割,计算策略的表现。如果策略是真正的空(没有技能),训练期(样本内)表现与测试期(样本外)表现之间的相关性应该接近零。高相关性暗示过拟合。
标准 purged K-fold 使用 k 个分割。CPCV 枚举所有可能的 m 训练/测试分割组合,其中 m 是为平衡计算成本与穷尽性而选择的。
CPCV 组合数:
对于 k=6:N_comb = 63。对于 k=8:N_comb = 255。计算量指数增长。
PBO 公式
对于每个组合分割,定义:
- :训练(样本内)期间的 Sharpe 比率
- :测试(样本外)期间的 Sharpe 比率
如果策略过拟合,高 将与低 相关联(当分割真正是样本外时)。如果策略有真正的技能,高 应该预测高 。
PBO 是:在所有组合分割中,策略的 排名在前 X%,但 排名在后 Y% 的比例。
常见阈值:训练 Sharpe 前 10%,测试 Sharpe 后 50%。
用大白话说:样本内看起来很棒的结果,有多大比例会在样本外排名低于中位数?
Python 实现
import numpy as np
import pandas as pd
from itertools import combinations
def compute_pbo(returns_matrix, n_train_test_splits=6, top_pct=0.10, bottom_pct=0.50):
"""
Compute Probability of Backtest Overfitting using CPCV.
Parameters
----------
returns_matrix : array (n_samples, n_strategies)
Backtest returns for each strategy
n_train_test_splits : int
Number of train/test partitions to enumerate (k in 2^k - 1)
top_pct : float
Top X% of train-Sharpe rankings considered "winning"
bottom_pct : float
Bottom Y% of test-Sharpe rankings considered "losing"
Returns
-------
pbo : float
Probability of backtest overfitting
out_of_sample_sharpes : array
Sharpe ratios for each combinatorial partition
"""
n_samples, n_strategies = returns_matrix.shape
n_partitions = 2 ** n_train_test_splits - 1
# Compute Sharpe for each strategy (full period)
full_sharpes = sharpe_ratio(returns_matrix)
# Enumerate all combinatorial splits
k = n_train_test_splits
splits = list(range(n_samples))
out_of_sample_sharpes = []
for r in range(1, k + 1):
for train_indices in combinations(range(k), r):
# Build train and test masks
train_mask = np.zeros(k, dtype=bool)
test_mask = np.zeros(k, dtype=bool)
train_indices = list(train_indices)
for i in range(k):
if i in train_indices:
train_mask[i] = True
else:
test_mask[i] = True
# Map fold indices to sample indices
samples_per_fold = n_samples // k
train_start_folds = [i * samples_per_fold for i in train_indices]
test_start_folds = [i * samples_per_fold for i in range(k) if i not in train_indices]
train_mask_samples = np.zeros(n_samples, dtype=bool)
test_mask_samples = np.zeros(n_samples, dtype=bool)
for i, fold_start in enumerate([idx * samples_per_fold for idx in train_indices]):
train_mask_samples[fold_start:fold_start + samples_per_fold] = True
for fold_start in test_start_folds:
test_mask_samples[fold_start:fold_start + samples_per_fold] = True
# Compute Sharpe for each strategy on train and test
if train_mask_samples.sum() > 0 and test_mask_samples.sum() > 0:
train_sharpes = sharpe_ratio(returns_matrix[train_mask_samples])
test_sharpes = sharpe_ratio(returns_matrix[test_mask_samples])
out_of_sample_sharpes.append(test_sharpes)
out_of_sample_sharpes = np.array(out_of_sample_sharpes)
# Rank strategies by full-period Sharpe
ranks = np.argsort(np.argsort(-full_sharpes)) # Descending rank
# PBO: top X% by train, bottom Y% by test
n_strats = len(full_sharpes)
top_threshold = int(top_pct * n_strats)
bottom_threshold = int(bottom_pct * n_strats)
n_overfitted = 0
for i, test_sharpe in enumerate(out_of_sample_sharpes):
train_rank = ranks[i]
test_rank = np.argsort(np.argsort(-test_sharpe))[i]
if train_rank < top_threshold and test_rank >= bottom_threshold:
n_overfitted += 1
pbo = n_overfitted / len(out_of_sample_sharpes)
return pbo, out_of_sample_sharpes
def sharpe_ratio(returns, risk_free=0.0, periods_per_year=252):
"""Annualized Sharpe ratio."""
excess = returns - risk_free
if np.std(excess) == 0:
return 0.0
return np.mean(excess) / np.std(excess) * np.sqrt(periods_per_year)
如何解读 PBO
| PBO | 解读 |
|---|---|
| < 5% | 策略可能是稳健的。最佳回测是运气的概率低。 |
| 5–25% | 谨慎行事。检查你的搜索是否穷尽。 |
| 25–50% | 过拟合风险高。考虑更简单的策略或更多数据。 |
| > 50% | 「最佳」策略是假阳性的可能性高于真实。这是掷硬币——勉强比随机好。 |
PBO 为 30% 意味着 30% 的情况下,一个按样本内 Sharpe 排名前 10% 的策略会按样本外 Sharpe 排名后 50%。这是掷硬币。
常见陷阱
使用的组合分割太少:k=4 只给出 15 个分割。这太少,无法可靠估计 PBO。如果数据允许,使用 k≥6(63+ 分割)或 k≥8(255 分割)。
不清仓:没有清仓的 CPCV 与标准 K-fold 一样存在前视偏差。清仓应根据策略的信号半衰期进行校准。
对单个策略报告 PBO:当你从许多策略变体中选择时,PBO 才有意义。如果你只测试一个策略与基准相比,PBO 不适用——使用标准统计检验。
忽略先验率:如果你的宇宙中 90% 的策略真正无价值,PBO 为 10% 仍然意味着大多数「最佳」策略是假阳性。PBO 需要在你任何策略有真实技能的先验概率背景下解读。
在知识体系中的位置
PBO 是策略选择过程是否过拟合的 definitive test。它需要组合清仓交叉验证作为输入。《多重检验》涵盖了你只筛选特征时的更简单(但功效较低)的校正。《概率化回测》提供了解释回测结果的贝叶斯框架。
我们的平台在接收任何策略之前会计算其 PBO。PBO > 25% 的策略会被标记为需要额外审查或拒绝。
参考文献
- Bailey, López de Prado, “The Share of Tradeable and Non-Tradeable Strategies,” Journal of Computational Finance, 2012.
- Bailey, Borwein, López de Prado, “Stock Return Data: A Non-Factorial Analysis,” Journal of Financial Data Science, 2017.
- López de Prado, Advances in Financial Machine Learning, 第 12 章. Wiley, 2018. ISBN: 978-1-119-48208-6.
- White, “A Reality Check for Data Snooping,” Econometrica, 2000.