量化策略"保鲜"全攻略:用 Python 检测因子衰减,动态调权收益提升 45%(完整代码)

5 阅读1分钟

代码声明: 本文所有代码仅供学习参考,不构成投资建议。量化交易有风险,实盘需谨慎。

为什么你的量化策略越跑越差?

2025 年,我帮一家私募搭建多因子选股模型,回测年化收益 32%,夏普比率 2.1,结果实盘 3 个月后收益骤降至 8%。排查发现:因子衰减在作祟。

数据显示,A 股市场单一因子的平均半衰期仅 3-6 个月,部分高频因子甚至只有 2 周。如果你还在用固定权重的多因子模型,收益下滑是必然的。

今天,我用完整代码演示:

  1. 如何检测因子是否衰减(IC 衰减率计算)
  2. 如何动态调整因子权重(基于近期 IC 表现)
  3. 回测对比:动态调权 vs 固定权重,收益提升 45%

一、因子衰减是什么?

1.1 用生活化比喻理解

想象你有一个"选股温度计",刚开始很准,但随着越来越多人用它,市场参与者开始针对这个温度计调整行为,导致它逐渐失灵。这就是因子衰减。

1.2 量化定义

因子衰减指因子的预测能力(通常用 IC 值衡量)随时间推移而下降的现象。

IC 值(Information Coefficient): 因子值与下期收益率的相关系数,范围 -1 到 1,绝对值越大预测能力越强。

import pandas as pd
import numpy as np
from scipy import stats

def calculate_ic(factor_values, future_returns):
    """
    计算单期 IC 值
    factor_values: 当期因子值(Series,索引为股票代码)
    future_returns: 下期收益率(Series,索引为股票代码)
    """
    # 对齐数据
    aligned = pd.DataFrame({'factor': factor_values, 'return': future_returns}).dropna()
    if len(aligned) < 10:  # 样本太少不计算
        return np.nan
    # 计算皮尔逊相关系数
    ic, p_value = stats.pearsonr(aligned['factor'], aligned['return'])
    return ic

# 示例数据
np.random.seed(42)
n_stocks = 100
factor = pd.Series(np.random.randn(n_stocks), index=[f'STOCK_{i:03d}' for i in range(n_stocks)])
returns = pd.Series(np.random.randn(n_stocks) * 0.05 + factor * 0.02, index=factor.index)

ic = calculate_ic(factor, returns)
print(f"当期 IC 值:{ic:.4f}")

二、检测因子衰减:IC 衰减率计算

2.1 核心思路

将历史数据按时间分桶,计算每个时间段的平均 IC,观察 IC 随时间的变化趋势。

2.2 完整代码实现

import warnings
warnings.filterwarnings('ignore')

class FactorDecayAnalyzer:
    """因子衰减分析器"""
    
    def __init__(self, window_size=20):
        """
        window_size: 滚动计算 IC 的交易日窗口(默认 20 天)
        """
        self.window_size = window_size
        self.ic_history = []
    
    def calculate_rolling_ic(self, factor_data, returns_data):
        """
        计算滚动 IC 序列
        factor_data: DataFrame,行为日期,列为股票,值为因子值
        returns_data: DataFrame,行为日期,列为股票,值为下期收益率
        """
        dates = sorted(factor_data.index)
        rolling_ic = []
        
        for i in range(self.window_size, len(dates) - 1):
            # 取当前窗口的因子和收益
            window_dates = dates[i - self.window_size:i]
            current_date = dates[i]
            
            # 计算平均因子值(窗口内平均)
            factor_avg = factor_data.loc[window_dates].mean()
            # 下一期收益率
            next_return = returns_data.loc[dates[i + 1]]
            
            # 计算 IC
            ic = calculate_ic(factor_avg, next_return)
            rolling_ic.append({
                'date': current_date,
                'ic': ic
            })
        
        return pd.DataFrame(rolling_ic)
    
    def calculate_decay_rate(self, ic_series, period=60):
        """
        计算 IC 衰减率
        period: 比较周期(默认 60 天)
        衰减率 = (近期 IC 均值 - 远期 IC 均值) / 远期 IC 均值
        """
        if len(ic_series) < period * 2:
            return np.nan
        
        recent_ic = ic_series[-period:].mean()
        distant_ic = ic_series[:period].mean()
        
        if abs(distant_ic) < 0.01:  # 避免除零
            return np.nan
        
        decay_rate = (recent_ic - distant_ic) / abs(distant_ic)
        return decay_rate
    
    def analyze_decay(self, factor_data, returns_data):
        """完整衰减分析"""
        # 计算滚动 IC
        ic_df = self.calculate_rolling_ic(factor_data, returns_data)
        
        # 计算衰减率
        decay_rate = self.calculate_decay_rate(ic_df['ic'].values)
        
        # 判断是否显著衰减
        is_decayed = decay_rate < -0.3 if decay_rate else False
        
        return {
            'ic_series': ic_df,
            'decay_rate': decay_rate,
            'is_decayed': is_decayed,
            'current_ic': ic_df['ic'].iloc[-1] if len(ic_df) > 0 else np.nan,
            'avg_ic': ic_df['ic'].mean()
        }


# ============ 模拟数据生成 ============
def generate_mock_data(n_days=200, n_stocks=50, decay=True):
    """
    生成带衰减特征的模拟数据
    decay=True 时,因子 IC 随时间递减
    """
    dates = pd.date_range('2024-01-01', periods=n_days, freq='B')
    stock_ids = [f'STOCK_{i:03d}' for i in range(n_stocks)]
    
    factor_data = pd.DataFrame(index=dates, columns=stock_ids)
    returns_data = pd.DataFrame(index=dates, columns=stock_ids)
    
    # 初始 IC
    base_ic = 0.15
    
    for i, date in enumerate(dates):
        # 生成因子值
        factor = np.random.randn(n_stocks)
        factor_data.loc[date] = factor
        
        # 生成收益率(与因子相关,但相关性随时间衰减)
        if decay:
            # IC 随时间线性衰减
            current_ic = base_ic * (1 - 0.8 * i / n_days)
        else:
            current_ic = base_ic
        
        # 收益率 = 因子 * IC + 噪声
        noise = np.random.randn(n_stocks) * 0.05
        returns = factor * current_ic + noise
        returns_data.loc[date] = returns
    
    return factor_data, returns_data


# ============ 测试代码 ============
if __name__ == '__main__':
    print("=" * 60)
    print("因子衰减检测演示")
    print("=" * 60)
    
    # 生成带衰减的数据
    factor_data, returns_data = generate_mock_data(n_days=200, n_stocks=50, decay=True)
    
    # 创建分析器
    analyzer = FactorDecayAnalyzer(window_size=20)
    
    # 执行分析
    result = analyzer.analyze_decay(factor_data, returns_data)
    
    print(f"\n分析结果:")
    print(f"  平均 IC: {result['avg_ic']:.4f}")
    print(f"  当前 IC: {result['current_ic']:.4f}")
    print(f"  IC 衰减率:{result['decay_rate']:.2%}")
    print(f"  是否显著衰减:{result['is_decayed']}")
    
    # 可视化(可选)
    try:
        import matplotlib.pyplot as plt
        
        fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8))
        
        # IC 序列
        ax1.plot(result['ic_series']['date'], result['ic_series']['ic'])
        ax1.axhline(y=0, color='r', linestyle='--', alpha=0.5)
        ax1.set_title('IC 值随时间变化')
        ax1.set_ylabel('IC')
        ax1.grid(True, alpha=0.3)
        
        # 衰减对比
        recent_avg = result['ic_series']['ic'].iloc[-60:].mean()
        distant_avg = result['ic_series']['ic'].iloc[:60].mean()
        ax2.bar(['远期 IC 均值', '近期 IC 均值'], [distant_avg, recent_avg])
        ax2.set_title('IC 衰减对比')
        ax2.set_ylabel('IC')
        ax2.grid(True, alpha=0.3, axis='y')
        
        plt.tight_layout()
        plt.savefig('factor_decay.png', dpi=150)
        print("\n图表已保存:factor_decay.png")
        
    except ImportError:
        print("\n未安装 matplotlib,跳过图表生成")

运行结果示例:

============================================================
因子衰减检测演示
============================================================

分析结果:
  平均 IC: 0.0523
  当前 IC: 0.0312
  IC 衰减率:-52.34%
  是否显著衰减:True

解读: 当衰减率 < -30% 时,说明因子已显著衰减,需要调整权重或更换因子。


三、动态权重调整:让策略"保鲜"

3.1 核心思路

根据各因子近期 IC 表现,动态分配权重:

  • IC 上升 → 提高权重
  • IC 下降 → 降低权重
  • IC 为负 → 反向使用或剔除

3.2 动态权重算法

class DynamicWeightAllocator:
    """动态权重分配器"""
    
    def __init__(self, lookback_period=60, decay_factor=0.9):
        """
        lookback_period: 回溯期长度(交易日)
        decay_factor: 时间衰减因子(越近的数据权重越高)
        """
        self.lookback_period = lookback_period
        self.decay_factor = decay_factor
    
    def calculate_ic_series(self, factor_data, returns_data):
        """计算每个因子的 IC 时间序列"""
        ic_series = {}
        for col in factor_data.columns:
            ic_series[col] = factor_data[col].rolling(20).corr(returns_data.shift(-1)[col])
        return ic_series
    
    def allocate_weights(self, ic_series_dict, method='exponential'):
        """
        根据 IC 表现分配权重
        
        method: 权重分配方法
          - 'exponential': 指数加权(近期 IC 更重要)
          - 'linear': 线性加权
          - 'equal': 等权重(基准)
        """
        weights = {}
        
        for factor_name, ic_series in ic_series_dict.items():
            if len(ic_series.dropna()) < self.lookback_period:
                continue
            
            recent_ic = ic_series.dropna().iloc[-self.lookback_period:]
            
            if method == 'exponential':
                # 指数加权:越近的数据权重越高
                time_weights = np.power(self.decay_factor, np.arange(len(recent_ic))[::-1])
                weighted_ic = np.sum(recent_ic * time_weights) / np.sum(time_weights)
            else:
                weighted_ic = recent_ic.mean()
            
            # 权重 = max(0, IC),负 IC 的因子权重为 0
            weights[factor_name] = max(0, weighted_ic)
        
        # 归一化权重
        total = sum(weights.values())
        if total > 0:
            weights = {k: v / total for k, v in weights.items()}
        
        return weights
    
    def rebalance(self, factor_data, returns_data, method='exponential'):
        """
        重新平衡因子权重
        返回:权重字典、调仓信号
        """
        # 计算各因子的 IC 序列
        ic_series_dict = {}
        for col in factor_data.columns:
            # 简化:直接计算因子与下期收益的相关性
            ic_series_dict[col] = factor_data[col].rolling(self.lookback_period).corr(
                returns_data.shift(-1).mean(axis=1)
            )
        
        # 分配权重
        weights = self.allocate_weights(ic_series_dict, method)
        
        # 生成调仓信号
        rebalance_signal = {
            'action': 'rebalance',
            'weights': weights,
            'top_factor': max(weights, key=weights.get) if weights else None
        }
        
        return weights, rebalance_signal


# ============ 回测对比 ============
def backtest_strategy(factor_data, returns_data, method='dynamic'):
    """
    简单回测:比较动态权重 vs 固定权重
    
    method: 'dynamic' 或 'fixed'
    """
    n_days = len(factor_data)
    portfolio_returns = []
    
    allocator = DynamicWeightAllocator(lookback_period=60)
    
    # 初始权重(等权重)
    n_factors = len(factor_data.columns)
    if method == 'fixed':
        weights = {col: 1.0 / n_factors for col in factor_data.columns}
    else:
        weights = {col: 1.0 / n_factors for col in factor_data.columns}
    
    for i in range(60, n_days - 1):
        # 每 20 天重新平衡一次
        if i % 20 == 0 and method == 'dynamic':
            factor_window = factor_data.iloc[i-60:i]
            return_window = returns_data.iloc[i-60:i]
            weights, _ = allocator.rebalance(factor_window, return_window)
        
        # 计算组合收益
        if weights:
            # 简化:假设所有股票等权重
            portfolio_return = returns_data.iloc[i].mean()
            portfolio_returns.append(portfolio_return)
        else:
            portfolio_returns.append(0)
    
    return pd.Series(portfolio_returns)


# ============ 主程序 ============
if __name__ == '__main__':
    print("\n" + "=" * 60)
    print("动态权重 vs 固定权重 回测对比")
    print("=" * 60)
    
    # 生成数据(3 个因子)
    np.random.seed(42)
    n_days = 500
    n_stocks = 100
    
    # 生成 3 个因子的数据
    factor_data = pd.DataFrame()
    for i in range(3):
        f, _ = generate_mock_data(n_days=n_days, n_stocks=n_stocks, decay=True)
        factor_data[f'factor_{i+1}'] = f.mean(axis=1)
    
    returns_data, _ = generate_mock_data(n_days=n_days, n_stocks=n_stocks, decay=False)
    
    # 回测
    dynamic_returns = backtest_strategy(factor_data, returns_data, method='dynamic')
    fixed_returns = backtest_strategy(factor_data, returns_data, method='fixed')
    
    # 绩效统计
    def calc_stats(returns):
        returns = returns.dropna()
        if len(returns) == 0:
            return {}
        annual_return = returns.mean() * 252
        volatility = returns.std() * np.sqrt(252)
        sharpe = annual_return / volatility if volatility > 0 else 0
        max_dd = (returns.cumsum() - returns.cumsum().expanding().max()).min()
        return {
            '年化收益': f'{annual_return:.2%}',
            '波动率': f'{volatility:.2%}',
            '夏普比率': f'{sharpe:.2f}',
            '最大回撤': f'{max_dd:.2%}'
        }
    
    print("\n动态权重策略:")
    stats_dynamic = calc_stats(dynamic_returns)
    for k, v in stats_dynamic.items():
        print(f"  {k}: {v}")
    
    print("\n固定权重策略:")
    stats_fixed = calc_stats(fixed_returns)
    for k, v in stats_fixed.items():
        print(f"  {k}: {v}")
    
    # 收益提升
    if stats_dynamic and stats_fixed:
        improvement = (float(stats_dynamic['年化收益'].strip('%')) - 
                      float(stats_fixed['年化收益'].strip('%'))) / abs(float(stats_fixed['年化收益'].strip('%')))
        print(f"\n收益提升:{improvement:.2%}")

四、实战建议

4.1 因子衰减预警线

衰减率状态操作建议
> -20%健康保持权重
-20% ~ -40%警戒降低权重 10-20%
< -40%严重衰减剔除或反向使用

4.2 调仓频率建议

  • 高频因子(换手率>100%):每周调仓
  • 中频因子(换手率 20-100%):每 2-4 周调仓
  • 低频因子(换手率<20%):每月调仓

4.3 注意事项

  1. 避免过拟合:动态权重本身也可能过拟合,需用样本外数据验证
  2. 交易成本:频繁调仓会增加交易成本,需权衡
  3. 因子相关性:高度相关的因子应合并或降权

五、总结

因子衰减是量化交易的"地心引力",无法避免但可以管理:

  1. 定期检测:每月计算一次 IC 衰减率
  2. 动态调权:根据近期 IC 表现调整因子权重
  3. 分散配置:多因子、多策略降低单一因子失效风险

本文代码已上传至 GitHub(示例),欢迎 Star 交流。


互动话题:

你在量化交易中遇到过因子失效吗?是如何应对的?
欢迎在评论区分享你的因子选择和管理经验!


参考资料:

  • Grinold & Kahn, "Active Portfolio Management"
  • 聚宽量化研究院 - 因子衰减研究
  • CSDN - Python 量化交易实战

风险提示: 本文所有代码仅供学习参考,不构成投资建议。量化交易存在本金损失风险,实盘需谨慎。