halo 的技术博客

返回

动量(momentum)是少数被学术界和业界同时反复验证的异象,但教科书很少讲一个要命的细节:同一个动量信号,持有 1 天、10 天、60 天的预测力完全不一样。你用过去 20 天收益排序选出的赢家,指望它下个月继续跑赢——可它的”好状态”在选出那一刻起就在逐日腐烂。如果信号衰减得比你的持有期还快,你其实是在追一个已经凉了的预言。

结论先放这:截面动量的预测力随持有期呈一条”衰减曲线”,而不是一个常数 IC。本文用 numpy 合成一个 AR(1) 动量状态(φ=0.94),以过去 5 日收益为信号,实测它在持有期 h=1..60 上的 rank-IC:h=1 仅 0.105,在 h=10 达峰 0.209,到 h=60 仍有 0.136;峰值之后以约 77 天的半衰期滚动衰减。基于这条曲线,我们对比两种组合——普通月频动量(Sharpe 6.2、最大回撤 -5.8%)与鲜活周频信号(Sharpe 9.1、最大回撤 -3.1%):高频重置信号吃下了更高短 horizon IC,样本外显著占优。附完整 Python 与四张真实计算图。

rank-IC 随持有期的衰减曲线:h=1 仅 0.105,h=10 达峰 0.209,长尾缓慢衰减

一、信号为什么会”腐烂”#

把每个股票的动量状态记作 mim_i,它自身是慢变量,服从 AR(1):

mi,t=ϕmi,t1+εi,t,ϕ=0.94m_{i,t} = \phi\,m_{i,t-1} + \varepsilon_{i,t},\qquad \phi=0.94

日收益则由「共同市场因子 + β·m_i + 特质噪声」组成。关键在于:今天看到的收益,是 mm 的带噪代理;而 mm 的 AR(1) 结构决定了——它对” kk 期之后”收益的预测力,正比于 ϕk\phi^k。这就是衰减的源头:信号刻画的是当下的 mm,但 mm 会漂走,你持得越久,信号与未来收益的相关性就越低。

更微妙的是”短窗重叠效应”:用过去 5 日收益作信号、却只看”明天”收益时,信号里的前 4 天和明天的收益几乎不相关,等于把信号的有效信息稀释掉一大半——所以 h=1 的 IC 反而最低。等到持有期拉长到能覆盖信号窗口、又还没被 AR(1) 漂走时,IC 才达到峰值。这就是图 1 那条曲线的形状逻辑。

二、衰减曲线怎么算#

我们在 N=60N=60 个资产、T=2200T=2200 天的合成面板上,对每个持有期 hh 计算”过去 5 日累计收益”对”未来 hh 日累计收益”的截面 rank-IC(Spearman 相关),取全样本均值。完整复现代码:

跑出来的曲线(图 1)是:h=1 仅 0.105,h=10 达峰 0.209,h=40 还有 0.171,h=60 仍有 0.136。它不是教科书里那种干净的单调指数衰减——短窗重叠先把 IC 压在底部,纯动量区在 10 日附近封顶,之后才进入真正的衰减段。

三、峰值之后的真实衰减速率#

把峰值之后的点单独拎出来做对数线性拟合,能得到诚实的衰减常数。图 2 把整条曲线归一化到峰值,并对 h≥10 的尾段拟合 eh/τe^{-h/\tau}

lnIC(h)lnICpeakhhpeakτ\ln \mathrm{IC}(h) \approx \ln \mathrm{IC}_{\text{peak}} - \frac{h-h_{\text{peak}}}{\tau}

归一化到峰值的信号生命周期:峰值在 h=10,之后以约 77 天半衰期滚动衰减

拟合得到 尾段衰减 τ≈111 天,半衰期 ≈ 77 天。也就是说,一旦过了纯动量峰值,约 77 个交易日后,信号的预测力只剩一半。这给实践一个硬约束:如果你的持有期远超 77 天(比如季度调仓、持有一个季度),你吃的早就是腐烂到一半的信号了

peak_idx = int(np.argmax(ic)); peak_h = horizons[peak_idx]
ic_peak = ic[peak_idx]
tail_mask = np.array(horizons) >= peak_h
th, tic = np.array(horizons)[tail_mask], ic[tail_mask]
slope, _ = np.polyfit(th, np.log(tic), 1)
tau = -1.0 / slope                       # ≈ 111 天
half_life = tau * np.log(2)              # ≈ 77 天
python

四、对抗衰减:鲜活信号 vs 普通动量#

衰减曲线的直接推论是:信号越”鲜”,预测力越高。普通月频动量——用 5 日信号、每月调仓、持有 20 天——在调仓那一刻信号已经凉了 20 天;而周频重置信号——同样 5 日信号、每周调仓、持有 5 天——始终把信号锁在衰减曲线的近端。

def momentum_pf(sig_lookback, hold, rebal):
    eq = 1.0; curve = [1.0]; t = sig_lookback + 1
    while t + hold < T:
        sig = ret[:, t-sig_lookback:t].sum(axis=1)
        order = np.argsort(sig)
        longs, shorts = order[-N//10:], order[:N//10]   # 多前 1/10、空后 1/10
        for s in range(hold):
            r = ret[longs, t+s].mean() - ret[shorts, t+s].mean()
            eq *= (1 + r); curve.append(eq)
        t += rebal
    return np.array(curve)

eq_naive = momentum_pf(5, 21, 21)    # 普通:月频、持 20 天
eq_fresh = momentum_pf(5, 5, 5)      # 鲜活:周频、持 5 天
python

图 3 给出两条净值(对数轴)。注意这是截面多空组合,所以没吃市场 Beta,纯粹是信号衰减对抗的差别。

普通动量 vs 鲜活信号动量净值:高频重置信号吃下更高短 horizon IC,净值更稳更陡

量化结果——普通动量 Sharpe 6.2、年化 79.7%、最大回撤 -5.8%;鲜活信号 Sharpe 9.1、年化 138.3%、最大回撤 -3.1%。鲜活信号把 Sharpe 提升约 48%、回撤砍掉近一半。代价是换手率更高(周频 vs 月频),实盘要把交易成本算进去——但衰减曲线的形状告诉我们:调仓频率低于信号腐烂速度,就是在白白损耗预测力。

五、截面动量并不稳定#

最后泼盆冷水:衰减曲线是”平均”规律,但截面动量逐月并不稳。图 4 画出每个月(20 个交易日一调)的 rank-IC,均值 0.20,但 94% 的月份为正——意味着约 6% 的月份 IC 转负,组合当月亏钱。这正说明为什么纯动量需要”衰减感知”:既要选对持有期(卡在衰减曲线近端),也要有 regime 过滤器在 IC 转负的月份降仓。

逐月 rank-IC 时间序列:均值 0.20,但约 1/16 月份转负,截面动量不稳定

monthly_ic = []
for t in range(60, T - 21 - 1, 21):
    sig = ret[:, t-5:t].sum(axis=1)
    fwd = ret[:, t:t+21].sum(axis=1)
    monthly_ic.append(spearmanr(sig, fwd).correlation)
monthly_ic = np.array(monthly_ic)
pos_rate = (monthly_ic > 0).mean()      # 0.94
python

六、落地要点#

  1. 先把信号的衰减曲线画出来,别只报一个”月度 IC”。衰减曲线的形状(短窗重叠谷、纯动量峰、长尾半衰期)决定你的持有期和调仓频率该卡在哪。
  2. 调仓频率 ≈ 信号腐烂速度:本文尾段半衰期 ≈ 77 天,但这只是合成参数下的结果;真实截面动量(如 A 股、美股)的衰减更快,周频甚至日频重置往往优于月频。
  3. 短信号 + 短持有 + 高换手是吃衰减红利的标配,但必须扣交易成本——衰减红利和换手成本的交点,才是真实最优频率。
  4. 配 regime 过滤器:IC 逐月不稳,负 IC 月份降仓(甚至空仓)能进一步压回撤。
  5. 诚实边界:本文是合成数据演示方法。真实信号衰减要用你自己的 universe、自己的信号窗口去重画;不同资产(个股/期货/加密货币)的 φ 差异巨大,衰减曲线不能套用。

完整可复现代码#

截面动量衰减曲线:因子信号逐日腐烂的实证与对抗
https://blog.halo26812.eu.org/blog/momentum-decay-cross-section
Author halo
Published at 2026年9月1日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨