「动量因子有效」——这句话每个做量化的人都听过。但它藏着一个几乎从不被追问的后半句:有效多久?
一个动量信号在生成的那一刻预测力最强,然后从第二天开始,它就像一块被端上桌的冰淇淋,每一分钟都在融化。你调仓越慢,吃到的是化了一半的糖水;你调仓越快,倒是能抢在它融化前多舀几口,可账单——交易成本——会把你多舀的那几口连本带利收回去。
这篇文章要做的,就是把这块「融化的冰」量化出来:用信息系数(IC)的衰减曲线画出信号腐烂的速度,再把它翻译成实盘最扎心的决策问题——到底该多久调一次仓?
一、动量为什么会「腐烂」#
先说清楚一件事:动量因子的衰减不是 bug,是它的本质。
横截面动量的经济逻辑,通常归因于投资者对信息的反应不足(underreaction)——好消息出来,价格没有一次性调整到位,而是分几天慢慢爬。动量策略赚的就是这个「慢慢爬」的过程。但这个过程有终点:一旦价格调整完毕,或者反应过度(overreaction)开始反转,信号就失效了。
所以任何动量信号都有一个有效期。用统计语言说,就是「形成期信号」与「未来第 k 天收益」的相关性,会随 k 增大而衰减到零,甚至转负(动量反转)。
我们用一个自洽的面板模拟来还原这件事:每只股票有一个缓慢均值回复的预期收益漂移 (服从强持续 AR(1) 过程),日收益等于这个漂移加上因子共同扰动再加上大量特异噪声。 的持续性决定了动量能撑多久,而噪声的主导地位保证了信号微弱到接近真实量级:
import numpy as np
import pandas as pd
rng = np.random.default_rng(20260715)
def simulate_panel(n_days=1500, n_stocks=300):
"""预期收益漂移 mu_t 服从强持续 AR(1),phi 越接近 1 → 动量腐烂越慢。"""
phi, sigma_mu, idio = 0.965, 0.0011, 0.016
n_factors, fac_load, fac_vol = 3, 0.012, 0.012
innov = np.sqrt(1 - phi ** 2) * sigma_mu
mu = rng.standard_normal(n_stocks) * sigma_mu
B = rng.standard_normal((n_stocks, n_factors)) * fac_load # 因子暴露
rets = np.zeros((n_days, n_stocks))
for t in range(n_days):
mu = phi * mu + innov * rng.standard_normal(n_stocks) # 动量漂移
f = rng.standard_normal(n_factors) * fac_vol # 当日因子收益
rets[t] = mu + B @ f + rng.standard_normal(n_stocks) * idio
return retspython这里的关键参数是 phi=0.965:它让预期收益的自相关每天只衰减约 3.5%,对应一个「几周量级」的动量记忆——既不会瞬间消失,也不会永久有效。
二、画出衰减曲线:边际 IC#
衡量信号预测力的标准工具是信息系数(Information Coefficient, IC)——因子值与未来收益的横截面秩相关。要看「衰减」,我们不能只算一个 IC,而要算一整条曲线:形成期信号 vs 未来第 k 天的当日收益,让 k 从 1 走到 40。
这里有一个极其常见、极其致命的坑:很多人用「信号 vs 未来 k 天累计收益」画衰减曲线,结果画出一条上升曲线,然后欢天喜地地得出「持有越久越好」的错误结论。
原因很简单:累计收益是把 k 天的预测力加总,只要每天还剩一点点正的边际预测力,累计值就会一直涨。它掩盖了「每天的边际贡献其实在快速缩小」这个真相。要看清腐烂,必须看边际 IC——信号对未来第 k 天单日收益的预测力:
def rank_ic(a, b):
"""Spearman 秩相关。"""
ar, br = pd.Series(a).rank().values, pd.Series(b).rank().values
if np.std(ar) == 0 or np.std(br) == 0:
return 0.0
return np.corrcoef(ar, br)[0, 1]
rets = simulate_panel()
T, N = rets.shape
form = 60 # 形成期:过去 60 日累计收益作为动量信号
momentum = pd.DataFrame(rets).rolling(form).sum().shift(1).values
ic_by_h = []
for k in range(1, 41):
fwd = pd.DataFrame(rets).shift(-k).values # 未来第 k 天的当日收益(非累计!)
ics = []
for t in range(form + 5, T - k - 1, 3):
rm, rf = momentum[t], fwd[t]
mask = ~(np.isnan(rm) | np.isnan(rf))
if mask.sum() > 30:
ics.append(rank_ic(rm[mask], rf[mask]))
ic_by_h.append(np.nanmean(ics))python画出来是这样:

这条曲线就是动量因子的「保质期证明」。模拟里边际 IC 从第 1 天的约 0.012 出发,波动着往下掉,到第 40 天左右已经贴近零。IC 减半的位置大致落在中段——这意味着你持有信号越久,每多持一天,它替你做出的正确判断就越少。
注意这里的 IC 量级只有 0.01 出头——这才是真实世界的样子。教科书里动辄 0.05、0.1 的 IC,要么是样本内过拟合,要么是没扣成本的理论值。微弱但持续的边际预测力,加上足够多的标的分散,才是量化 alpha 的真实形态。
三、从 IC 到实盘:调仓频率的两难#
衰减曲线告诉我们信号会腐烂,但它没直接回答那个真金白银的问题:该多久调一次仓?
逻辑上有两股相反的力:
- 信号侧偏向高频:既然信号每天都在腐烂,那当然应该尽快按最新信号调仓,趁 alpha 还新鲜赶紧吃。持有期越短 → 平均信号越「新」 → 毛收益(gross)越高。
- 成本侧偏向低频:每次调仓都要付出买卖价差、冲击成本、佣金。持有期越短 → 换手率越高 → 成本吞噬越狠。
这两股力的交点,就是最优调仓频率。我们用一个多空组合把它跑出来:每 hold 天调仓一次,做多动量最强的 20%、做空最弱的 20%,等权持有,并对每次换手扣单边 10bps 的成本:
def long_short(hold, cost_bps=0.0):
"""每 hold 天调仓,多 top20% 空 bottom20%,等权。扣单边 cost_bps 成本。"""
port_ret = np.zeros(T)
pos = np.zeros(N)
for t in range(form + 1, T):
if (t - form - 1) % hold == 0: # 调仓日
sig = momentum[t]
valid = ~np.isnan(sig)
new_pos = np.zeros(N)
if valid.sum() > 20:
idx = np.where(valid)[0]
ranks = pd.Series(sig[valid]).rank(pct=True).values
longs, shorts = idx[ranks >= 0.8], idx[ranks <= 0.2]
if len(longs) and len(shorts):
new_pos[longs] = 1.0 / len(longs)
new_pos[shorts] = -1.0 / len(shorts)
turnover = np.sum(np.abs(new_pos - pos)) # 双边换手
port_ret[t] -= turnover * cost_bps / 1e4 # 成本在调仓日一次性扣除
pos = new_pos
port_ret[t] += np.nansum(pos * rets[t]) # 当日持仓收益
return port_ret[form + 1:]python把持有期从 1 天扫到 40 天,分别算毛夏普与净夏普:

结果非常有教育意义:
| 持有期(天) | 毛夏普 | 净夏普(10bps) | 年化换手 | 含成本净收益 |
|---|---|---|---|---|
| 1 | 3.77 | 1.62 | 97.7× | 7.4% |
| 2 | 4.01 | 2.48 | 68.4× | 11.3% |
| 5 | 3.66 | 2.67 | 43.7× | 12.2% |
| 10 | 3.74 | 3.03 | 30.8× | 13.5% |
| 20 | 3.06 | 2.55 | 22.2× | 11.6% |
| 40 | 1.72 | 1.36 | 16.1× | 6.2% |
毛夏普在 2 天持有期最高(4.01),但净夏普的峰值却挪到了 10 天(3.03)。 换句话说,如果你只看不扣成本的回测,你会得出「越短越好」的结论,然后满怀信心地上线一个每天调仓、年化换手近 100 倍的策略——上线后被交易成本活活拖死。
四、成本侵蚀曲线:换手率是隐形杀手#
把「年化换手率」和「含成本净收益」画成一条曲线,能更直观地看到成本是怎么吃掉 alpha 的:

曲线呈明显的倒 U 形:换手太低(持有期太长),吃到的信号太陈旧,收益上不去;换手太高(持有期太短),成本吞噬超过了信号新鲜度带来的增益,净收益反而跌下来。峰顶落在中等换手区间。
再看不同持有期的净值曲线,1 天持有期那条线明显最疲弱——它的毛收益其实不差,全被成本磨掉了:

这条倒 U 形曲线的位置极度依赖你的成本假设。单边 10bps 对应一个流动性尚可的中大盘 A 股组合;如果你做的是小微盘、或者资金体量大到有显著冲击成本,成本可能是 30bps 甚至更高,那么整条曲线会向右下方塌陷,最优持有期会进一步右移(被迫拉长),甚至可能让整个策略在扣成本后无利可图。
五、对抗衰减:三条可落地的思路#
既然信号会腐烂、高频又被成本吃掉,怎么办?三个方向:
1. 信号平滑,而非频繁重构。 与其每天推翻重来,不如对信号做指数加权平滑,让持仓「缓慢漂移」到新目标而不是跳变。这能在几乎不损失信号新鲜度的前提下大幅压低换手——本质是在换手和信号之间做一次凸优化。
2. 换手带(no-trade band)。 只有当某只股票的目标权重与当前权重偏离超过一个阈值时才交易。小的信号变化不值得为它付成本,直接忽略。这是实盘组合管理里最朴素也最有效的降本手段之一。
3. 成本感知的组合优化。 把交易成本显式写进目标函数:最大化 预期收益 − λ·风险 − 成本。这样优化器会自动在「追新信号」和「省成本」之间找平衡,而不是靠人拍脑袋定一个固定持有期。
这三条的共同哲学是:不要和衰减硬刚,要和它共处。 承认信号会腐烂,然后把「腐烂速度」和「交易成本」放进同一个决策框架里权衡。
六、实现细节与落地关卡#
A. 实现细节
- 信号构造:形成期用过去 60 日累计收益(经典 12-1 月动量的日频缩影),并
shift(1)严格滞后,杜绝用当日信息交易当日。 - 执行假设:调仓在信号日按当日收益结算,成本在调仓日一次性按双边换手扣除(单边 10bps)。真实实盘应改为信号日次日开盘执行,避免用收盘信号成交收盘价的隐性前视。
- 多空构造:top/bottom 20% 等权,市值中性但未做行业中性;净敞口约为零(多空各半)。
- IC 口径:用边际 IC(vs 未来单日收益)而非累计 IC,这是本文方法论的核心,避免「越长越好」的假象。
B. 已知偏差
- 合成数据的自洽性:图表数据来自自洽模拟——动量的持续性与衰减速度由 AR(1) 的
phi显式设定,因此衰减曲线的形状是被数据生成过程「安排」的。真实市场的衰减往往更不规则,还可能在尾部转负(动量反转),本模拟未刻画反转段。 - 成本模型过简:单边固定 10bps 是线性成本,忽略了冲击成本随交易量非线性上升、以及不同流动性股票成本差异巨大的现实。大资金体量下真实成本曲线会陡峭得多。
- 无容量约束:模拟假设可以无限扩大规模,实盘中动量策略的容量受限于被交易股票的日成交额,高频版本尤其吃紧。
- 单一成本情景:只测了 10bps 一种成本,没做成本敏感性扫描——而最优持有期对成本假设极度敏感。
C. 结果解读
- 衰减是真金白银的约束:边际 IC 从 0.012 掉到 0 不是学术曲线,它直接决定了你能持有信号多久还不亏。忽视衰减 = 用过期的判断做交易。
- 毛净夏普的分裂是核心教训:毛夏普峰值(2 天)和净夏普峰值(10 天)错开 8 天,这个缺口就是交易成本的「税率」。任何不扣成本的动量回测都系统性地高估了高频策略——看毛收益选调仓频率,等于让成本替你选,且选得很糟。
- 倒 U 形是普适结构:换手率-净收益的倒 U 形几乎出现在所有带成本的高换手策略里,不止动量。找到那个峰顶,比把毛收益再榨高 10% 重要得多。
- 适用边界:本文结论对「流动性好、成本可控」的组合成立;对小微盘、高冲击成本、大资金体量的场景,最优持有期会被迫大幅拉长,甚至可能让策略整体失去经济意义。上线前务必用自己真实的成本曲线重跑这条倒 U 形。
动量因子最容易骗人的地方,不在于它是否有效,而在于它有效的时长被大多数回测悄悄忽略了。当你把边际 IC 的衰减曲线和交易成本的倒 U 形放在一起看,那个「该多久调一次仓」的问题就不再是玄学——它是两条曲线的交点,是新鲜度与成本之间一个冷冰冰的最优解。承认信号会腐烂,你才配拿到它腐烂前那点稍纵即逝的 alpha。