3步算出因子IC半衰期:用 gs-quant 拟合衰减曲线
【免费下载链接】gs-quantPython toolkit for quantitative finance项目地址: https://gitcode.com/GitHub_Trending/gs/gs-quant
某动量因子回测曲线漂亮,三个月后IC悄悄腰斩——这是量化团队最常遇到的"因子有效性"塌方。与其反复换因子,不如先用 gs-quant 把它的 IC 半衰期算出来,定位衰减到底快还是慢。
IC 是信息系数(Information Coefficient),衡量当期因子暴露与下期收益的相关强度。它不是静态数字,而是一条会随时间走低的时间序列。把这条序列拟合到指数上,就能得到一个有业务含义的参数:因子信号"半衰"到一半所需的时间,也就是 IC 半衰期。
为什么值得盯住IC衰减
把因子想成一管药。药效打进去后不会永远维持峰值,而是按时间一点点消退。IC 也是同理:刚计算出来的那一期信息系数最高,往后每一期都在衰减。这种衰减在多数因子上接近指数形式,可以写成 $IC(t)=IC_0,e^{-\lambda t}$,其中 $IC_0$ 是起始强度,$\lambda$ 是衰减速度。$\lambda$ 越大,曲线掉得越快。
把"半衰"这件事翻译成参数:令 $IC(t)=IC_0/2$,解出来就是 $t_{1/2}=\ln 2/\lambda$。这个 $t_{1/2}$ 直接把"因子还有多有用"变成了一个可比较的数字——它告诉你,信号在多久之后只剩下原来一半,也就决定了你多久必须重新调仓。
盯住IC衰减的价值在于:它把"这个因子还灵不灵"从主观判断变成了一条可回测的曲线。曲线掉得快,说明信号被市场消化得很快,拖得越久越接近噪声;曲线掉得慢,说明信息还在缓慢释放,低频调仓也不亏。
从取数到半衰期的三步落地
取数与IC序列
先取因子收益和资产收益两条时间序列,再用滚动相关算出逐期的IC。gs-quant 的 gs_quant/models/risk_model.py 里FactorRiskModel负责取数,gs_quant/timeseries/econometrics.py 的correlation负责滚动相关,窗口大小用参数w控制,缺值自动处理。
from datetime import date import numpy as np from gs_quant.models.risk_model import FactorRiskModel from gs_quant.timeseries import correlation model = FactorRiskModel.get("MODEL_ID") # 换成你的模型ID fret = model.get_factor_returns_by_name( # 因子收益序列 start_date=date(2023, 1, 1), end_date=date(2023, 12, 31), factors=["MOMENTUM"]) ar = model.get_daily_return( # 资产日收益 start_date=date(2023, 1, 1), end_date=date(2023, 12, 31)) # 取单只资产/组合的"下一期"收益,与当期因子收益做滚动相关 ic = correlation(fret, ar["GS US 005"].shift(-1), w=20).dropna()跑完你会得到一条带日期索引的ic序列:横轴是时间,纵轴是每期的信息系数,这正是拟合衰减的原料。
指数拟合求λ
原始IC噪声大、且会在零附近来回跳,直接对整条曲线拟指数经常失败。稳妥做法是先滚动平滑、只取正半轴那段再拟合,$IC(t)=IC_0,e^{-\lambda t}$ 在这里用scipy.optimize.curve_fit求 $\lambda$。
from scipy.optimize import curve_fit import numpy as np ic_s = ic.rolling(10).mean().dropna() # 平滑噪声 ic_s = ic_s[ic_s > 0] # 只拟合衰减(正值)段 def decay(t, ic0, lam): return ic0 * np.exp(-lam * t) t = np.arange(len(ic_s)) popt, _ = curve_fit(decay, t, ic_s.values, p0=[abs(ic_s.iloc[0]), 0.05], maxfev=20000) half_life = np.log(2) / popt[1] # 半衰期 = ln2 / λ print(f"λ={popt[1]:.4f}, 半衰期≈{half_life:.1f} 期")结果是一个具体的期数,比如"半衰期≈24期",含义是因子信号大约一个月后只剩一半。
画图验证
拟合完务必画图确认曲线形状确实向下弯、而不是拟合到了噪声上。下面把原始IC、平滑IC和拟合曲线叠在一起,一眼就能判断衰减是否成立。
import matplotlib.pyplot as plt plt.figure(figsize=(10, 6)) plt.plot(ic.index, ic, alpha=0.5, label="滚动 IC") plt.plot(ic_s.index, ic_s, label="平滑 IC") plt.plot(t, decay(t, *popt), "r--", label=f"拟合曲线 (半衰期≈{half_life:.1f})") plt.xlabel("日期"); plt.ylabel("IC 值") plt.legend(); plt.title("因子 IC 衰减与半衰期") plt.show()如果拟合曲线明显贴着平滑IC的下降段,说明指数假设成立;如果拟合线飘到数据外面,多半是这段IC根本不在衰减,该重新切样本。
📉 半衰期数字怎么读
算出来的 $t_{1/2}$ 要落到调仓动作上才有意义。经验对应关系大致如下:
| 半衰期区间 | 建议调仓节奏 |
|---|---|
| < 5 期(约一周) | 高频,信号消耗快,交易成本敏感 |
| 5–20 期(一两周到一个月) | 月度或双周调仓 |
| 20–60 期(一个月到季度) | 季度或低频再平衡 |
| > 60 期 | 低频即可,重点看稳定性而非频率 |
两个容易踩的坑:一是IC窗口太短,噪声盖过真信号,半衰期会被随机拉得忽长忽短;建议先在 20–60 期之间对照着试。二是别只看单段,因子有效性会随行情切换,gs_quant/models/risk_model_utils.py 里的工具函数可以在拟合前后对因子序列做稳定性校验,确认衰减不是某段行情的偶发。
从单因子到组合的延伸
单因子算完 $t_{1/2}$ 只是起点。真正上线时,多因子之间的相关与半衰期差异,可以直接喂给 gs_quant/markets/ 里的组合优化模块,让每个因子的权重随其信号半衰期动态衰减,把调仓频率和成本一并纳入约束。
IC 半衰期把"因子还灵不灵"变成了一条可回测的曲线和一个可调仓的数字。拿你手里因子的近一年数据跑一遍,看看衰减是快是慢,再决定是加仓还是收手。
【免费下载链接】gs-quantPython toolkit for quantitative finance项目地址: https://gitcode.com/GitHub_Trending/gs/gs-quant
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考