第二十二章 因子衰减与半衰期
做因子挖掘,最怕什么?
不是因子没效果,而是效果突然消失了。
我见过太多人,辛辛苦苦挖出一个 IC 高达0.08的因子,兴奋得不行。结果实盘三个月,收益曲线一路向下。为什么?因为因子在衰减,而他们根本没意识到。
这一章,我们就来聊聊因子衰减这件事。说白了,就是你的因子能活多久,以及怎么判断它快不行了。
因子衰减速度的计算
因子衰减,指的是因子预测能力随时间下降的现象。我个人习惯用两种方式衡量衰减速度:
- IC 衰减曲线:计算每个时间窗口的 IC 值,看它随时间如何变化
- 收益衰减率:因子分组收益的差值随时间缩小的速度
先看代码实现:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import curve_fit
def calculate_factor_decay(factor_values, forward_returns, max_lag=20):
"""
计算因子衰减速度
factor_values: 因子值序列
forward_returns: 未来N期收益
max_lag: 最大滞后天数
"""
decay_ics = []
for lag in range(1, max_lag + 1):
# 计算滞后lag期的IC
ic = factor_values.rolling(20).corr(
forward_returns.shift(-lag)
).mean()
decay_ics.append(ic)
# 拟合衰减曲线
x = np.arange(1, max_lag + 1)
# 用指数衰减模型拟合
def exp_decay(t, a, b, c):
return a * np.exp(-b * t) + c
params, _ = curve_fit(exp_decay, x, decay_ics,
p0=[0.1, 0.1, 0.01])
return decay_ics, params
# 使用示例
decay_ics, (a, b, c) = calculate_factor_decay(
factor_series,
forward_returns_series
)
print(f"衰减系数 b = {b:.4f}")
print(f"半衰期 = {np.log(2)/b:.1f} 天")
这里的关键是衰减系数 b。b 越大,因子死得越快。我在项目中遇到过 b=0.3 的因子,两周就废了,根本没法用。
半衰期的定义与计算
半衰期这个概念,借用了物理学。它指的是因子预测能力衰减到初始值一半所需的时间。
计算公式很简单:
半衰期 = ln(2) / 衰减系数
举个例子:
| 衰减系数(b) | 半衰期(天) | 实际含义 |
|---|---|---|
| 0.05 | 13.9 | 约两周衰减一半 |
| 0.10 | 6.9 | 约一周衰减一半 |
| 0.20 | 3.5 | 三天半就废了一半 |
| 0.50 | 1.4 | 基本一天一换 |
实战经验:我个人认为,半衰期低于5天的因子,除非是做高频交易,否则很难在实际策略中稳定盈利。你想想看,刚挖出来还没用几天就失效了,这谁受得了?
不同频率因子的衰减特征
不同频率的因子,衰减特征完全不同。我整理了一个对比表:
| 因子频率 | 典型半衰期 | 衰减原因 | 应对策略 |
|---|---|---|---|
| 高频(分钟级) | 1-3 天 | 市场微观结构变化 | 实时更新,快速迭代 |
| 中频(日频) | 5-20 天 | 短期动量反转 | 周度重算,动态加权 |
| 低频(周/月频) | 30-90 天 | 基本面变化 | 月度更新,长期持有 |
| 超低频(季度) | 半年以上 | 宏观环境变迁 | 季度复核,谨慎调整 |
为什么会这样?说白了,频率越高的因子,越依赖市场短期的不平衡。这种不平衡很快就会被套利者抹平。而低频因子,比如基于财务报表的因子,变化就慢得多。
我曾经犯过一个错误:把一个日频因子直接用在分钟级策略上。结果呢?半衰期从10天直接掉到2天。嗯,那段时间回撤得我头皮发麻。
动态因子更新策略
既然因子会衰减,我们就得想办法让它"续命"。我常用的策略有四种:
1. 滚动窗口重算
定期用最新数据重新计算因子值。这是最基础的方法。
def rolling_factor_update(factor_func, data, window=60):
"""
滚动窗口更新因子
window: 训练窗口大小
"""
factor_values = []
for i in range(window, len(data)):
# 用最近window天的数据重新训练
train_data = data.iloc[i-window:i]
# 重新计算因子参数
new_factor = factor_func(train_data)
factor_values.append(new_factor)
return pd.Series(factor_values, index=data.index[window:])
2. 衰减加权平均
给不同时期的因子值赋予不同权重,越近的权重越大。
def decay_weighted_factor(factor_series, half_life=10):
"""
衰减加权平均
half_life: 半衰期参数
"""
# 计算权重
n = len(factor_series)
weights = np.exp(-np.log(2) * np.arange(n) / half_life)
weights = weights / weights.sum()
# 加权平均
weighted_factor = np.dot(factor_series, weights)
return weighted_factor
3. 自适应半衰期
根据市场状态动态调整半衰期。市场波动大时,缩短半衰期;市场平稳时,延长半衰期。
def adaptive_half_life(factor_series, market_volatility):
"""
自适应半衰期调整
"""
# 基础半衰期
base_half_life = 10
# 根据波动率调整
vol_ratio = market_volatility / market_volatility.mean()
# 波动率越高,半衰期越短
adjusted_half_life = base_half_life / (1 + vol_ratio * 0.5)
return max(adjusted_half_life, 3) # 不低于3天
4. 因子组合轮动
同时维护多个因子,根据它们的半衰期动态切换权重。
我的建议:不要把所有希望寄托在一个因子上。我一般同时维护 5-8 个因子,每个因子分配 10%-30% 的权重。哪个因子衰减了,就降低它的权重,把资金转移到其他因子上去。
避坑指南
我曾经踩过的坑:
- 过度拟合半衰期参数。用历史数据算出来的半衰期,未来不一定适用。我建议留出 20% 的数据做验证。
- 忽略因子之间的相关性。两个半衰期都很长的因子,如果高度相关,其实等于一个因子。
- 更新频率过高。有些因子需要一定的时间积累才能稳定,频繁更新反而引入噪声。
最后说一句:因子衰减是常态,不是异常。你想想看,如果所有因子都不衰减,那市场早就被量化基金吃干抹净了。关键是要建立一套监测和应对机制,让因子在衰减之前,你已经准备好了替代方案。
嗯,这就是因子衰减与半衰期的全部内容。记住:挖因子容易,管因子难。能管理好因子衰减的人,才是真正的量化高手。