Romano-Wolf 逐步下降法:兼顾统计功效的 Bootstrap 多重检验
你的 50 个策略不是 50 个独立检验。Bonferroni 假装它们是,Romano-Wolf 用 bootstrap 把真正的有效检验数测出来——这就是它多出来的功效的全部来源。
你有 50 个策略,全都是同一个动量信号的不同变体。Bonferroni 拒绝 1 个,Holm 拒绝 3 个。你的直觉是这个结果太苛刻了,而且你的直觉是对的——但理由不是”检验太严”,而是这 50 个检验根本不是 50 个检验。
如果这 50 个策略的收益率两两相关性是 0.95,它们提供的独立信息大约相当于 3 个检验,不是 50 个。Bonferroni 把阈值按 50 分,Holm 按排序稍微放松一点,但两者都不看你的策略之间到底有多像。Romano-Wolf 逐步下降程序做的就是这件事:用 bootstrap 把”有效检验数”从数据里测出来,再据此定阈值。
本文推导这个程序,走一遍 bootstrap 逐步下降逻辑,并说清楚它多出来的功效具体从哪里来、在什么条件下这些功效会消失。
功效问题:有效检验数
Bonferroni 和 Holm 都只用一个输入——检验数量 ——来设阈值。它们不看检验统计量之间的依赖结构。在正相关下,有效独立检验数远小于 ,所以这两个阈值都定得过严。
这句话可以直接量化。取 个 studentized 检验统计量,在原假设下服从标准正态,两两等相关系数为 。我们关心的是 在原假设下的分布——因为控制 FWER 就等于控制”最大值超出阈值”这件事的概率。下面是 20 万次抽样得到的第 95 百分位,以及反解出来的有效检验数(满足 的那个 ):
| 两两相关性 | 原假设分布的 95 百分位 | 有效检验数 |
|---|---|---|
| 0(独立) | 2.92 | 29.3 |
| 0.5 | 2.75 | 17.2 |
| 0.9 | 2.23 | 4.0 |
| 0.99 | 1.84 | 1.5 |
第一行是这张表的自检:独立时反解出来的有效检验数是 29.3,基本就是 30,说明这套算法没有偏。Bonferroni 在 、 下的单侧阈值是 ,和第一行的 2.92 几乎重合——这就是 Bonferroni 的全部内容:它永远按”独立”那一行给你阈值,不管你的策略实际长在哪一行。
而量化研究的策略池几乎从不落在第一行。同一个动量信号换 5 个回看窗口、再乘 6 个股票池,得到的 30 条收益率曲线相关性轻松到 0.9 以上,也就是表里第三行:真正的阈值是 2.23,Bonferroni 却给你 2.94。一个真实年化夏普 0.8、跑了 10 年的策略,t 统计量大约是 。它过得了 2.23,过不了 2.94。Bonferroni 会把它当噪声扔掉。
Romano-Wolf(2005)通过 bootstrap 计算修正 p 值。修正 p 值自动落在你的策略池实际所在的那一行,因此程序在同样控制 FWER 的前提下,能比 Bonferroni 或 Holm 拒绝更多原假设。
算法
- 对每个策略 ,在原始数据上计算检验统计量 (例如 studentized 夏普)。
- 对每次 bootstrap 迭代 :
- 重采样数据(时间序列数据用 block bootstrap)。
- 对每个策略 ,在原假设下计算 bootstrap 检验统计量 ,得到一个 的矩阵。
- 把原始检验统计量从大到小排序:。
- 逐步下降。令 是走到第 步时还没有被拒绝的那些假设,取 bootstrap 的最大值只在这个集合上取:
第 个假设的 Romano-Wolf 修正 p 值是
其中 ,外层取 max 是为了保证修正 p 值随排序单调不减。若 就拒绝 并进入第 步;否则停止,剩下的假设全部不拒绝。
关键在第 4 步里那个 。它每走一步就少一个元素,而 是在一个越来越小的集合上取的,所以 的分布随 单调左移,后面的假设面对的阈值越来越低。这就是”逐步下降”这四个字的全部含义,也是它比单步版本更有功效的唯一原因。
单步版本,以及差在哪
作为对照,更简单的单步(single-step)版本永远在全部 个假设上取最大值:
对最大的那个统计量(),两者完全相同——因为此时 就是全集。差别从第二个开始出现,并且随着 增大而扩大:单步版本让排第 20 位的策略去跟”30 个统计量的最大值”比,逐步下降版本只让它跟”剩下 11 个的最大值”比。
在一次 、、 的模拟里,两者的修正 p 值是这样的(只列前几名):
| 逐步下降 | 单步 | |
|---|---|---|
| 4.75 | 0.000 | 0.000 |
| 3.91 | 0.001 | 0.001 |
| 2.96 | 0.017 | 0.017 |
| 2.88 | 0.020 | 0.021 |
| 2.60 | 0.041 | 0.043 |
| 2.10 | 0.129 | 0.141 |
| 1.73 | 0.129 | 0.247 |
排名靠前时差别小到看不见,排名靠后时差别就明显了。注意逐步下降那一列在 0.129 处平掉了——那是程序在 处停下来之后,剩余假设共享同一个修正 p 值的结果。标准实现用逐步下降,用它就是了。
Studentization:不是可选项
检验统计量可以用原始值 ,也可以用 studentized 值 。当各策略的波动率不同时,后者不是风格问题而是正确性问题:没有 studentization,高波动策略会系统性地主导最大值,于是 的 bootstrap 分布实际上只反映了那几个高波动策略的原假设,低波动策略被拿一个跟自己无关的阈值去检验。标准的 Romano-Wolf 实现一律 studentize:
比 studentization 更容易写错的是原假设中心化——bootstrap 必须在原假设成立的世界里生成统计量,而”原假设成立”具体是什么样,取决于你检验的是哪个原假设。
检验 时,中心化后的收益率是 ,于是 ,bootstrap 样本的期望超额收益正好被压到零。检验 ( 是基准)时,中心化后的收益率是 ,此时 ,原假设下的检验统计量是 。检验 时对夏普做同样性质的中心化。
实现上有个坑值得单独点出来:中心化必须减掉原始样本的均值,不能减掉每个 bootstrap 重采样自己的均值。后者会把每一次重采样的均值精确压成零,于是每个 bootstrap 统计量恒等于零,最大值也恒等于零,所有策略都会被”拒绝”。这个 bug 不会报错,只会给你一份漂亮的结果。
Bootstrap 是怎么学到依赖结构的
Romano-Wolf 能工作,是因为 bootstrap 重采样的是联合收益率分布。多个策略的收益率相关时,重采样保留了这种相关性,于是 的 bootstrap 原假设分布自动落在前面那张表里对应的那一行上。
上面那张表其实就是这件事的封闭形式版本:Bonferroni 假设最坏情况(独立),Holm 用了排序但阈值仍来自解析公式,而 bootstrap 是以你手上这批数据的实际依赖结构为条件在算分布。它不需要你告诉它 是多少,也不需要依赖结构是等相关这种好看的形式。
代价是 bootstrap 得重采样对,才能保留依赖。金融收益率有自相关和波动率聚集,不可交换,所以简单的逐点有放回重采样会打碎时间依赖。这也是 Romano-Wolf 用 bootstrap 而不是用置换的原因:置换是不放回地重排数据,保留边际分布但打破联合结构;它在可交换数据上是精确的,在时间序列上不是。正确做法是 block bootstrap,按观测块重采样,或者用 stationary bootstrap(块长随机)。
块长(或者说截断滞后) 是这里唯一一个需要你拿主意的参数。Politis 和 White(2004)给了一个基于自相关函数谱估计的自动选择规则,R 的 np::b.star() 是它的标准实现,能用就用它。手工选的话,Newey-West 式的经验规则 是个合理起点;对日频股票收益率取 (一周)、对月收益率取 (一个季度)也够用,更高频的数据需要更大的 。好消息是在中等以上的 下,p 值对 相当不敏感—— 且 取值合理时,差异基本可以忽略。
一个算到底的例子
回到开头那批策略,把数字填实。30 个策略,全是同一个动量信号的变体,收益率两两相关性 ,日频数据 (10 年)。
先看真实优势能产生多大的统计量。一个真实年化夏普 0.8 的策略,跑满 10 年,它的 t 统计量是
(注意这里不是拿夏普去除以波动率——夏普本身已经是波动率归一化过的量,再除一次没有意义。把年化夏普换算成 t 统计量,乘的是样本年数的平方根。)
再看阈值。按前面那张表, 时 的原假设分布 95 百分位是 2.23,而 Bonferroni 给的是 2.94。于是:
- Romano-Wolf 拒绝所有 的策略。
- Bonferroni 只拒绝 的策略。
一个真实夏普 0.8 的策略,期望统计量 2.53,落在这两个阈值中间。Romano-Wolf 找得到它,Bonferroni 找不到。这就是那 0.7 个单位的阈值差在实践中意味着什么。
顺带说一句这个优势的边界在哪:把 换成 0.5,阈值是 2.75,跟 Bonferroni 的 2.94 只差 0.19,多拒绝的策略寥寥无几。Romano-Wolf 的功效增益正比于你的策略实际有多像。策略池越同质,它赢得越多;策略池真的多样化,它和 Bonferroni 就没什么差别——这时候 Bonferroni 也不冤枉,因为你确实做了那么多独立检验。
各程序的对照
把 50 个策略、其中 10 个真实夏普 0.5、40 个真实夏普为 0 的情形跑一遍,各程序大致是这样的表现:
| 程序 | FWER 控制 | 功效(拒绝的真阳性) | 假阳性 |
|---|---|---|---|
| 不修正 | 无(FWER ≈ 87%) | 10 个中拒绝 9 个 | 40 个中约 2 个 |
| Bonferroni | 是(在 下) | 10 个中拒绝 1 个 | 40 个中 0 个 |
| Holm | 是(在 下) | 10 个中拒绝 3 个 | 40 个中 0 个 |
| Benjamini-Hochberg | FDR(在 下) | 10 个中拒绝 6 个 | 40 个中 0–1 个 |
| Romano-Wolf | 是(在 下) | 10 个中拒绝 7 个 | 40 个中 0 个 |
这些数字是示意,实际功效取决于效应大小、样本量和依赖结构,但模式很稳定:在保持 FWER 控制的同时,Romano-Wolf 的功效高于 Bonferroni 和 Holm。
第一行需要解释一下,因为它最容易被误读。不修正时,40 个真原假设在 下的期望假阳性只有 2 个,听起来不算多;真正的问题是至少出现一个假阳性的概率是 。FWER 控制的对象是这个 87%,不是那个 2。你只从池子里挑一个策略去部署时,“至少一个假阳性”就是”你挑中的那个可能是假的”。
BH 的位置也值得说清楚。它产生更多拒绝,但明确允许其中混入一定比例的假阳性——这是 FDR 的定义,不是它的缺陷。如果你的目的是”选一批候选进下一轮测试”,BH 合适;如果你的目的是”选出一个能直接上钱的策略”,你要的是 FWER,Romano-Wolf 更对路。
它什么时候不管用
三种失效模式。
Bootstrap 没抓到依赖结构。 如果原始数据和 bootstrap 重采样之间的依赖结构不一致(最常见的原因是非平稳),修正 p 值就是错的,而且错的方向不确定。用 block bootstrap 保留时间依赖是底线,但 block bootstrap 也救不了真正的结构断裂。
策略太少。 只有 5 个策略时,最大值的 bootstrap 分布本身就很嘈杂,Romano-Wolf 相对 Bonferroni 的优势小到不值得多写这些代码。直接用 Bonferroni。
策略几乎完全相同。 如果 30 个策略的相关性达到 0.99,按前面那张表,有效检验数是 1.5——bootstrap 基本把它们看成一个策略。Romano-Wolf 仍然正确控制 FWER,但它拒绝的策略数会少于你的期待。这不是程序失灵,这是它在如实汇报:30 个实质相同的策略,不等于 30 份证据。
一份可跑的实现
import numpy as np
def romano_wolf(returns_matrix, benchmark=0.0, B=1000, alpha=0.05, seed=0):
"""
Romano-Wolf step-down procedure for multiple testing (one-sided).
Parameters
----------
returns_matrix : ndarray of shape (T, m)
Returns for each of m strategies over T observations.
benchmark : float
Benchmark mean return (default 0).
B : int
Number of bootstrap iterations.
alpha : float
FWER level.
Returns
-------
rejected : ndarray of bool, shape (m,)
t_stats : ndarray of float, shape (m,)
rw_pvals : ndarray of float, shape (m,)
"""
rng = np.random.default_rng(seed)
T, m = returns_matrix.shape
# Test statistics on the original data
excess = returns_matrix - benchmark
means = excess.mean(axis=0)
stds = excess.std(axis=0, ddof=1)
t_stats = means / (stds / np.sqrt(T))
# Bootstrap under the null; the B x m matrix is computed ONCE
boot_t = np.zeros((B, m))
for b in range(B):
idx = rng.integers(0, T, size=T)
resampled = excess[idx]
centered = resampled - means # centre on the ORIGINAL mean, not the resample's
boot_means = centered.mean(axis=0)
boot_stds = centered.std(axis=0, ddof=1)
boot_t[b] = boot_means / (boot_stds / np.sqrt(T))
# Step-down: the max is taken over the shrinking active set
order = np.argsort(-t_stats)
rw_pvals = np.ones(m)
rejected = np.zeros(m, dtype=bool)
active = list(order)
prev_p = 0.0
while active:
i = active[0]
max_active = boot_t[:, active].max(axis=1)
p = max((max_active >= t_stats[i]).mean(), prev_p) # enforce monotonicity
rw_pvals[i] = p
if p > alpha:
for j in active:
rw_pvals[j] = p
break
rejected[i] = True
prev_p = p
active.pop(0)
return rejected, t_stats, rw_pvals
# Demo: 30 correlated strategies, 5 of them with a real edge
rng = np.random.default_rng(14)
T, m, rho = 2520, 30, 0.7
annual_sr = np.array([1.0] * 5 + [0.0] * 25)
daily_vol = 0.01
daily_mean = annual_sr / np.sqrt(252) * daily_vol
common = rng.standard_normal((T, 1))
idio = rng.standard_normal((T, m))
returns = daily_mean + (np.sqrt(rho) * common + np.sqrt(1 - rho) * idio) * daily_vol
rejected, t_stats, rw_pvals = romano_wolf(returns, B=1000, seed=7)
print(f"Romano-Wolf rejects: {rejected.sum()} of {m}")
print(f"True positives: {rejected[:5].sum()} of 5, false positives: {rejected[5:].sum()} of 25")
这个 demo 会拒绝 4 个策略:4 个真阳性、0 个假阳性。第 5 个真有优势的策略被漏掉了,因为它这一次的 t 统计量只有 1.74——这就是 FWER 控制的代价,你换来的是”拒绝的 4 个里一个假的都没有”这个保证。换几十个不同的数据种子重跑,典型结果是拒绝 4 到 5 个真策略、假阳性一直是 0;把 annual_sr 全部设成 0 重复几百次,经验 FWER 落在 0.05 以下,程序偏保守——这符合预期,单侧 bootstrap 在所有原假设都恰好取在边界上时本来就会保守一些。
代码里有一处值得再强调:boot_t 这个 矩阵只算一次。逐步下降的每一步只是在这个矩阵的一个子集列上重新取 max,成本是 ,相对于 bootstrap 本身( 次重采样、每次 次运算)可以忽略。所以”逐步下降比单步慢很多”是个误解——多花的那点时间在几百个策略的规模上都看不出来。真正的成本一直是 和 。
它和 DSR、SPA、Westfall-Young 的关系
去膨胀夏普比率(DSR)和 Romano-Wolf 回答的是不同的问题。DSR 问:“既然我从 个策略里挑了最好的一个,我报告的这个夏普能不能和原假设下的期望最大值区分开?“这是对选中的那一个做单假设检验。Romano-Wolf 问:“修正多重检验、并考虑依赖结构之后,我这 个策略里哪些的收益能和基准区分开?“这是多假设检验,输出是一个子集而不是一个是非题。
两者配合使用的顺序是清楚的:先用 Romano-Wolf 划出有统计证据的候选子集,再用 DSR 评估最终挑中的那一个。如果 Romano-Wolf 一个都没拒绝,就没必要往下走了;如果拒绝了几个,就对其中最好的那个跑 DSR,看这个”最好”是不是选择偏差的产物。
Westfall-Young(1993)的重采样多重检验和 Romano-Wolf 血缘很近,两者都用重采样算修正 p 值、都尊重依赖结构,而且 Westfall-Young 本身就同时给出了单步和逐步下降两个版本——所以差别不在”有没有 step-down”。真正的差别在假设上:Westfall-Young 的理论依赖 subset pivotality(子集枢轴性)这个条件,它在一些实际情形下会失效;Romano-Wolf 的贡献是给出一个不需要这个条件的框架,在相当一般的依赖结构下证明了渐近 FWER 控制。金融数据上两种方法通常给出接近的结果,推荐 Romano-Wolf 是因为它对假设的要求更弱。
至于 SPA,它和 Romano-Wolf 处理的是同一类问题的不同切面:SPA 针对异方差和单侧备择做了专门处理,输出是对”最好的策略是否优于基准”的单个 p 值。实践中三个一起跑是有价值的——DSR 先做一轮快速检查,Romano-Wolf 划候选子集,SPA 从另一个方法论视角确认。三者一致时结论比较硬;不一致时,多半是假设出了分歧:DSR 假设收益率高斯且试验独立,Romano-Wolf 通过 bootstrap 尊重依赖结构,SPA 处理异方差和单侧备择。查清楚是哪条假设在你的数据上不成立,比追问”哪个方法对”更有用。
家族结构:一个真实的边界
Romano-Wolf 假设所有检验属于同一个家族,FWER 在整个家族上得到控制。当家族本身有结构时,这个假设值得推敲。
举例:在 10 个股票池上测 5 个动量窗口,一共 50 个检验,但它们天然分成 10 组或 5 组。Romano-Wolf 会把 FWER 控制在这 50 个检验的整体上——注意,问题不是它把它们当成了独立检验(bootstrap 恰恰会看到组内的高相关,这正是它的长处),而是”整体 FWER”这个目标本身可能比你真正想要的更严。如果你的决策单位是”每个股票池里选一个窗口”,那么合适的目标是在每个股票池内部控制错误,而不是在 50 个检验的全局上控制。
对这类结构化家族,文献里有专门的分层检验程序可用。Benjamini 和 Yekutieli(2001)常在这个语境里被提到,但要说清楚它做的是什么:它证明的是 BH 型 FDR 控制在任意依赖结构下的有效性(代价是一个 的调整因子),不是分层家族的 FWER 程序。分层结构要另找工具。
对绝大多数量化场景,基础的 Romano-Wolf 已经够用,把整个搜索空间当成一个家族也是诚实的——毕竟你确实在整个空间上搜过。
怎么读它的输出,以及它不告诉你什么
Romano-Wolf 的输出是一个通过 FWER 修正检验的策略子集,这个子集带一个明确的保证:以至少 的概率,子集里没有任何一个假阳性。这是 FWER 陈述,比 FDR 强得多——FDR 只保证假阳性占拒绝数的比例受控。(上面的算法用的是 ,对应单侧备择”优于基准”;要做双侧,把最大值换成 。)
这个保证很强,但它保证的东西很窄。它说的是”这些策略的收益在统计上能和基准区分开”,不是”这些策略经过了验证”。一个通过 Romano-Wolf 的策略仍然可能预期收益为负(如果基准本身很高)、可能对容量极度敏感、可能只在某一种市场状态下有效、也可能有实现层面的问题。这些东西没有一样在 Romano-Wolf 的射程之内。
所以它在流程里的位置是筛选,不是判决。一个可用的工作流大致是:搜索假设空间生成 个候选策略;对每个策略计算相对基准的 studentized 表现差异;用 次 bootstrap(块长按数据自相关选)跑 Romano-Wolf,得到候选子集;对子集里的每个策略再跑其他诊断——Haircut Sharpe、PBO、容量分析、状态检验;综合所有结果决定升级到模拟交易还是拒绝。
这里的关键是”综合”。过了 Romano-Wolf 但过不了容量分析的策略没准备好;容量没问题却过不了 Romano-Wolf 的策略值得怀疑。两项都要过,而且这两项谁也代替不了谁。
报告的时候,“Romano-Wolf 在 50 个策略中拒绝了 5 个”这一句是不完整的。读者需要看到策略数量 、用的是哪个检验统计量、bootstrap 方法和迭代次数 、每个策略的修正 p 值,以及结果对 bootstrap 参数(尤其是块长)的敏感性。修正 p 值那一列尤其重要——一个 的策略和一个 的策略在”拒绝”这个二元标签下看起来一样,实际上完全不是一回事。
对中等规模的 ,Romano-Wolf 是控制 FWER 最有功效的程序,这也是它在研究论文里成为默认选择的原因。行业里 SPA 和 DSR 更常见,主要是因为它们更容易实现、更容易向非统计背景的人解释,而 Romano-Wolf 需要更多 bootstrap 迭代才能让 p 值稳定。这是工程取舍,不是方法论优劣。
参考文献
- Romano, J. P. and Wolf, M. (2005). “Stepwise Multiple Testing as Formalized Data Snooping.” Econometrica, 73(4), 1237–1282.
- Romano, J. P. and Wolf, M. (2005). “Exact and Approximate Stepdown Methods for Multiple Hypothesis Testing.” Journal of the American Statistical Association, 100(469), 94–108.
- Westfall, P. H. and Young, S. S. (1993). Resampling-Based Multiple Testing. Wiley。重采样多重检验的完整专著,单步与逐步下降版本都在里面。
- Politis, D. N. and White, H. (2004). “Automatic Block-Length Selection for the Dependent Bootstrap.” Econometric Reviews, 23(1), 53–70.
- Benjamini, Y. and Yekutieli, D. (2001). “The Control of the False Discovery Rate in Multiple Testing under Dependency.” Annals of Statistics, 29(4), 1165–1188.
- López de Prado, M. (2018). Advances in Financial Machine Learning. Wiley. ISBN 978-1-119-48208-6。第 14 章讲回测统计量与去膨胀夏普比率。