跳到正文

回测过拟合概率(PBO):完整推导

PBO 打分的对象是你的挑选动作,不是你的策略。从 CSCV 出发把 logit 排名和 PBO 定义推一遍,顺带说清楚这个数字自己有多噪。

第 3 篇 · 共 12 篇 约 12 分钟

你从 NN 个候选策略中挑出了样本内夏普最高的那个。它在样本外掉到中位数以下的概率是多少?这个概率就是 PBO。

在开始推导之前,先把一句话钉住,因为整篇文章都挂在它上面:PBO 打分的对象是你的挑选动作,不是你的策略。它不回答”这个策略好不好”,它回答”我刚才那个’挑样本内最好的’的操作,有没有比抛硬币强”。这个区分听起来像语义学,但它决定了 PBO 的参考点为什么是 0.5 而不是 0,也决定了为什么一个 PBO = 0.1 的策略仍然可能完全不能用。

本文从 CSCV 推导 PBO:走一遍 logit 变换,写清楚定义,然后拿一份能跑的实现去量一量这个数字自己有多不稳。

PBO 衡量什么

PBO 回答的问题是:“如果我反复运行这个程序——搜索 NN 个策略、选出样本内最好的那一个——被选中的策略有多大比例会掉到这 NN 个策略的样本外中位数以下?”

生产环境的对应物是样本外时期,也就是搜索过程中策略没见过的数据。CSCV 通过反复保留一半数据来模拟这件事,PBO 在所有这些模拟上取平均。

三个锚点:PBO = 0 意味着样本内赢家在样本外始终是最好的;PBO = 0.5 意味着它落在中位数上下的次数一样多,你的挑选动作没提供任何信息;PBO = 0.9 意味着样本内赢家几乎总是掉到中位数以下,你的程序在稳定地挑错。

注意最后一种情况的性质:这不是”策略不好”,这是”你的挑选方式反着来”。同一批策略换个挑法,结果可能完全不同。

CSCV 给出的东西

上一篇文章介绍的 CSCV 算法,输出是一个相对样本外排名的向量:

ωS∈[0,1]for each path S\omega_S \in [0, 1] \quad \text{for each path } S

其中 SS 是 kk 个块上 (kk/2)\binom{k}{k/2} 条对称等质量路径之一。

这里必须把方向约定说死,否则后面所有符号都会反过来。本文用的约定是:把 NN 个策略按样本外表现从好到坏排序,样本内赢家排在第 rr 位(r=1r = 1 表示样本外也最好),则

ωS=rN+1\omega_S = \frac{r}{N+1}

分母用 N+1N+1 而不是 NN,是为了让 ωS\omega_S 严格落在开区间 (0,1)(0,1) 内,后面取 logit 时不会跑到无穷。在这个约定下 ωS\omega_S 越小越好,而 ωS>0.5\omega_S > 0.5 就表示样本内赢家掉到了样本外中位数以下。

(Bailey、Borwein、López de Prado 和 Zhu 的原文用的是升序排名,也就是 ω\omega 越大越好,于是他们的 PBO 写成 P(λc≤0)P(\lambda_c \le 0)。两种写法完全等价,只是符号相反。读文献时先确认作者用的是哪一种,不然会把结论读反。)

ωS\omega_S 的经验 CDF 是:

F^ω(x)=1∣S∣∑S∈S1[ωS≤x]\hat{F}_\omega(x) = \frac{1}{|\mathcal{S}|} \sum_{S \in \mathcal{S}} \mathbb{1}[\omega_S \le x]

这就是全部的原料。PBO 是从这个 CDF 上读出来的一个数。

Logit 变换

排名 ωS\omega_S 被限制在 [0,1][0,1] 内,这对做正态近似不方便。标准做法是取 logit:

ωˉS=logit(ωS)=log⁡(ωS1−ωS)\bar{\omega}_S = \text{logit}(\omega_S) = \log\left(\frac{\omega_S}{1 - \omega_S}\right)

变换之后 ωˉS\bar{\omega}_S 落在整条实数轴上。在”没有过拟合也没有优势”的原假设下,ωˉS\bar{\omega}_S 的经验分布近似正态、以 0 为中心。

要说清楚的是,logit 不增加也不减少信息——它是单调变换,带 logit 和不带 logit 的 CDF 携带同样的内容。它带来的是两个便利:分布更接近正态,于是可以用一个闭式近似代替离散计数;以及”中位数”这个关键位置从 ω=0.5\omega = 0.5 挪到了 ωˉ=0\bar{\omega} = 0,对称性看得见了。

PBO 的定义

有了方向约定,定义只需要一行。PBO 是样本内赢家掉到样本外中位数以下的路径占比:

PBO=1∣S∣∑S∈S1[ωS>0.5]=1−F^ω(0.5)PBO = \frac{1}{|\mathcal{S}|} \sum_{S \in \mathcal{S}} \mathbb{1}[\omega_S > 0.5] = 1 - \hat{F}_\omega(0.5)

用 logit 写是同一件事:

PBO=1∣S∣∑S∈S1[ωˉS>0]=1−F^ωˉ(0)PBO = \frac{1}{|\mathcal{S}|} \sum_{S \in \mathcal{S}} \mathbb{1}[\bar{\omega}_S > 0] = 1 - \hat{F}_{\bar{\omega}}(0)

两者等价,因为 ωˉS>0  ⟺  ωS>0.5\bar{\omega}_S > 0 \iff \omega_S > 0.5。

注意公式里是 1−F^1 - \hat{F} 而不是 F^\hat{F}。F^ω(0.5)\hat{F}_\omega(0.5) 按定义是 ωS≤0.5\omega_S \le 0.5 的占比,也就是样本内赢家表现好的那一半;PBO 要的是另一半。这个符号很容易写反,写反了会让一个好策略看起来像灾难。

阈值 0.5 不需要从数据里估,它就是样本外中位数的定义位置。有些实现用 ωS≥0.5\omega_S \ge 0.5 而不是严格大于;在 ωS=r/(N+1)\omega_S = r/(N+1) 这个取值网格上,只有 NN 为奇数时才可能取到恰好 0.5,差别可以忽略。

为什么 0.5 是参考点

在既没有过拟合也没有优势的世界里,样本内夏普最高的那个策略相当于是从 NN 个里随机抽的。它的样本外名次 rr 在 {1,2,…,N}\{1, 2, \ldots, N\} 上均匀分布,于是 ωS=r/(N+1)\omega_S = r/(N+1) 在 {1/(N+1),…,N/(N+1)}\{1/(N+1), \ldots, N/(N+1)\} 上均匀,这个网格关于 0.5 对称。落在 0.5 以上和以下的概率各一半,所以

PBO原假设=0.5PBO_{\text{原假设}} = 0.5

这解释了为什么”PBO = 0.4 算不算好”这种问题需要先想清楚参考点。0.4 不是”40% 概率过拟合”,是”比随机挑略好一点”。

顺着这条线,最常见的误读也就清楚了:PBO 不是”策略过拟合的概率”。它是样本内赢家掉到样本外中位数以下的概率,而这个概率在无信息的情况下天然是 0.5。大于 0.5 是坏消息,小于 0.5 是好消息,等于 0.5 是没消息。

怎么读一个 PBO 数字

PBO解读
< 0.1挑选程序信息量强,样本内赢家在样本外稳定靠前
0.1 – 0.3中等信息量,是研究阶段升级的常见门槛
0.3 – 0.5微弱信息量,略好于随机挑
0.5 – 0.7微弱过拟合,挑选程序略差于随机
0.7 – 0.9强过拟合,样本内赢家通常是样本外较差的那批
> 0.9极端过拟合,挑选程序在稳定地反向选择

多数研究团队把 PBO < 0.3 作为从研究升级到模拟交易的门槛,PBO > 0.5 则视为搜索程序本身已经失效、结果不可用。这个门槛是团队参数而不是自然常数:假阳性代价高的场景(部署成本大的高频策略)会把它压到 0.1,容忍度高的探索性研究可能放到 0.4。决定门槛的应该是”选错一次要付多少钱”,不是习惯。

Logit 正态近似

离散计数出来的 PBO 只能取 1/∣S∣1/|\mathcal{S}| 的整数倍,路径少时会很粗糙。用 logit 排名的样本均值和样本标准差

μ^ωˉ=1∣S∣∑SωˉS\hat{\mu}_{\bar{\omega}} = \frac{1}{|\mathcal{S}|} \sum_S \bar{\omega}_S σ^ωˉ2=1∣S∣−1∑S(ωˉS−μ^ωˉ)2\hat{\sigma}_{\bar{\omega}}^2 = \frac{1}{|\mathcal{S}| - 1} \sum_S (\bar{\omega}_S - \hat{\mu}_{\bar{\omega}})^2

可以得到一个平滑估计:

PBO≈Φ(μ^ωˉσ^ωˉ)PBO \approx \Phi\left(\frac{\hat{\mu}_{\bar{\omega}}}{\hat{\sigma}_{\bar{\omega}}}\right)

其中 Φ\Phi 是标准正态 CDF。原假设下 μ^ωˉ=0\hat{\mu}_{\bar{\omega}} = 0,代进去得到 Φ(0)=0.5\Phi(0) = 0.5,和离散版本对上。

σ^ωˉ\hat{\sigma}_{\bar{\omega}} 的量级由 NN 决定,因为策略越多、排名网格越宽。均匀排名下算一下:N=5N = 5 时 logit 排名的标准差约 1.11,N=10N = 10 时约 1.33,N=20N = 20 时约 1.50,N=50N = 50 时约 1.64。增长很慢——这是 logit 的对数尺度决定的,也意味着 μ^ωˉ\hat{\mu}_{\bar{\omega}} 稍微偏离 0 一点,PBO 就会明显偏离 0.5。

路径数 ∣S∣|\mathcal{S}| 够大(一般 ≥100\ge 100)时正态近似不错;路径少时它比经验 CDF 更平滑,但也更依赖”分布确实近似正态”这个前提,遇到双峰分布会给出误导性的结果。这时候应该直接看分布,不要看任何单个数字。

一个算到底的例子

取 N=10N = 10 个策略,k=16k = 16 个块,于是路径数是 ∣S∣=(168)=12870|\mathcal{S}| = \binom{16}{8} = 12870。

跑完 CSCV,你手上有 12870 个相对样本外排名 ωS\omega_S。假设经验分布长这样:

  • 5000 条路径落在 ωS∈[0,0.25)\omega_S \in [0, 0.25):样本内赢家在样本外也是第一四分位。
  • 2000 条路径落在 [0.25,0.5)[0.25, 0.5):第二四分位。
  • 1500 条路径落在 [0.5,0.75)[0.5, 0.75):第三四分位。
  • 4370 条路径落在 [0.75,1][0.75, 1]:第四四分位。

PBO 就是后两档的占比:

PBO=1500+437012870=587012870=0.456PBO = \frac{1500 + 4370}{12870} = \frac{5870}{12870} = 0.456

0.456 略低于 0.5,提示挑选程序有微弱信息量。但请注意这个分布的形状——两头都很重(5000 条在最好的四分位,4370 条在最差的四分位),中间很空。这是双峰,不是”略好于随机”的平滑分布。单看 0.456 这个数会得出”温和向好”的印象,看分布则会得出”这个挑选动作方差极大”的印象,后者才是真的。这也是为什么下面要专门讲图。

换一份过拟合严重的分布:

  • [0,0.25)[0, 0.25):1000 条
  • [0.25,0.5)[0.25, 0.5):800 条
  • [0.5,0.75)[0.5, 0.75):2000 条
  • [0.75,1][0.75, 1]:9070 条
PBO=2000+907012870=1107012870=0.860PBO = \frac{2000 + 9070}{12870} = \frac{11070}{12870} = 0.860

0.86,强过拟合:样本内赢家有 86% 的时间掉到样本外中位数以下,而且质量高度集中在最差的四分位。

一份可跑的实现

import numpy as np
from itertools import combinations
from scipy.special import logit as scipy_logit


def cscv_pbo(returns_matrix, k=16):
    """
    Compute PBO via CSCV.

    Convention: omega is the IS winner's OOS rank counted from the TOP,
    scaled to (0, 1). Small omega = good OOS performance.
    PBO is therefore the fraction of paths with omega > 0.5.

    Parameters
    ----------
    returns_matrix : ndarray of shape (T, N)
        Returns for N strategies.
    k : int
        Number of blocks (must be even).

    Returns
    -------
    pbo : float
        Probability of Backtest Overfitting.
    omega : ndarray
        Relative OOS ranks, one per path.
    omega_bar : ndarray
        Logit of the relative OOS ranks.
    """
    T, N = returns_matrix.shape
    block_size = T // k
    blocks = [returns_matrix[i * block_size:(i + 1) * block_size] for i in range(k)]

    paths = list(combinations(range(k), k // 2))
    omega = np.zeros(len(paths))

    for s_idx, S in enumerate(paths):
        is_data = np.concatenate([blocks[j] for j in S], axis=0)
        oos_blocks = [j for j in range(k) if j not in S]
        oos_data = np.concatenate([blocks[j] for j in oos_blocks], axis=0)

        is_sharpe = is_data.mean(axis=0) / is_data.std(axis=0, ddof=1)
        oos_sharpe = oos_data.mean(axis=0) / oos_data.std(axis=0, ddof=1)

        is_winner = np.argmax(is_sharpe)
        oos_ranking = np.argsort(-oos_sharpe)          # descending: position 0 is best
        rank = np.where(oos_ranking == is_winner)[0][0]
        omega[s_idx] = (rank + 1) / (N + 1)            # (0, 1), so logit stays finite

    omega_bar = scipy_logit(omega)
    pbo = (omega > 0.5).mean()

    return pbo, omega, omega_bar


# Demo: 10 strategies, one of them with a real edge
rng = np.random.default_rng(42)
T, N = 2000, 10
daily_vol = 0.01
true_mean = np.zeros(N)
true_mean[0] = 0.0008        # daily mean -> annualized Sharpe of about 1.27

returns = true_mean + rng.normal(0, daily_vol, size=(T, N))

pbo, omega, omega_bar = cscv_pbo(returns, k=8)
print(f"PBO: {pbo:.4f}")
print(f"Median omega: {np.median(omega):.4f}")

这份 demo 用 k=8k = 8,路径数只有 (84)=70\binom{8}{4} = 70,跑得快。输出是 PBO = 0.0857、中位数 ω\omega = 0.0909——那个真有优势的策略被稳定地挑了出来,符合预期。把 true_mean[0] 设回 0(谁都没有优势),PBO 会跳到 0.5 附近。

注意 true_mean[0] = 0.0008 是日均收益而不是夏普:配上 0.01 的日波动率,它对应的年化夏普是 0.0008/0.01×252≈1.270.0008 / 0.01 \times \sqrt{252} \approx 1.27。这两个量在代码里长得很像,混起来会让你以为自己在测一个微弱信号,实际上在测一个很强的信号。

这个数字自己有多噪

上面那句”把 true_mean[0] 设回 0,PBO 会跳到 0.5 附近”值得展开,因为它藏着 PBO 最容易被忽略的性质。

拿 N=10N = 10 个纯噪声策略、T=2000T = 2000、k=8k = 8(70 条路径),换 40 个随机种子各跑一次,得到的 PBO 分布是:均值 0.507,标准差 0.249,最小 0.029,最大 0.929。

均值 0.507 完美落在理论原假设 0.5 上,这说明估计量本身是对的。但标准差 0.249 意味着:一批完全没有任何优势的策略,有相当大的机会给出 PBO = 0.03 这种看起来”证据确凿”的数字,也有相当大的机会给出 0.93 这种看起来”灾难性过拟合”的数字。在这个配置下,单个 PBO 数字几乎不携带可靠信息。

把块数从 k=8k = 8 提到 k=16k = 16(路径数从 70 涨到 12870),标准差降到约 0.17。有帮助,但没有解决——路径之间高度重叠,它们不是 12870 份独立证据。

这带来两条实操结论。第一,块数别省,k=16k = 16 应该是起点而不是上限,k=8k = 8 只适合写 demo。第二,也是更重要的一条:不要拿 PBO 的小数点后两位做决策。PBO = 0.28 和 PBO = 0.34 在一条阈值线两侧,但它们的差距远小于这个估计量自身的抖动。把 PBO 当成量级读——“接近 0”、“接近 0.5”、“明显大于 0.5”——才是它能支持的精度。

顺带说,这也是”多个 kk 各报一个 PBO”这条建议的真正理由。它不只是稳健性检查,它是在给读者看这个数字的抖动幅度。

看图,不只看数

单个 PBO 数字是摘要,分布才是诊断。常见的画法是把 ωˉS\bar{\omega}_S 的经验 CDF 和标准正态 CDF 叠在一起,CSCV 用实线、正态用虚线;或者直接画 F^ω(x)\hat{F}_\omega(x) 对 x∈[0,1]x \in [0,1],拿对角线当参照。两种画法读法一样:

均匀分布对应 F^ω(x)=x\hat{F}_\omega(x) = x,曲线贴着对角线,没有过拟合也没有优势。曲线在小 xx 处沉到对角线下方,说明质量往大 ω\omega 跑,过拟合,PBO > 0.5。曲线在小 xx 处抬到对角线上方,说明质量往小 ω\omega 跑,有信息量,PBO < 0.5。

真正需要图才能看出来的是第四种:双峰,曲线在 0 和 1 附近各有一次跳跃,中间很平。这意味着样本内赢家有时是样本外最好的、有时是样本外最差的,方差巨大。前面那个 PBO = 0.456 的例子正是这一类。两个策略可以有完全相同的 PBO 而分布形状完全不同,一个是平滑偏移、一个是双峰,而这两种情况该做的事完全不一样:前者是”优势微弱”,后者是”这个挑选程序不稳定,先别信它”。

研究报告里 PBO 数字和这张图应该一起给。数字方便比较,图防止误读。

PBO 与 DSR

DSR 和 PBO 都在处理多重检验问题,路子不同。

DSR 走闭式的 Gumbel 近似,假设收益率高斯、试验独立,输出的是”报告的夏普能否和原假设下的期望最大值区分开”这个概率。PBO 走 CSCV,一种与模型无关的重采样,不需要分布假设,输出的是”样本内赢家会不会掉到样本外中位数以下”。

取舍是清楚的:DSR 快、解析、可复现,在假设成立时更有区分度;PBO 慢、计算式、结果带抖动,但在假设不成立时更稳健。对高斯收益率加大量试验,两者结论接近。对典型交易策略的厚尾非高斯收益率,PBO 更可靠。NN 很大(几百个策略)时 PBO 也更实用,因为它不需要处理高维的 Gumbel 近似。

实践上两个都算。结论一致就往下走;不一致时,按更保守的那个(过拟合概率更高的)来报告,同时去查是哪条假设不成立——这通常比争论哪个方法对更有收获。

PBO 不告诉你的事

PBO 是必要诊断,不是充分条件。它的射程之外至少有三件事。

样本内赢家在绝对意义上好不好。 PBO = 0.3 说的是样本内赢家更常落在样本外靠前的位置,是一个相对陈述。如果这 NN 个策略全都是亏钱的,那么”稳定地挑出最不亏的那个”照样给你一个漂亮的 PBO,而样本外夏普依然是负的。PBO 从头到尾只看排名,不看水平。

优势有没有大到值得做。 PBO = 0.1 配样本外夏普 0.3,和 PBO = 0.1 配样本外夏普 1.5,是两回事。前者说明你的挑选程序很可靠地挑出了一个不值得部署的策略。幅度是另一套诊断的事。

策略在生产里成不成立。 PBO 默认样本外时期能代表生产环境,而容量约束、市场状态切换、执行滑点这些东西都不在它的模型里。一个 PBO = 0.1 的策略照样可能因为容量撑不住而在实盘失败。

还有几条方法层面的边界值得记着。PBO 依赖块的划分,不同 kk 会给出不同的数(这一点上一节量化过了)。它假设 NN 个策略彼此可比,如果你的候选里混了跨资产类别、跨信号类型的东西,“排名”这个操作本身的意义就变弱了。它还假设按块重采样能保留时间依赖,遇到长记忆过程这个假设会破。

它在流程里的位置

一个用得上 PBO 的研究流程大致是这样:通过参数搜索、信号组合或机器学习生成 NN 个候选;选出样本内表现最好的那个;跑 CSCV 算 PBO;PBO 低于门槛就进入样本外测试,高于门槛就放弃或回去改搜索。PBO 卡在边缘时,正确的动作不是调门槛,而是改进搜索本身——减少候选数量、加更强的先验、上正则化。

有一点需要反复提醒:PBO 是数据的函数,数据变了它就变。去年 PBO = 0.2 的策略,今年可能变成 0.5,因为新到的一年数据没有确认那个优势。把 PBO 算一次就当定论是最常见的误用;诚实的做法是随着数据积累重算,并且把变化记下来。

它也只是诊断体系里的一格:

诊断回答的问题
PSR报告夏普能否和基准区分开?
Haircut Sharpe夏普是否被自相关抬高?
DSR夏普是否被多重检验抬高?
MBL回测是否足够长?
PBO策略选择是否过拟合?
CPCV交叉验证是否诚实?
容量优势在更大规模下是否保持?
状态优势在不同市场状态下是否保持?

这些问题彼此不能替代。PBO 过关只说明你的挑选动作有信息量,剩下每一格都得自己过。

顺便一提,PBO 和 NN、TT、真实夏普之间有可以预期的方向性关系:候选越多 PBO 越大(虚假赢家的机会更多),历史越长 PBO 越小(样本内外的估计都更准),真实夏普越高 PBO 越小(赢家更容易脱颖而出)。但和 DSR 那边的最小回测长度(MBL)不一样,PBO 这边没有干净的闭式反解,因为它还取决于策略之间的依赖结构。想知道”要多长历史才能把 PBO 压到某个水平”,最靠谱的办法是照你自己的策略池做模拟,而不是套公式。

参考文献

  • Bailey, D. H., Borwein, J., López de Prado, M., and Zhu, Q. J. (2017). “The Probability of Backtest Overfitting.” Journal of Computational Finance, 20(4), 39–70。PBO 的原始论文。
  • López de Prado, M. (2018). Advances in Financial Machine Learning. Wiley. ISBN 978-1-119-48208-6。第 11 章讲回测过拟合与相关的 Python 实现。
  • Harvey, C. R., Liu, Y., and Zhu, H. (2016). “…and the Cross-Section of Expected Returns.” Review of Financial Studies, 29(1), 5–68。因子研究里多重检验偏差的实证证据。