White Reality Check:推导与中心化问题
White 的 Reality Check 是金融里第一个正经的数据窥探检验。整篇推导的重心其实只有一步:在 bootstrap 之前把每个策略的表现差异减掉自己的均值。这一步决定了检验是有意义的还是永远不会拒绝。
你搜了几百个策略,留下了最好的那个。它的表现看上去很不错——但你要回答的不是”它好不好”,而是”在一堆纯噪声策略里挑最好的那个,本来也能好到什么程度”。
Halbert White 在 2000 年给出的答案是:别去猜这个分布,把它 bootstrap 出来。Reality Check 的推导本身不长,bootstrap 是引擎,真正决定检验成不成立的是中间那一步中心化。这一步做错,检验永远不会拒绝原假设,而且它错得毫无声响——你还是会拿到一个漂亮的 p 值。下面从设定开始,一路推到那一步,再看后来的方法是从哪儿接着往下改的。
问题与统计量
你有 个候选策略,每个都有一段回测收益率序列,以及一个共同的基准。定义 为策略 在时点 的表现指标, 为基准在同一时点的表现,两者之差是
原假设说的是”这批策略里没有一个真的比基准强”:
注意这是个复合原假设, 个不等式同时成立才算原假设为真。这也解释了为什么它是个多重检验问题。但和 Bonferroni、Holm 那类修正不同,Reality Check 不去套一个参数分布做近似,而是直接从数据里把原假设分布生成出来。
检验统计量取所有策略中最大的 studentized 差异:
其中 , 是 的样本标准差。取最大值而不是逐个看 ,是因为备择假设本身就是”至少有一个策略真的更好”,而你实际做的动作也正是把最大的那个挑出来——检验统计量应该和你的选择过程对齐。备择稀疏(只有少数策略真有 edge)时,最大值统计量的功效不错;备择稠密(很多策略都不是原假设)时,别的统计量可能更灵敏。它是个合理的默认值,不是什么最优检验,别把它当成一致最优功效检验来用。
一处出入值得先说清楚:White 原文用的统计量是未 studentize 的 。这里用的是 studentized 形式,好处是不同波动率的策略可比,这也是 Hansen 后来在 SPA 里正式采用的写法。bootstrap 的逻辑不受影响,两种写法都能往下推。
把原假设 bootstrap 出来
关键要求是:bootstrap 重采样必须来自一个真的满足原假设的分布,也就是对每个策略都有 的分布。观测到的 显然不满足这一点——你挑出来的策略均值是正的。所以先减掉自己的均值:
这样每个策略的 ,被强行摁在原假设的边界上。之后所有的重采样都从 上做。
每一次 bootstrap 迭代 做五件事:
- 从 中有放回地重采样 个观测。所有策略使用同一组索引,以保留横截面依赖。
- 计算 bootstrap 差异均值:\bar{d}_i^{(b)} = \frac{1}{T} \sum_t d^c_{i,t}^{(b)}。
- 计算 bootstrap 标准差:。
- 计算 bootstrap studentized 统计量:。
- 记录最大值:。
跑完 次, 就是最大值统计量在原假设下的经验分布,p 值是观测值落在它右尾多深:
就拒绝。拒绝的含义要说准:把搜索过程本身的多重检验结构算进去之后,最优策略的表现仍然和基准可区分。
中心化为什么是全部
White 的贡献不是 bootstrap,Efron 几十年前就把 bootstrap 提出来了。White 的贡献是想清楚了在 bootstrap 什么。
去掉中心化那一行看看会发生什么。重采样直接从观测到的 上做,而观测数据里带着你挑出来的那个正均值。于是 bootstrap 最大值分布不是围绕零,而是围绕 ——观测统计量落在自己分布的正中间,p 值大约 0.5,检验永远不会拒绝。它不报错、不发警告,就是安静地失效。
中心化把每个策略移到 上,重采样生成的是”如果原假设成立、而依赖结构又和你手上的数据一样,最大统计量通常能有多大”。观测值只有落到这个分布的尾部才算证据。所有 bootstrap 假设检验都靠这个技巧吃饭,White 只是第一个把它用在数据窥探问题上的人。
代价藏在一个细节里。原假设是 ,不是 ;把每个策略都摁到零上,等于假设所有策略都恰好站在原假设最不利的那个边界上。真实情况是一堆策略的 明显为负,它们本来不该对 bootstrap 最大值有什么贡献,中心化之后却个个都有机会贡献一个大的 。结果是原假设分布被抬高,检验偏保守。这个保守性不是小毛病:往候选池里多塞一批明显没用的策略,Reality Check 的功效会实实在在地掉下去,而按理说加一堆垃圾策略不该让一个真有 edge 的策略更难被发现。Hansen(2005)的 SPA 检验改的就是这里——它对明显为负的策略做不同的重新中心化,把它们从原假设分布里剔出去。
保留横截面依赖
上面第 1 步里”所有策略使用同一组索引”不是实现细节。记第 次迭代抽到的索引为
策略 在第 个位置上取到的是 。因为所有策略共用这组索引,如果策略 和策略 在时点 的表现是相关的,重采样后它们仍然在同一批时点上一起好、一起差。
这件事对量化研究的分量比看上去大。假设你搜出来的 30 个策略两两相关性 95%——常见得很,同一个动量信号换 30 组参数就是这个样子——那它们实际上只是一个策略的 30 个抖动版本。最大值的原假设分布会接近单个策略的原假设分布,而不是 30 个独立试验取最大值的那个分布。Reality Check 的拒绝阈值因此比 Bonferroni 低不少:Bonferroni 按 30 个独立检验收费,bootstrap 按实际的有效试验数收费。
时间依赖:重采样什么
上面的第 1 步写的是逐个观测独立重采样,也就是 iid bootstrap。它只在 时间上独立时才对,而交易策略的日度表现差异基本都有自相关,尤其是有持仓惯性或者波动率聚集的时候。iid 重采样把这些结构洗掉,bootstrap 标准差偏小,原假设分布偏窄,p 值偏乐观。
所以实际要用的是块重采样。White(2000)自己用的就是 Politis 和 Romano(1994)的 stationary bootstrap:块长度 从几何分布里抽,
其中 控制期望块长,。期望块长就是这里的平滑参数:块越长保留的依赖越多,但相互独立的块数变少、有效样本量下降。经验做法是让期望块长和序列的依赖长度大致相当。自相关较弱的日频股票收益率,(期望块长 10 天)是合理起点;波动率这类高度自相关的序列,(期望块长 20 天)更合适。常见的块大小区间是 5 到 21 天,也就是一周到一个月;不想手调的话,Politis 和 White(2004)给了一个按数据自相关结构自动选块长的办法。
另一种块方案是 moving block bootstrap:块长固定为 ,重采样连续观测块,边界靠循环或截断处理。它实现更简单,但块边界处会留下不连续,而 stationary bootstrap 的随机块长天然把边界效应抹平,在弱依赖条件下是抽样分布的一致估计量。大多数量化场景优先选 stationary bootstrap,实现也没复杂多少。
块长是个自由参数,这是躲不掉的:选小了丢依赖,选大了独立块不够用。它同时也是 p 值敏感性的主要来源,后面报告标准那一节还会提到。
一个算完的例子
设有 20 个策略,其中 3 个的真实差异是 ,另外 17 个是 。每个策略差异的标准差都是 ,你有 个日频观测,策略之间相关性 。
那 3 个真有 edge 的策略,studentized 差异的期望值是 ;加上抽样噪声,三个里最大的那个观测到 4.0 左右很正常。另外 17 个的期望值是 0,观测值就在 0 附近抖。于是 。
接着 bootstrap:跑 1000 次迭代,每次重采样中心化后的差异、算最大值。假定这个原假设分布的第 95 百分位落在 2.8。,p 值低于 0.05,拒绝原假设。
拿 Bonferroni 对一下就有意思了。、 时,Bonferroni 的单侧阈值是 ,和 bootstrap 阈值几乎一样。这不是巧合: 的相关性还很弱,20 个策略差不多就是 20 个独立试验,bootstrap 原假设和独立同分布原假设自然重合。真正的差别出现在相关性高的时候——同样这批策略如果 ,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 样本里 接近零就会产生 NaN 或无穷值,这些值一旦进了 max 就毁掉整个分布,实现时要显式处理。
最后是两个”选择”带来的敏感性。bootstrap 方法本身(iid、moving block、circular block、stationary)以及块长参数的不同取值,可能让 p 值相差 10%–20%;中心化方式也可以换成别的(SPA 就是换了一种),每换一种都得到不同的 p 值。大多数应用里标准实现(stationary bootstrap、 中心化)已经够用,但如果这个 p 值要支撑一个高风险决策,把上面两个维度各扫几档做一次敏感性分析很值。
计算成本是另一种约束。Reality Check 要跑 次迭代,每次包含 次重采样和 个统计量计算,总成本 。、、 时大约 6300 万次运算,现代机器上几秒钟。、、 时是 100 亿次运算,几分钟到几小时都有可能。bootstrap 迭代之间彼此独立,可以直接按核并行,墙钟时间大致等于单核时间除以核数。 本身也是个取舍: 有限时 p 值自带蒙特卡洛方差,筛选阶段 够用,要写进结论的确认性检验用 。真到了策略极多、历史极长的场景,闭式的 DSR 是个有用的退路。
该报告什么
只说一句”Reality Check 的 p 值是 0.04”是不完整的。同一批数据换个 bootstrap 方法、换个块长、换个检验统计量,p 值就变了,读者没有这些信息就无法解释你的结果。一份可复现的报告至少要给出:
- 策略数量 。
- 使用的表现函数(夏普、平均收益等)。
- 基准(零、无风险利率、被动指数等)。
- bootstrap 迭代次数 。
- bootstrap 方法(iid、moving block、stationary)。
- 块长参数(stationary bootstrap 报期望块长 )。
- 观测检验统计量 。
- bootstrap p 值。
- 在选定显著性水平下的决策。
基准这一项容易被当成走过场,其实它决定了你在回答哪个问题。差异 是相对于某个具体基准定义的:一个策略跑赢无风险利率但跑不赢标普 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。早期交易规则应用。