跳到正文

White Reality Check:推导与中心化问题

White 的 Reality Check 是金融里第一个正经的数据窥探检验。整篇推导的重心其实只有一步:在 bootstrap 之前把每个策略的表现差异减掉自己的均值。这一步决定了检验是有意义的还是永远不会拒绝。

第 12 篇 · 共 12 篇 约 12 分钟

你搜了几百个策略,留下了最好的那个。它的表现看上去很不错——但你要回答的不是”它好不好”,而是”在一堆纯噪声策略里挑最好的那个,本来也能好到什么程度”。

Halbert White 在 2000 年给出的答案是:别去猜这个分布,把它 bootstrap 出来。Reality Check 的推导本身不长,bootstrap 是引擎,真正决定检验成不成立的是中间那一步中心化。这一步做错,检验永远不会拒绝原假设,而且它错得毫无声响——你还是会拿到一个漂亮的 p 值。下面从设定开始,一路推到那一步,再看后来的方法是从哪儿接着往下改的。

问题与统计量

你有 mm 个候选策略,每个都有一段回测收益率序列,以及一个共同的基准。定义 fi,tf_{i,t} 为策略 ii 在时点 tt 的表现指标,f0,tf_{0,t} 为基准在同一时点的表现,两者之差是

di,t=fi,t−f0,td_{i,t} = f_{i,t} - f_{0,t}

原假设说的是”这批策略里没有一个真的比基准强”:

H0:E[di]≤0for all i=1,…,mH_0: E[d_i] \le 0 \quad \text{for all } i = 1, \ldots, m

注意这是个复合原假设,mm 个不等式同时成立才算原假设为真。这也解释了为什么它是个多重检验问题。但和 Bonferroni、Holm 那类修正不同,Reality Check 不去套一个参数分布做近似,而是直接从数据里把原假设分布生成出来。

检验统计量取所有策略中最大的 studentized 差异:

Tobs=max⁡iTdˉiσ^iT_{obs} = \max_i \frac{\sqrt{T} \bar{d}_i}{\hat{\sigma}_i}

其中 dˉi=1T∑tdi,t\bar{d}_i = \frac{1}{T} \sum_t d_{i,t},σ^i\hat{\sigma}_i 是 di,td_{i,t} 的样本标准差。取最大值而不是逐个看 tit_i,是因为备择假设本身就是”至少有一个策略真的更好”,而你实际做的动作也正是把最大的那个挑出来——检验统计量应该和你的选择过程对齐。备择稀疏(只有少数策略真有 edge)时,最大值统计量的功效不错;备择稠密(很多策略都不是原假设)时,别的统计量可能更灵敏。它是个合理的默认值,不是什么最优检验,别把它当成一致最优功效检验来用。

一处出入值得先说清楚:White 原文用的统计量是未 studentize 的 max⁡iTdˉi\max_i \sqrt{T} \bar{d}_i。这里用的是 studentized 形式,好处是不同波动率的策略可比,这也是 Hansen 后来在 SPA 里正式采用的写法。bootstrap 的逻辑不受影响,两种写法都能往下推。

把原假设 bootstrap 出来

关键要求是:bootstrap 重采样必须来自一个真的满足原假设的分布,也就是对每个策略都有 E[di]=0E[d_i] = 0 的分布。观测到的 di,td_{i,t} 显然不满足这一点——你挑出来的策略均值是正的。所以先减掉自己的均值:

di,tc=di,t−dˉid^c_{i,t} = d_{i,t} - \bar{d}_i

这样每个策略的 dˉic=0\bar{d}_i^c = 0,被强行摁在原假设的边界上。之后所有的重采样都从 di,tcd^c_{i,t} 上做。

每一次 bootstrap 迭代 b=1,…,Bb = 1, \ldots, B 做五件事:

  1. 从 {di,tc}t=1T\{d^c_{i,t}\}_{t=1}^{T} 中有放回地重采样 TT 个观测。所有策略使用同一组索引,以保留横截面依赖。
  2. 计算 bootstrap 差异均值:\bar{d}_i^{(b)} = \frac{1}{T} \sum_t d^c_{i,t}^{(b)}。
  3. 计算 bootstrap 标准差:σ^i(b)\hat{\sigma}_i^{(b)}。
  4. 计算 bootstrap studentized 统计量:ti(b)=Tdˉi(b)/σ^i(b)t_i^{(b)} = \sqrt{T} \bar{d}_i^{(b)} / \hat{\sigma}_i^{(b)}。
  5. 记录最大值:T(b)=max⁡iti(b)T^{(b)} = \max_i t_i^{(b)}。

跑完 BB 次,{T(b)}b=1B\{T^{(b)}\}_{b=1}^{B} 就是最大值统计量在原假设下的经验分布,p 值是观测值落在它右尾多深:

p=1B∑b=1B1[T(b)≥Tobs]p = \frac{1}{B} \sum_{b=1}^{B} \mathbb{1}\left[T^{(b)} \ge T_{obs}\right]

p<αp < \alpha 就拒绝。拒绝的含义要说准:把搜索过程本身的多重检验结构算进去之后,最优策略的表现仍然和基准可区分。

中心化为什么是全部

White 的贡献不是 bootstrap,Efron 几十年前就把 bootstrap 提出来了。White 的贡献是想清楚了在 bootstrap 什么。

去掉中心化那一行看看会发生什么。重采样直接从观测到的 di,td_{i,t} 上做,而观测数据里带着你挑出来的那个正均值。于是 bootstrap 最大值分布不是围绕零,而是围绕 TobsT_{obs}——观测统计量落在自己分布的正中间,p 值大约 0.5,检验永远不会拒绝。它不报错、不发警告,就是安静地失效。

中心化把每个策略移到 E[di]=0E[d_i] = 0 上,重采样生成的是”如果原假设成立、而依赖结构又和你手上的数据一样,最大统计量通常能有多大”。观测值只有落到这个分布的尾部才算证据。所有 bootstrap 假设检验都靠这个技巧吃饭,White 只是第一个把它用在数据窥探问题上的人。

代价藏在一个细节里。原假设是 E[di]≤0E[d_i] \le 0,不是 E[di]=0E[d_i] = 0;把每个策略都摁到零上,等于假设所有策略都恰好站在原假设最不利的那个边界上。真实情况是一堆策略的 E[di]E[d_i] 明显为负,它们本来不该对 bootstrap 最大值有什么贡献,中心化之后却个个都有机会贡献一个大的 ti(b)t_i^{(b)}。结果是原假设分布被抬高,检验偏保守。这个保守性不是小毛病:往候选池里多塞一批明显没用的策略,Reality Check 的功效会实实在在地掉下去,而按理说加一堆垃圾策略不该让一个真有 edge 的策略更难被发现。Hansen(2005)的 SPA 检验改的就是这里——它对明显为负的策略做不同的重新中心化,把它们从原假设分布里剔出去。

保留横截面依赖

上面第 1 步里”所有策略使用同一组索引”不是实现细节。记第 bb 次迭代抽到的索引为

indices(b)={i1(b),i2(b),…,iT(b)}\text{indices}^{(b)} = \{i_1^{(b)}, i_2^{(b)}, \ldots, i_T^{(b)}\}

策略 jj 在第 tt 个位置上取到的是 dj,it(b)cd^c_{j, i_t^{(b)}}。因为所有策略共用这组索引,如果策略 jj 和策略 kk 在时点 tt 的表现是相关的,重采样后它们仍然在同一批时点上一起好、一起差。

这件事对量化研究的分量比看上去大。假设你搜出来的 30 个策略两两相关性 95%——常见得很,同一个动量信号换 30 组参数就是这个样子——那它们实际上只是一个策略的 30 个抖动版本。最大值的原假设分布会接近单个策略的原假设分布,而不是 30 个独立试验取最大值的那个分布。Reality Check 的拒绝阈值因此比 Bonferroni 低不少:Bonferroni 按 30 个独立检验收费,bootstrap 按实际的有效试验数收费。

时间依赖:重采样什么

上面的第 1 步写的是逐个观测独立重采样,也就是 iid bootstrap。它只在 di,td_{i,t} 时间上独立时才对,而交易策略的日度表现差异基本都有自相关,尤其是有持仓惯性或者波动率聚集的时候。iid 重采样把这些结构洗掉,bootstrap 标准差偏小,原假设分布偏窄,p 值偏乐观。

所以实际要用的是块重采样。White(2000)自己用的就是 Politis 和 Romano(1994)的 stationary bootstrap:块长度 LL 从几何分布里抽,

P(L=ℓ)=(1−p)ℓ−1⋅p,ℓ=1,2,…P(L = \ell) = (1 - p)^{\ell-1} \cdot p, \quad \ell = 1, 2, \ldots

其中 pp 控制期望块长,E[L]=1/pE[L] = 1/p。期望块长就是这里的平滑参数:块越长保留的依赖越多,但相互独立的块数变少、有效样本量下降。经验做法是让期望块长和序列的依赖长度大致相当。自相关较弱的日频股票收益率,p=0.1p = 0.1(期望块长 10 天)是合理起点;波动率这类高度自相关的序列,p=0.05p = 0.05(期望块长 20 天)更合适。常见的块大小区间是 5 到 21 天,也就是一周到一个月;不想手调的话,Politis 和 White(2004)给了一个按数据自相关结构自动选块长的办法。

另一种块方案是 moving block bootstrap:块长固定为 LL,重采样连续观测块,边界靠循环或截断处理。它实现更简单,但块边界处会留下不连续,而 stationary bootstrap 的随机块长天然把边界效应抹平,在弱依赖条件下是抽样分布的一致估计量。大多数量化场景优先选 stationary bootstrap,实现也没复杂多少。

块长是个自由参数,这是躲不掉的:选小了丢依赖,选大了独立块不够用。它同时也是 p 值敏感性的主要来源,后面报告标准那一节还会提到。

一个算完的例子

设有 20 个策略,其中 3 个的真实差异是 E[di]=0.001E[d_i] = 0.001,另外 17 个是 E[di]=0E[d_i] = 0。每个策略差异的标准差都是 σ=0.01\sigma = 0.01,你有 T=1260T = 1260 个日频观测,策略之间相关性 ρ=0.3\rho = 0.3。

那 3 个真有 edge 的策略,studentized 差异的期望值是 1260×0.001/0.01≈3.5\sqrt{1260} \times 0.001 / 0.01 \approx 3.5;加上抽样噪声,三个里最大的那个观测到 4.0 左右很正常。另外 17 个的期望值是 0,观测值就在 0 附近抖。于是 Tobs≈4.0T_{obs} \approx 4.0。

接着 bootstrap:跑 1000 次迭代,每次重采样中心化后的差异、算最大值。假定这个原假设分布的第 95 百分位落在 2.8。Tobs=4.0>2.8T_{obs} = 4.0 > 2.8,p 值低于 0.05,拒绝原假设。

拿 Bonferroni 对一下就有意思了。m=20m = 20、α=0.05\alpha = 0.05 时,Bonferroni 的单侧阈值是 Φ−1(0.9975)=2.81\Phi^{-1}(0.9975) = 2.81,和 bootstrap 阈值几乎一样。这不是巧合:ρ=0.3\rho = 0.3 的相关性还很弱,20 个策略差不多就是 20 个独立试验,bootstrap 原假设和独立同分布原假设自然重合。真正的差别出现在相关性高的时候——同样这批策略如果 ρ=0.7\rho = 0.7,bootstrap 原假设的第 95 百分位大概会落到 2.1 附近,而 Bonferroni 还是死守 2.81。同一批数据,bootstrap 能拒绝的策略更多,而且这个”更多”不是放水,是因为它看见了 Bonferroni 看不见的重复计数。

实现

import numpy as np

def white_reality_check(differentials, B=1000, alpha=0.05):
    """
    White's Reality Check, iid bootstrap version.

    The iid resampling below assumes the differentials are serially
    independent. For real strategy returns, replace the index draw with a
    stationary (Politis-Romano) block bootstrap.
    
    Parameters
    ----------
    differentials : ndarray of shape (T, m)
        Performance differentials d_{i,t} = f_{i,t} - f_{0,t}
    B : int
        Number of bootstrap iterations.
    alpha : float
        Significance level.
    
    Returns
    -------
    p_value : float
        Bootstrap p-value.
    rejected : bool
        Whether to reject the null.
    """
    T, m = differentials.shape
    
    # Original test statistics (studentized)
    d_means = differentials.mean(axis=0)
    d_stds = differentials.std(axis=0, ddof=1)
    t_stats = np.sqrt(T) * d_means / d_stds
    T_obs = t_stats.max()
    
    # Centered differentials (impose the null)
    centered = differentials - d_means
    
    # Bootstrap null distribution
    boot_max = np.zeros(B)
    for b in range(B):
        # Resample with replacement -- same indices for every strategy
        idx = np.random.choice(T, size=T, replace=True)
        boot_centered = centered[idx]
        # Recompute test statistics
        boot_means = boot_centered.mean(axis=0)
        boot_stds = boot_centered.std(axis=0, ddof=1)
        boot_t = np.sqrt(T) * boot_means / boot_stds
        boot_max[b] = boot_t.max()
    
    # P-value
    p_value = (boot_max >= T_obs).mean()
    return p_value, p_value < alpha, t_stats

# Demo
np.random.seed(42)
T, m = 1260, 20
# 3 strategies with true SR = 1.0 (annualized), 17 with SR = 0
true_diff = np.zeros((T, m))
for i in range(3):
    true_diff[:, i] = 0.01 / np.sqrt(252)  # SR ~ 1.0
# Independent noise: this demo has no cross-sectional correlation
noise = np.random.normal(0, 0.01, size=(T, m))
differentials = true_diff + noise

p_value, rejected, t_stats = white_reality_check(differentials, B=1000)
print(f"Reality Check p-value: {p_value:.4f}")
print(f"Reject null: {rejected}")

centered = differentials - d_means 这一行就是整篇文章的重点,删掉它代码照跑,p 值稳定在 0.5 附近。另外注意 idx 是在循环里抽一次、给所有策略共用的,这是横截面依赖得以保留的地方。

Bootstrap 还是参数近似

自然会问:为什么不用去膨胀夏普比率(DSR)那种参数化的 Gumbel 近似,一个闭式解算完就走?

因为 bootstrap 要的假设更少。它不要求收益率高斯,也不要求时间上独立——块重采样正是为后一条准备的。它要求的是弱平稳,以及重采样能有效近似检验统计量的抽样分布。参数化的 Gumbel 近似则要求收益率高斯、各次试验独立、渐近近似在你的样本量上已经够准。

真实数据里,日频策略收益率的尾部比高斯厚,试验之间也远谈不上独立。样本有限、收益率非高斯时,bootstrap 更准;策略高度相关时,bootstrap 尊重实际的依赖结构,Gumbel 近似不会。反过来,收益率接近高斯、试验数很多且彼此独立时,两者结果接近,这时候闭式解便宜得多。计算成本的账下面还会算。

它在什么时候会骗你

前面已经讲了最重要的那条保守性:中心化把所有策略钉在原假设边界上,往候选池里加一堆明显没用的策略会稀释功效。剩下几条也都具体。

统计量的异质性是一条。策略之间的方差差得很远时,最大值会被高方差的那个策略主导——它只是抖得厉害,不是真的强。studentization 缓解了一部分,SPA 在这上面做得更彻底。

平稳性是一条硬假设。stationary bootstrap 的名字就来自它:序列必须弱平稳,重采样出来的块才能代表原过程。策略在样本期内经历了结构性变化——换了执行方式、标的池扩容、市场微观结构变了——bootstrap 原假设分布本身就是错的,p 值再精确也没有意义。

数值上也有坑。方差非常小或非常大的策略,studentized 统计量会不稳定,某些 bootstrap 样本里 σ^i(b)\hat{\sigma}_i^{(b)} 接近零就会产生 NaN 或无穷值,这些值一旦进了 max 就毁掉整个分布,实现时要显式处理。

最后是两个”选择”带来的敏感性。bootstrap 方法本身(iid、moving block、circular block、stationary)以及块长参数的不同取值,可能让 p 值相差 10%–20%;中心化方式也可以换成别的(SPA 就是换了一种),每换一种都得到不同的 p 值。大多数应用里标准实现(stationary bootstrap、dˉ\bar{d} 中心化)已经够用,但如果这个 p 值要支撑一个高风险决策,把上面两个维度各扫几档做一次敏感性分析很值。

计算成本是另一种约束。Reality Check 要跑 BB 次迭代,每次包含 TT 次重采样和 mm 个统计量计算,总成本 O(B⋅T⋅m)O(B \cdot T \cdot m)。B=1000B = 1000、T=1260T = 1260、m=50m = 50 时大约 6300 万次运算,现代机器上几秒钟。B=10000B = 10000、T=5000T = 5000、m=200m = 200 时是 100 亿次运算,几分钟到几小时都有可能。bootstrap 迭代之间彼此独立,可以直接按核并行,墙钟时间大致等于单核时间除以核数。BB 本身也是个取舍:BB 有限时 p 值自带蒙特卡洛方差,筛选阶段 B=1000B = 1000 够用,要写进结论的确认性检验用 B=10000B = 10000。真到了策略极多、历史极长的场景,闭式的 DSR 是个有用的退路。

该报告什么

只说一句”Reality Check 的 p 值是 0.04”是不完整的。同一批数据换个 bootstrap 方法、换个块长、换个检验统计量,p 值就变了,读者没有这些信息就无法解释你的结果。一份可复现的报告至少要给出:

  1. 策略数量 mm。
  2. 使用的表现函数(夏普、平均收益等)。
  3. 基准(零、无风险利率、被动指数等)。
  4. bootstrap 迭代次数 BB。
  5. bootstrap 方法(iid、moving block、stationary)。
  6. 块长参数(stationary bootstrap 报期望块长 1/p1/p)。
  7. 观测检验统计量 TobsT_{obs}。
  8. bootstrap p 值。
  9. 在选定显著性水平下的决策。

基准这一项容易被当成走过场,其实它决定了你在回答哪个问题。差异 di,t=fi,t−f0,td_{i,t} = f_{i,t} - f_{0,t} 是相对于某个具体基准定义的:一个策略跑赢无风险利率但跑不赢标普 500,说它”相对现金有能力”成立,说它”相对被动股票有能力”就不成立。两句话都是真的,只是不能互相替代。如果你想同时对多个基准做检验,得对每个基准分别跑一遍 Reality Check,然后跨基准的合并结论还需要再做一层多重比较修正(Bonferroni 或 Romano-Wolf)。

它到底证明了什么

Reality Check 早期最有名的应用是 Sullivan、Timmermann 和 White(1999):他们在股指数据上测了 7846 条技术交易规则。很多规则在样本内产生了统计显著的收益,这一点毫不意外;经过 Reality Check 修正之后,剩下显著的规则很少,而且那几条剩下的,经济价值也很有限。这篇论文之所以成为经典,是因为它把”样本内显著”和”考虑了搜索过程之后仍然显著”之间的落差量化了出来——落差大到足以解释当时相当一部分已发表的”有效规则”。

这也是 Reality Check 的历史位置。2000 年之前,量化研究员发表”显著”策略时常常不交代自己试过多少个;White 说明了这类结果里有多大比例可能是假阳性。它开启的方法论讨论一直延续到今天,多重检验修正已经是学术金融的标准要求(Harvey、Liu 和 Zhu 2016 对因子研究的那次清算就是这个传统),业界也在跟上。

到今天,Reality Check 很少再是研究里唯一使用的检验,SPA 和 DSR 在大多数场合更合适。但它仍然值得从头推一遍,因为它把逻辑露在外面:用 bootstrap 生成原假设分布,把观测统计量放进去比,读尾部概率。SPA 换了中心化方式,Romano-Wolf 加了逐步下降,DSR 换成闭式近似——三者都是在这条骨架上改的某一处。把 Reality Check 推明白了,它们看起来就都是自然的下一步。

如果这篇文章只留下一件事,那就是中心化:bootstrap 必须从一个真的满足原假设的分布里采样。少了这一步,剩下的推导都还在,只是不再检验任何东西。

参考文献

  • White, H. (2000). “A Reality Check for Data Snooping.” Econometrica, 68(5), 1097–1126。原始论文。
  • Hansen, P. R. (2005). “A Test for Superior Predictive Ability.” Journal of Business and Economic Statistics, 23(4), 365–380。SPA 修正。
  • Politis, D. N. and Romano, J. P. (1994). “The Stationary Bootstrap.” Journal of the American Statistical Association, 89(428), 1303–1313。Stationary bootstrap。
  • Sullivan, R., Timmermann, A., and White, H. (1999). “Data-Snooping, Technical Trading Rule Performance, and the Bootstrap.” Journal of Finance, 54(5), 1647–1691。早期交易规则应用。