halo 的技术博客

返回

因子研究的死亡陷阱之一:你算出一个 Rank-IC = 0.05,兴奋地写进报告,上线后却亏钱。问题出在哪?那个 0.05 只是点估计,没有显著边界——它可能是真信号,也可能只是 120 个月里某一段牛市的偶然巧合。没有置信区间,你无法区分”弱因子”和”纯噪声”。

本文用 numpy 从零实现 block bootstrap,给你的因子 Rank-IC 算出一个有统计意义的 95% 置信区间。关键细节:金融时间序列有强自相关、截面有横截面相关,普通 bootstrap(逐点重抽)会破坏这些结构、虚假夸大显著性——所以必须用按月整块重抽(block bootstrap)。在 N=200 股票 / T=120 月、真实 IC=0.06 的合成数据上,观测 IC=0.094,block bootstrap 给出 95% CI = [0.082, 0.104]、t-stat=16.4、p≈0,稳健判显著。附完整 Python 与三张真实计算图。

一、为什么普通 bootstrap 在金融里会骗你#

因子检验的统计量通常是截面 Rank-IC:

ICt=rank-corr(zi,t,ri,t+1)IC_t = \mathrm{rank\text{-}corr}\big(z_{i,t},\, r_{i,t+1}\big)

把所有月份拼起来算一个总体 IC。要判断它是否显著不为 0,朴素做法是正态近似:

t=ICˉSE(ICˉ),SE=σICTt = \frac{\bar{IC}}{SE(\bar{IC})}, \quad SE = \frac{\sigma_{IC}}{\sqrt{T}}

但这依赖一个隐性假设:各月 IC 独立同分布。金融里这几乎不成立——

  • 月份之间收益高度自相关(动量/反转的延续),IC 不是独立抽样的;
  • 同一天所有股票共享市场冲击,截面相关让”N=200 只”远不是 200 个独立样本。

普通 bootstrap(把每个月 IC 当独立样本逐点重抽)会假装这些相关不存在,从而低估标准误、夸大显著性——把噪声因子判成显著。

Block bootstrap 的解法:把数据按”月”切成整块,重抽时整块抽,块内结构(自相关、截面相关)原样保留。这样 bootstrap 出来的 IC 分布,才反映真实的抽样不确定性。

二、造数据:一个真实 IC=0.06 的弱因子#

这里因子只对下期收益解释 0.06 的相关,叠加很强的个股噪声——正是实战里”弱但真实”的因子形态。

三、Block bootstrap:按月整块重抽#

B = 2000
block = 6                              # 整月块长,保留自相关/截面相关
ic_boot = np.empty(B)
for b in range(B):
    starts = rng.integers(0, T - block + 1, size=int(np.ceil(T / block)))
    idx = np.r_[tuple(np.arange(s, s + block) for s in starts)] % T   # 环绕拼接
    idx = idx[:T]
    ic_boot[b] = rank_ic(z[idx], y[idx])   # 整块重抽后重算 IC

ci_lo, ci_hi = np.percentile(ic_boot, [2.5, 97.5])   # 95% percentile 区间
se_boot = ic_boot.std(ddof=1)
t_stat = ic_obs / se_boot
p_val = 2 * (1 - stats.t.cdf(abs(t_stat), df=T - 1))
python

要点:每次重抽不是随机挑 120 个独立月份,而是随机挑若干连续的 6 个月块拼成新的 120 个月序列(超出末尾就环绕)。块内月份保持原顺序,于是自相关和截面相关都被保留。重抽 B=2000 次得到 IC 的经验分布,直接取 2.5%/97.5% 分位作为 95% 置信区间——不依赖任何正态假设

跑出来的结果:

观测 Rank-IC0.0935
bootstrap 均值 / SE0.0933 / 0.0057
95% 置信区间[0.0823, 0.1045]
t-stat / p-value16.38 / ≈0

关键结论:置信区间整个落在正半轴、远离 0,t-stat=16.4、p≈0——这个 IC=0.06 的弱因子在 T=120 月的长度下,被稳健判为显著。注意:区间下界 0.082 仍远大于 0,说明即便取最保守的 bootstrap 情形,因子也还有正预测力。

四、IC 的经验分布长什么样#

block bootstrap 给出的 Rank-IC 经验分布与 95% 置信区间

直方图是 2000 次 block bootstrap 重抽的 IC 分布。红线是观测 IC=0.093,两条绿虚线是 95% 置信区间上下界,灰点线是 IC=0(无预测力的零假设)。分布明显右偏、整体远离 0,这正是”真因子”该有的 bootstrap 画像。对照一下:如果这是纯噪声因子,分布会以 0 为中心、置信区间横跨正负——一眼就能判死。

五、B 取多大才够稳#

CI 半宽随 B 收敛;B≥2000 后基本稳定

置信区间的半宽(上限减下限除以 2)随 bootstrap 次数 B 收敛。本例里 B=1000 到 B=3000,半宽在一个很窄的带里波动,B≥2000 后基本稳定。实务建议:B 取 1000–2000 通常够用;若要做分位精确(比如金融里关心的 99% 极值区),可提到 5000。再多主要是算力浪费。

六、功率视角:多弱的因子能被检出来#

样本足够长时,一个真实 IC=0.06 的弱因子也能被稳健判显著

这张图回答一个更本质的问题:给定样本长度 T=120,最小能检出多大(绝对值)的真实 IC?用 bootstrap 的 SE 反推单样本 t 检验的临界:

minIC显著=t0.975,T1×SEboot\min |IC|_{\text{显著}} = t_{0.975,\,T-1} \times SE_{\text{boot}}

本例 SEboot=0.0057SE_{\text{boot}}=0.0057,临界约 0.0106——意思是真实 |IC| 超过 ≈0.01 就可能被判显著。这条曲线把”样本量 → 可检最小因子强度”讲清楚了:样本越短,你能检出的因子必须越强;样本够长,一个真实的 0.06 弱因子也站得住脚。

七、工程落地要点(诚实讲清边界)#

  1. 块长 block 怎么选。太短(block=1)退化成普通 bootstrap,破坏自相关;太长(block=T)几乎不重抽、区间收敛不到真实方差。经验上取能覆盖”因子衰减周期”的长度——月度因子 block=3~12 月常见。若不确定,做一个块长敏感性扫描(3/6/12),看 CI 是否稳定。

  2. 为什么不用 t 检验直接算? 你可以用,但要先确认 IC 的月度序列近似正态、独立。block bootstrap 的优势是不假设分布、保留相关结构,对尖峰厚尾、_cluster 相关的金融数据更稳。两者结果差很多时,以 block bootstrap 为准。

  3. 截面相关让”N”失效。上面合成数据里 N=200,但真实有效样本远小于 200(因为每天共享市场冲击)。block bootstrap 重抽”整月块”已经隐含处理了截面相关——它没有假装 200 只是独立样本。这是它比”逐股票 bootstrap”更对的地方。

  4. CI 显著 ≠ 因子能赚钱。(铁律提醒)置信区间只回答”IC 是否显著不为 0”,不回答”扣除成本后是否正期望”。一个 IC 显著但极弱(比如 0.01)的因子,交易成本一吃可能变负。CI 是必要非充分条件——先过显著性,再过成本/容量/衰减。

  5. 多重检验别忘了。如果你同时检验 50 个因子,按 5% 水准平均会误判 2.5 个显著。多因子扫描要做 BH/FDR 校正(本专栏另有专文),否则”显著因子”里混着噪声。

八、小结#

因子回测里,“IC=0.05”这句话没有置信区间就等于没说。Block bootstrap 用按月整块重抽保住金融数据特有的自相关和截面相关,给你的 Rank-IC 算出一个有统计意义的上下界,顺带给出 t-stat 和 p-value。本文合成实验里真实 IC=0.06 的弱因子,在 120 月样本上得到 95% CI=[0.082, 0.104]、t=16.4、p≈0,稳健判显著;而纯噪声因子会得到横跨 0 的 CI、一眼可辨。

记住分层:先过 block bootstrap 显著性(排除噪声)→ 再过成本/容量/衰减(排除亏钱)→ 再多因子场景做 FDR 校正(排除偶然命中)。置信区间是这道过滤器里最便宜、也最常被跳过的一道。

完整可复现代码(含三张图的生成)已随本文给出,固定 seed 即可重现全部数值。

Bootstrap IC 置信区间:给你的因子 Rank-IC 算出一个真显著的上下界
https://blog.halo26812.eu.org/blog/bootstrap-ic-confidence-interval
Author halo
Published at 2026年8月30日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨