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,说明它们捕捉的是差不多的信息。等权合成相当于给这个信息重复赋权了。我曾在项目中遇到过这种情况:三个动量因子相关性极高,合成后整个组合的动量暴露过大,市场风格一转,回撤惨不忍睹。
处理相关性,我一般分三步走:
- 计算相关性矩阵:看看哪些因子「抱团」了。
- 合并同类项:相关性超过0.7的,考虑只保留一个,或者做PCA降维。
- 正交化处理:如果不想丢掉因子,可以用施密特正交化,把相关性剥离掉。
我的小技巧:在做正交化之前,先看看因子的逻辑是否真的不同。有时候两个因子虽然统计上相关,但背后的驱动逻辑完全不同,这时候我倾向于保留,用加权的方式去平衡。
正交化的代码实现:
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分析检验因子是否真的能预测收益,分层回测则用最直观的方式告诉你——这个因子到底能不能赚钱。
我做了这么多年量化,最大的体会就是:不要追求花哨的合成方法。等权合成跑赢大多数复杂模型的情况,我见过太多次了。先把基础做扎实,再考虑优化。