单因子测试框架:IC/IR分析、分层回测法、因子收益率计算
做过多因子模型的朋友都知道,选因子就像相亲——看着顺眼不行,得真能过日子。单因子测试就是帮我们把关的环节。我个人习惯把这部分拆成三个维度:预测能力(IC/IR)、区分能力(分层回测)、收益贡献(Fama-MacBeth回归)。咱们一个一个聊。
IC/IR分析:因子的预测能力到底行不行
IC,全称是Information Coefficient,信息系数。说白了就是因子值和未来收益之间的相关性。我刚开始做量化时,总觉得IC越高越好,后来发现没那么简单。
IC的计算方式有两种:
- Spearman秩相关系数:看排序一致性,对极端值不敏感
- Pearson相关系数:看线性关系,但容易被异常值带偏
我个人更推荐Spearman。为什么?因为因子值本身可能分布很偏,但排序关系往往更稳定。我在项目中遇到过一只因子,Pearson IC只有0.02,但Spearman IC有0.08——后来分层回测发现,它确实能区分好坏股票,只是线性关系不强。
IR呢?IR = mean(IC) / std(IC)。它衡量的是IC的稳定性。你想想看,一个因子IC忽高忽低,你敢重仓用吗?
实战经验:我一般要求月度IC均值 > 0.02,IR > 0.5。低于这个门槛的因子,基本可以放弃了。
代码实现其实不复杂:
import pandas as pd
import numpy as np
from scipy.stats import spearmanr
def calc_ic(factor_series, return_series, method='spearman'):
"""计算截面IC"""
if method == 'spearman':
ic, p_value = spearmanr(factor_series, return_series)
else:
ic = factor_series.corr(return_series)
return ic
def calc_ir(ic_series):
"""计算IR"""
return ic_series.mean() / ic_series.std()
# 月度滚动计算
monthly_ic = []
for date in unique_dates:
cross_data = data[data['date'] == date]
ic = calc_ic(cross_data['factor'], cross_data['next_return'])
monthly_ic.append(ic)
ir = calc_ir(pd.Series(monthly_ic))
避坑指南:我曾经犯过一个错误——直接用未来收益和当期因子算IC,没考虑行业中性化。结果IC虚高,实盘一跑就崩。记得先做行业市值中性化处理。
分层回测法:因子能不能区分好坏股票
IC告诉你因子和收益有没有关系,但分层回测告诉你——这种关系能不能用来赚钱。
做法很简单:每个月把所有股票按因子值从大到小排序,分成10组(或者5组)。然后看第1组(因子最大)和第10组(因子最小)的收益差异。
我习惯用这个框架:
- 每月末按因子值排序,分10层
- 每层等权或市值加权构建组合
- 持有到下个月末,记录收益
- 滚动重复,计算每层的累计净值
好的因子应该呈现单调性——第1组收益最高,第10组最低,中间依次排列。如果出现第5组收益最高,那这个因子就有问题。
注意:分层回测最怕幸存者偏差。我曾经用了一个退市股票没剔除的数据集,结果因子表现好得离谱——后来发现是因为垃圾股退市了,剩下的都是好股票。一定要做退市处理。
代码示例:
def layer_backtest(data, factor_col, return_col, n_layers=10):
"""分层回测"""
results = []
for date in data['date'].unique():
cross_data = data[data['date'] == date].copy()
cross_data['rank'] = cross_data[factor_col].rank(pct=True)
cross_data['layer'] = pd.cut(cross_data['rank'],
bins=n_layers,
labels=range(n_layers))
layer_returns = cross_data.groupby('layer')[return_col].mean()
results.append(layer_returns)
result_df = pd.DataFrame(results)
return result_df.mean() # 返回每层平均收益
因子收益率计算:Fama-MacBeth回归
终于到了最硬核的部分。Fama-MacBeth回归,简称FM回归。它解决了一个核心问题:因子到底能带来多少超额收益?
FM回归分两步走:
- 第一步:每个时间截面做一次横截面回归。比如每个月,用当期因子值对下期收益做回归。
- 第二步:把各期的回归系数取均值,做t检验。
为什么这么做?因为传统的时间序列回归假设残差独立同分布,但股票收益的残差是相关的——同行业的股票往往一起涨跌。FM回归通过截面回归+时间序列平均,解决了这个问题。
我当年学FM回归时,最困惑的是为什么要做两步。后来想明白了:第一步是看因子在每个时点上的解释力,第二步是看这种解释力是否稳定。
import statsmodels.api as sm
def fama_macbeth(data, factor_cols, return_col, date_col='date'):
"""Fama-MacBeth回归"""
results = []
for date in data[date_col].unique():
cross_data = data[data[date_col] == date].dropna()
if len(cross_data) < 30:
continue
X = cross_data[factor_cols]
X = sm.add_constant(X)
y = cross_data[return_col]
model = sm.OLS(y, X).fit()
results.append(model.params)
# 第二步:时间序列平均
coef_df = pd.DataFrame(results)
final_results = {}
for col in coef_df.columns:
mean_coef = coef_df[col].mean()
t_stat = coef_df[col].mean() / (coef_df[col].std() / np.sqrt(len(coef_df)))
final_results[col] = {'系数': mean_coef, 't值': t_stat}
return final_results
关键点:t值的绝对值大于2,通常认为因子显著。但要注意——如果因子数量多,多重共线性会干扰结果。我一般先做VIF检验,剔除高度相关的因子。
三种方法的联动关系
这三种方法不是孤立的。我通常这样组合使用:
- 先用IC/IR做初筛,剔除预测能力差的因子
- 再用分层回测验证单调性,确保因子能区分好坏
- 最后用FM回归量化因子收益率,确认统计显著性
只有三个条件都满足的因子,我才会纳入最终的多因子模型。少一个都不行——这是我在实盘里摔过跟头换来的教训。
个人习惯:我会把每个因子的IC序列、分层收益曲线、FM回归系数画在一张图上。一眼就能看出这个因子靠不靠谱。数据可视化比看数字直观多了。
嗯,单因子测试这部分内容就这些。记住一个原则:因子不是越多越好,而是越精越好。一个经过严格测试的优质因子,胜过十个凑数的垃圾因子。