4、多因子合成:等权与加权合成、因子相关性处理、合成因子IC分析、分层回测

好,咱们进入多因子合成的环节。

说实话,这一步是整个量化选股流程里最体现「手艺」的地方。你单因子做得再好,如果合成这步搞砸了,前面全白干。我自己刚入行那会儿,就吃过这个亏——辛辛苦苦挖了十几个因子,等权一合,结果还不如单因子表现好。后来才明白,合成不是简单拼凑,而是一门平衡的艺术。

4.1 等权合成 vs 加权合成

先聊最基础的两种方式:等权和加权。

等权合成,说白了就是每个因子都一视同仁。你想想看,如果你有5个因子,每个因子权重就是20%。这种做法最大的好处是——简单、稳健。我个人的习惯是,在因子刚开发完、还没摸清各自脾气的时候,先用等权跑一遍。为什么?因为等权不会放大某个因子的噪声,也不容易过拟合。

但等权有个致命问题:它假设所有因子都一样好。这显然不现实。有的因子IC高、稳定性好,有的因子时灵时不灵。这时候就需要加权合成了。

加权的方式有很多种,我列几个常用的:

  • IC均值加权:用过去一段时间的IC均值作为权重。IC高的因子多给点权重。
  • ICIR加权:用IC的均值除以标准差。这比单纯IC均值更稳健,因为它惩罚了波动大的因子。
  • 半衰加权:近期的IC更重要,远期的权重逐渐衰减。这个我特别喜欢用,因为因子的有效性是会变的。

核心原则:加权不是越复杂越好。我见过有人用机器学习去优化权重,结果回测漂亮,实盘一塌糊涂。记住,权重的稳定性比「最优」更重要。

来个代码示例,看看怎么实现ICIR加权:

import pandas as pd
import numpy as np

def icir_weight(factor_ic_df, half_life=60):
    """
    基于ICIR的加权合成
    factor_ic_df: 每列是一个因子的日度IC序列
    """
    # 计算每个因子的ICIR
    ic_mean = factor_ic_df.mean()
    ic_std = factor_ic_df.std()
    icir = ic_mean / ic_std
    
    # 归一化权重
    weights = icir / icir.sum()
    return weights

# 假设我们有3个因子的IC数据
ic_data = pd.DataFrame({
    'factor1': np.random.randn(100) * 0.05 + 0.03,
    'factor2': np.random.randn(100) * 0.08 + 0.02,
    'factor3': np.random.randn(100) * 0.03 + 0.04
})

weights = icir_weight(ic_data)
print("因子权重:", weights.values)

4.2 因子相关性处理

嗯,这里要注意。因子之间如果有强相关性,直接合成会出问题。

为什么?你想想看,两个因子如果相关系数0.9,说明它们捕捉的是差不多的信息。等权合成相当于给这个信息重复赋权了。我曾在项目中遇到过这种情况:三个动量因子相关性极高,合成后整个组合的动量暴露过大,市场风格一转,回撤惨不忍睹。

处理相关性,我一般分三步走:

  1. 计算相关性矩阵:看看哪些因子「抱团」了。
  2. 合并同类项:相关性超过0.7的,考虑只保留一个,或者做PCA降维。
  3. 正交化处理:如果不想丢掉因子,可以用施密特正交化,把相关性剥离掉。

我的小技巧:在做正交化之前,先看看因子的逻辑是否真的不同。有时候两个因子虽然统计上相关,但背后的驱动逻辑完全不同,这时候我倾向于保留,用加权的方式去平衡。

正交化的代码实现:

from sklearn.linear_model import LinearRegression

def orthogonalize(factor_matrix, target_factor):
    """
    将target_factor对factor_matrix做回归,取残差作为正交化后的因子
    """
    model = LinearRegression()
    model.fit(factor_matrix, target_factor)
    residual = target_factor - model.predict(factor_matrix)
    return residual

# 假设factor1和factor2高度相关,我们想保留factor1,对factor2做正交化
factor1 = np.random.randn(1000)
factor2 = np.random.randn(1000) * 0.5 + factor1 * 0.8  # 强相关

factor2_orth = orthogonalize(factor1.reshape(-1, 1), factor2)
print("正交化后相关系数:", np.corrcoef(factor1, factor2_orth)[0, 1])
# 输出应该接近0

4.3 合成因子IC分析

因子合成完了,怎么知道它好不好?看IC。

IC(Information Coefficient)就是因子值和未来收益的相关系数。合成因子的IC分析,我一般看三个维度:

  • IC均值:越大越好,一般绝对值超过0.02就算不错了。
  • IC标准差:越小越好,说明因子稳定。
  • ICIR:IC均值除以标准差,这个指标比单纯的IC更有说服力。

我个人习惯把IC画成时间序列图,一眼就能看出因子什么时候失效了。有一次我回测一个合成因子,IC均值0.04,看起来不错。但画出来一看,2018年之后IC就一直在0附近晃悠。这就是典型的「幸存者偏差」——早期表现好,后期不行了。

避坑指南:我曾经犯过一个错误——只看全样本的IC均值,忽略了IC的时序稳定性。后来我养成了一个习惯:滚动计算IC,比如用过去60天的窗口,看看IC是不是在衰减。如果IC在持续下降,那这个因子可能已经「死」了。

滚动IC计算的代码:

def rolling_ic(factor_series, return_series, window=60):
    """
    计算滚动IC
    """
    ic_list = []
    for i in range(window, len(factor_series)):
        ic = factor_series[i-window:i].corr(return_series[i-window:i])
        ic_list.append(ic)
    return pd.Series(ic_list, index=factor_series.index[window:])

# 示例
factor = pd.Series(np.random.randn(500))
returns = pd.Series(np.random.randn(500) * 0.02 + factor * 0.01)

rolling_ic_series = rolling_ic(factor, returns)
print("滚动IC均值:", rolling_ic_series.mean())
print("滚动IC标准差:", rolling_ic_series.std())

4.4 分层回测

最后一步,也是最有意思的一步——分层回测。

说白了,就是把股票按因子值从高到低分成N组(通常是5组或10组),然后看每组未来的收益表现。一个好的因子,应该是分组收益单调递增或递减的。

我一般分5层:

分组 因子值范围 预期表现
第1组(多) 最高20% 收益最高
第2组 次高20% 收益次高
第3组 中间20% 收益中等
第4组 次低20% 收益次低
第5组(空) 最低20% 收益最低

如果第1组收益最高、第5组收益最低,而且中间各组是单调的,那这个因子就非常漂亮。如果出现第3组比第2组还高,说明因子有非线性问题,需要进一步处理。

分层回测的代码实现:

def layered_backtest(factor_df, return_df, n_groups=5):
    """
    分层回测
    factor_df: 每期股票的因子值,行是时间,列是股票
    return_df: 每期股票的收益
    """
    group_returns = []
    
    for date in factor_df.index:
        # 获取当期因子值和收益
        factors = factor_df.loc[date].dropna()
        returns = return_df.loc[date]
        
        # 按因子值分组
        labels = pd.qcut(factors, n_groups, labels=False)
        
        # 计算每组收益
        for g in range(n_groups):
            group_stocks = factors[labels == g].index
            group_ret = returns[group_stocks].mean()
            group_returns.append({
                'date': date,
                'group': g + 1,
                'return': group_ret
            })
    
    result = pd.DataFrame(group_returns)
    # 计算每组累计收益
    cum_returns = result.groupby('group')['return'].apply(
        lambda x: (1 + x).cumprod()
    )
    return cum_returns

# 示例
factor_df = pd.DataFrame(np.random.randn(252, 100), 
                         index=pd.date_range('2023-01-01', periods=252))
return_df = pd.DataFrame(np.random.randn(252, 100) * 0.02,
                         index=pd.date_range('2023-01-01', periods=252))

cum_ret = layered_backtest(factor_df, return_df)
print("各组最终累计收益:")
print(cum_ret.groupby('group').last())

分层回测的核心:不要只看多空组合的收益差。我建议你仔细看看每一组的收益曲线,特别是第1组和第5组。如果第1组收益曲线平稳向上,第5组平稳向下,那这个因子就是「真金」。

最后,我画一张图,把整个多因子合成的流程串起来:

多因子合成与评估流程 单因子输入 因子相关性检测与处理 等权合成 / 加权合成 合成因子IC分析 分层回测验证 原始因子 去冗余 权重分配 有效性检验 收益验证 相关系数 > 0.7 需处理 ICIR加权更稳健 关注IC时序稳定性 分组收益需单调

整个流程走下来,你会发现多因子合成其实是个「去伪存真」的过程。相关性处理去掉冗余信息,加权合成突出有效因子,IC分析检验因子是否真的能预测收益,分层回测则用最直观的方式告诉你——这个因子到底能不能赚钱。

我做了这么多年量化,最大的体会就是:不要追求花哨的合成方法。等权合成跑赢大多数复杂模型的情况,我见过太多次了。先把基础做扎实,再考虑优化。

专注资料整理