很多日内策略研究员都有过这种体验:回测里因子「看起来很能打」,实盘却一上就软。问题往往出在两个被忽视的维度——因子的预测力在一天之内是衰减的,以及你用来证明它显著的统计检验,本身就被多重比较污染了。
本文用一套可复现的模拟,把这两件事讲清楚。
一、日内因子为什么会「过期」#
一个典型的高频 / 日内因子(如开盘前 30 分钟的订单流不平衡、隔夜跳空反转)预测的是随后一段时间的收益。它的信息来源有三道衰减机制:
- 信息被快速套利:一旦信号被足够多的参与者看到并执行,超额收益就被抹平。
- 微观结构噪声累积:越靠近收盘,买卖价差、收盘集合竞价的扰动越大,信号信噪比下降。
- 样本截面异质性漂移:不同时段参与交易的资金性质不同(开盘机构、午间散户、尾盘调仓),因子生效的子群体在变。
合起来的结果:因子在开盘前后 IC 最高,午后显著下滑。我们用模拟把这条曲线画出来。
二、模拟一个日内衰减因子#
设定 12 个半小时时段(09:30 → 16:00),真实 IC 随时段指数衰减:IC(b) = IC0 · exp(−b/τ),其中 IC0=0.075、τ=4.5。每个 (交易日, 时段) 取 600 只股票的横截面,由信号与噪声合成收益,再用 250 个交易日平均得到稳健的时段 IC 估计。
import numpy as np
from scipy.optimize import curve_fit
from scipy import stats
rng = np.random.default_rng(20260711)
B, N, D_days = 12, 600, 250
IC0, tau = 0.075, 4.5
true_ic = IC0 * np.exp(-np.arange(B) / tau)
ic = np.zeros((D_days, B))
for d in range(D_days):
for b in range(B):
z = rng.normal(0, 1, N)
rho = true_ic[b]
r = rho * z + np.sqrt(max(1e-6, 1 - rho*rho)) * rng.normal(0, 1, N)
ic[d, b] = np.corrcoef(z, r)[0, 1] # 该日的横截面 IC
mean_ic = ic.mean(0)
se_ic = ic.std(0) / np.sqrt(D_days) # 标准误
tstat = mean_ic / se_icpython跑出来的平均 IC 从开盘的 7.5% 一路滑到午后的 0.5%~0.6%——衰减不是线性的,而是指数式的:

误差棒(±1.96 SE)显示:早盘几个时段的 IC 远高于 0,而午后已基本落入噪声区间。
三、拟合半衰期:信号能「撑」几个时段#
把衰减曲线用指数拟合,半衰期 τ·ln2 直接告诉你因子的「有效期」:
def exp_decay(b, a, t):
return a * np.exp(-b / t)
popt, _ = curve_fit(exp_decay, np.arange(B), mean_ic, p0=[IC0, tau], maxfev=20000)
half_life = popt[1] * np.log(2)python
拟合得到半衰期约 3 个时段(约 1.5 小时)。这意味着:如果你在开盘算出因子、打算持有到尾盘,实际吃到的是一段被严重稀释的暴露——因子该在衰减掉一半之前就平仓或再平衡。这是高频因子和日频因子在持仓周期设计上最根本的差异。
四、统计显著性:t 统计量而非「看起来大于 0」#
「IC 是正的」不等于「显著大于 0」。正确的判据是 t 统计量 t = 均值IC / 标准误,在正态近似下与 0 的偏离是否超过临界值。单检验 95% 临界值 |t| ≈ 1.96;但由于你看了 12 个时段,应当用 Bonferroni 校正:|t| > Φ⁻¹(1 − 0.05/2B) ≈ 2.87。
t95 = stats.norm.ppf(0.975) # 1.96
t_bonf = stats.norm.ppf(1 - 0.05 / (2*B)) # 2.87python
图上绿柱是通过 Bonferroni 的时段——基本集中在开盘前 4 个时段。午后那些「IC 还是正的」时段,t 统计量早已跌破阈值,纯粹是噪声。一个只报告「因子 IC 为正」的研究,等于把噪声当信号。
实务提示:横截面 IC 在有限样本下并非严格正态,更稳健的做法是用 Newey-West 调整标准误(处理时序相关)或 bootstrap(对交易日重采样)得到经验 p 值,结论方向不变但更抗造。
五、多重检验陷阱:挑「最好时段」报告必然翻车#
最隐蔽的坑在这里:研究员往往扫完 12 个时段,挑 IC 最高的那个写进报告。但即使因子完全无效(真实 IC=0),扫 12 遍也大概率能撞出一个「显著的」时段。我们模拟 200 个纯噪声零因子,看它们「最优时段」的 |t| 分布:
K = 200
best_t = np.zeros(K)
for k in range(K):
z = rng.normal(0, 1, (D_days, N))
r = rng.normal(0, 1, (D_days, N)) # 纯噪声,真实 IC = 0
ic_k = np.array([np.corrcoef(z[d], r[d])[0, 1] for d in range(D_days)])
se = ic_k.std() / np.sqrt(D_days)
best_t[k] = abs(ic_k.mean() / se) # 报告「最强时段」python
结果:200 个零因子里 16 个(8%)越过了单检验 95% 线,但只有 2 个(1%)越过 Bonferroni 线。换句话说,如果你不校正,每 12 个因子就会「凭空」冒出 1 个假显著——这正是文献里「因子动物园」泛滥的根源。落地时至少要做三选一:
- Bonferroni / Holm 步进法:保守但简单;
- 去膨胀夏普比率(Deflated Sharpe Ratio):用最大回撤与检验次数同时校正;
- 样本外 / Walk-forward:把「最好时段」放到没参与选型的样本里再验一次。
边界声明#
- 模拟不等于实盘:本文用合成数据演示机制,真实日内因子的衰减形状取决于该因子的信息源(订单流、舆情、隔夜跳空各异),务必用你自己的数据重画这张图。
- 时段边界有前视风险:用「收盘收益」做某时段的标签时,必须确保因子只用该时刻之前的信息,否则午后 IC 会被偷偷高估。
- 截面幸存者偏差:模拟里股票数固定,实盘中退市的票会让早段 IC 偏高,需做幸存者中性处理。
- 半衰期是点估计:用一个指数拟合得到的 τ 有估计误差,实战应给半衰期一个置信区间,而不是当成精确刻度。
- 显著性 ≠ 经济显著:即使午后 IC 在统计上「显著」,其量级可能小到被交易成本吃掉,仍需结合换手与手续费做净收益判断。
把「日内衰减曲线 + 半衰期 + 多重检验校正」三件套钉进你的因子评审流程,你筛掉的将不是「不显著的因子」,而是「看起来显著其实只是被多次比较挑中的噪声」。