你跑了 40 个策略变体,挑出夏普最高的那个准备上仓位。问题来了:这个「最优」,是真有 edge,还是 40 次抽奖里运气最好的那张彩票?传统回测无法回答,因为它只给你一个夏普数字,不告诉你这个数字里有多少是运气。Bailey 和 López de Prado 在 2014 年提出的回测过拟合概率(Probability of Backtest Overfitting,PBO),用一套叫组合对称交叉验证(Combinatorially Symmetric Cross-Validation,CSCV)的方法,把「你选出的最优在样本外其实是平庸货」这件事,算成一个 0 到 1 之间的概率。这篇讲清 PBO 到底在算什么、怎么算、以及为什么它应该和夏普一起出现在每一份策略报告里。
过拟合的本质:选择偏差#
先厘清一个常被混淆的概念。这里说的「过拟合」,不是单个模型参数太多那种过拟合,而是选择偏差(selection bias)造成的过拟合。机制是这样的:
假设你有 个策略,它们真实的 alpha 全是零——纯噪声。你在样本内(in-sample,IS)比较它们的夏普,总会有一个碰巧最高。你把它选出来。但它样本内高,纯粹因为它在这段历史里噪声运气最好,到了样本外(out-of-sample,OOS),它的运气会均值回归,掉回平庸。「在一堆随机变量里取最大值」这个动作本身,就是过拟合的来源——搜的变体越多,选出来的「最优」越可能只是极值噪声。
一个简单的数字能吓到你:假设 个策略真实夏普都是 0,你测 次,「至少有一个样本内夏普看起来显著」的概率高得离谱。这就是为什么「一晚跑几百个变体挑最好的」是量化里最隐蔽、也最致命的坑——你不是在发现 alpha,你是在挖掘噪声。
CSCV:用组合把「一次样本外」变成「几千次样本外」#
要量化选择偏差,你需要反复问「样本内选出的最优,在样本外表现如何」。传统做法切一段样本外,只能问一次。CSCV 的核心思想是:把时间轴切成 块,然后穷举「一半块做训练、一半块做测试」的所有对称组合,每种组合都问一次。
块里取 块做训练集,剩下 块做测试集,一共有 种分法。 时是 种——一次回测数据,硬是榨出 3432 条独立的「样本内 vs 样本外」对照。下图用 ( 种,方便看)画出这个组合结构:

每一行是一种划分,蓝色「训」是训练块、红色「测」是测试块。对称性体现在:训练和测试各占一半,且每一种「取一半」的组合都被枚举到。这保证了不偏袒任何特定的时间切分方式。
PBO 的算法:logit 排名分布#
有了这几千种划分,PBO 的计算流程干净利落。对每一种划分 :
- 在训练集上算出所有 个策略的夏普,找出样本内最优的那个,记它的序号为 。
- 到测试集上,看这个 号策略的样本外夏普,在全部 个策略的样本外夏普里排第几。用相对排名 表示(1 = 样本外也最好,0 = 样本外最差)。
- 把相对排名转成 logit:。样本外排在中位数以上,;掉到中位数以下,。
PBO 就是样本内最优策略、在样本外掉到中位数以下的组合占比:
直觉很清楚:如果你选出的「最优」是真本事,它样本外也该排在前面, 大多为正,PBO 低;如果它只是噪声运气,样本外会随机散开、常常掉到下半区,PBO 高(接近甚至超过 0.5)。
import numpy as np
from itertools import combinations
def sharpe(x):
return x.mean(0) / (x.std(0) + 1e-12) * np.sqrt(252)
def cscv_pbo(R, S=14):
"""
R: 收益矩阵,形状 (T 期, N 策略)
S: 时间分块数(偶数),组合数 = C(S, S/2)
返回:logit 数组, PBO
"""
idx = np.array_split(np.arange(R.shape[0]), S)
logits = []
for combo in combinations(range(S), S // 2):
tr = np.concatenate([idx[b] for b in combo])
te = np.concatenate([idx[b] for b in range(S) if b not in combo])
sr_is = sharpe(R[tr])
sr_oos = sharpe(R[te])
n_star = np.argmax(sr_is) # 样本内最优
rank = (sr_oos < sr_oos[n_star]).sum() / (len(sr_oos) - 1)
rank = min(max(rank, 1e-6), 1 - 1e-6) # 防 logit 溢出
logits.append(np.log(rank / (1 - rank)))
logits = np.array(logits)
return logits, (logits < 0).mean()python两个对照实验:噪声池 vs 真 alpha 池#
光有公式不够,得看它在「已知答案」的场景下是否给出正确判断。我构造了两个策略池,各 40 个策略、1000 期日收益:
- 纯噪声池:所有 40 个策略真实 alpha 都是 0,只有随机波动。
- 含真 alpha 池:其中 8 个策略注入了稳定的正 alpha,其余 32 个是噪声。
跑 CSCV(,3432 条路径),结果对比鲜明:

左边纯噪声池,logit 分布大幅骑跨在红线()两侧,左半区占了 PBO ≈ 0.39——近四成的组合里,你样本内挑的「最优」样本外掉进了下半区。这正确地告诉你:这个池子里的最优基本靠不住。右边含真 alpha 池,logit 分布整体被推到红线右侧,PBO ≈ 0.06——样本内最优几乎总是样本外也最优,说明这里的「最优」是真 edge。PBO 干净地区分了两种情形。
再看样本内夏普对样本外夏普的散点,退化过程一目了然:

红色(纯噪声池)的点:样本内夏普再高,样本外都被打回零附近,完全偏离对角线(IS=OOS 理想线)——这就是过拟合的视觉签名。绿色(含真 alpha 池)的点:样本内高的,样本外也守在对角线附近,说明业绩能延续。一句话:过拟合的策略,样本内夏普对样本外夏普没有预测力,而 PBO 把这个「没有预测力」的程度量化成了一个概率。
PBO 相比「切一段样本外」强在哪#
传统验证切最后 20% 当样本外、跑一次。PBO 有三个实质提升。
一、它给分布,不给点。 单一样本外只有一个数字,你不知道它是运气还是本事。PBO 背后是几千条样本外路径,能看到整个 logit 分布的形状、均值、偏斜。
二、它直接量化选择偏差。 这是 PBO 最独特的地方。单一样本外无法回答「我搜了 40 个变体,选出的最优有多大概率是蒙的」——而 PBO 就是为这个问题设计的。做参数扫描、因子挖掘时,PBO 应该和夏普并列上报。
三、它对称、无偏。 传统样本外偏爱「最后那段时间」,如果那段恰好是特定市场环境,结论就有偏。CSCV 穷举所有对称划分,不押注任何单一时间切法。
一条实践准则:夏普 2.0 但 PBO 0.5 的策略,远不如夏普 1.2 但 PBO 0.1 的策略可信。前者告诉你「这个漂亮夏普有一半概率是自欺欺人」,后者告诉你「这个夏普虽然没那么炸裂,但很可能是真的」。
小结#
PBO 的一句话本质:用组合对称交叉验证榨出几千条样本外路径,量化『你从一堆策略里选出的最优,在样本外其实是平庸货』的概率。它不能让坏策略变好,但能在你上真金白银之前,诚实地告诉你「这个最优里有多少是选择偏差挖出来的噪声」。在参数越调越多、变体越跑越密的今天,PBO 这种把过拟合概率算成数字的工具,比又一个高夏普回测有价值得多——因为它戳破的,正是量化研究里最贵的那个幻觉。
诚实边界:CSCV 的组合数 随 爆炸增长( 就是 18 万种),对需要重训练的模型(深度学习)计算上不现实,需在分块数和算力间权衡。其次,PBO 假设你喂进去的策略集合能代表真实的搜索空间——那些没记录下来的隐性调参(试了但没保存的变体)若未计入,PBO 仍会系统性低估真实过拟合程度。第三,CSCV 打乱时间块的组合方式,隐含假设各分块间机制大致平稳;遇到强 regime 切换时,组合出来的训练/测试拼接可能包含现实中不会发生的时间顺序,使夏普估计失真。最后,PBO 用夏普作为绩效指标默认收益近似 i.i.d.,对强自相关或路径依赖的策略,块内夏普估计本身就有偏,会污染整个 PBO。(中阶)