Skip to content

第二十二章 因子衰减与半衰期

做因子挖掘,最怕什么?

不是因子没效果,而是效果突然消失了。

我见过太多人,辛辛苦苦挖出一个 IC 高达0.08的因子,兴奋得不行。结果实盘三个月,收益曲线一路向下。为什么?因为因子在衰减,而他们根本没意识到。

这一章,我们就来聊聊因子衰减这件事。说白了,就是你的因子能活多久,以及怎么判断它快不行了。

因子衰减速度的计算

因子衰减,指的是因子预测能力随时间下降的现象。我个人习惯用两种方式衡量衰减速度:

  1. IC 衰减曲线:计算每个时间窗口的 IC 值,看它随时间如何变化
  2. 收益衰减率:因子分组收益的差值随时间缩小的速度

先看代码实现:

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% 的数据做验证。
  • 忽略因子之间的相关性。两个半衰期都很长的因子,如果高度相关,其实等于一个因子。
  • 更新频率过高。有些因子需要一定的时间积累才能稳定,频繁更新反而引入噪声。

最后说一句:因子衰减是常态,不是异常。你想想看,如果所有因子都不衰减,那市场早就被量化基金吃干抹净了。关键是要建立一套监测和应对机制,让因子在衰减之前,你已经准备好了替代方案。

嗯,这就是因子衰减与半衰期的全部内容。记住:挖因子容易,管因子难。能管理好因子衰减的人,才是真正的量化高手。