第四节:单因子测试——IC分析、IR分析、分组回测与因子有效性评估
单因子测试,说白了就是给每个候选因子做“体检”。
我刚开始做量化那会儿,总喜欢堆砌因子,觉得越多越好。结果呢?回测曲线漂亮得不行,实盘一跑就露馅。后来我才明白——因子不是越多越好,关键是每个因子都得经得起检验。
这一节,我们就来聊聊怎么给因子做全面体检。核心就四步:IC分析、IR分析、分组回测、有效性评估。
4.1 IC分析——因子与收益的相关性检验
IC,全称是Information Coefficient,信息系数。它衡量的是因子值与未来收益之间的相关性。
我个人习惯用Spearman秩相关系数,而不是普通的Pearson相关系数。为什么?因为秩相关对极端值不敏感,更稳健。你想想看,A股市场动不动就出个涨停跌停,用Pearson很容易被带偏。
IC的两种常见计算方式:
- Normal IC(Pearson): 因子值与收益的线性相关性
- Rank IC(Spearman): 因子值排名与收益排名的相关性
我个人推荐用Rank IC,实战中更靠谱。
来看代码实现:
import pandas as pd
import numpy as np
from scipy.stats import spearmanr
def calc_ic(factor_series, return_series, method='rank'):
"""
计算单期IC值
:param factor_series: 因子值序列
:param return_series: 下期收益序列
:param method: 'rank' 或 'normal'
:return: IC值
"""
if method == 'rank':
ic, p_value = spearmanr(factor_series, return_series)
else:
ic = factor_series.corr(return_series)
return ic
# 实战中,我们通常计算月度IC
def calc_monthly_ic(factor_df, return_df):
"""
计算月度IC序列
:param factor_df: 因子值DataFrame,index为日期,columns为股票
:param return_df: 下月收益DataFrame
:return: 月度IC序列
"""
ic_series = pd.Series(index=factor_df.index)
for date in factor_df.index:
ic_series[date] = calc_ic(
factor_df.loc[date],
return_df.loc[date]
)
return ic_series
嗯,这里要注意:IC值不是越高越好。IC的绝对值在0.05以上就算不错了,超过0.1就是很优秀的因子。我见过有人拿IC=0.3的因子当宝贝,结果一查,样本量才30只股票——这明显是过拟合了。
4.2 IR分析——IC的稳定性比大小更重要
IR,Information Ratio,信息比率。它衡量的是IC的稳定性。
公式很简单:IR = mean(IC) / std(IC)
为什么IR重要?我举个例子你就明白了。
因子A:IC序列为[0.1, -0.1, 0.1, -0.1, 0.1],均值0.02,标准差0.1,IR=0.2
因子B:IC序列为[0.05, 0.04, 0.06, 0.05, 0.05],均值0.05,标准差0.007,IR=7.14
因子A的IC均值更高,但波动太大,你根本不敢用它。因子B虽然IC均值低一些,但非常稳定,实盘时你心里有底。
实战经验: 我个人习惯把IR大于0.5作为及格线,大于1.0算优秀。IR低于0.3的因子,基本可以放弃了。
def calc_ir(ic_series):
"""
计算IR值
:param ic_series: IC序列
:return: IR值
"""
return ic_series.mean() / ic_series.std()
# 顺便算一下IC的胜率——IC为正的比例
def calc_ic_win_rate(ic_series):
return (ic_series > 0).mean()
我曾经踩过一个坑:有个因子IC均值0.08,IR高达2.3,看起来完美。结果仔细一看,IC序列在2015年股灾期间突然变成负的,直接把组合带沟里了。所以,光看整体IR还不够,还得看IC的时间序列图,看看有没有明显的结构性变化。
4.3 分组回测——最直观的因子检验方法
分组回测,就是把股票按因子值从小到大分成N组,然后看每组未来的收益表现。
理想情况下,第1组(因子值最小)到第N组(因子值最大)的收益应该是单调的。要么递增,要么递减。如果中间有跳跃,说明因子可能有问题。
我一般分5组或10组。分5组看得更宏观,分10组能发现更细微的模式。
def group_backtest(factor_df, return_df, n_groups=5):
"""
分组回测
:param factor_df: 因子值DataFrame
:param return_df: 收益DataFrame
:param n_groups: 分组数
:return: 每组收益序列
"""
group_returns = pd.DataFrame(index=return_df.index,
columns=range(1, n_groups+1))
for date in factor_df.index:
# 获取当期的因子值和收益
factors = factor_df.loc[date].dropna()
returns = return_df.loc[date]
# 按因子值排序并分组
sorted_factors = factors.sort_values()
group_size = len(sorted_factors) // n_groups
for i in range(n_groups):
start_idx = i * group_size
end_idx = start_idx + group_size if i < n_groups-1 else len(sorted_factors)
group_stocks = sorted_factors.index[start_idx:end_idx]
# 计算等权收益
group_returns.loc[date, i+1] = returns[group_stocks].mean()
return group_returns
避坑指南: 我曾经犯过一个低级错误——分组时没考虑停牌股票。结果某组里全是停牌股,收益曲线平平的,我还以为发现了低波动因子。后来才发现,那组股票根本就没交易!所以,分组前一定要先剔除停牌、ST等异常股票。
4.4 因子有效性评估——综合打分
单个指标都有局限性,我习惯用一套综合评估体系来给因子打分。
我的评估框架包括以下维度:
| 评估维度 | 指标 | 及格线 | 优秀线 |
|---|---|---|---|
| 预测能力 | IC均值(绝对值) | 0.02 | 0.05 |
| 稳定性 | IR | 0.5 | 1.0 |
| 单调性 | 分组收益单调性得分 | 0.6 | 0.8 |
| 多头收益 | Top组年化超额收益 | 5% | 10% |
| 空头收益 | Bottom组年化超额收益(绝对值) | 3% | 8% |
| 换手率 | 因子值月度自相关性 | 0.6 | 0.8 |
每个维度打分(0-100分),然后加权平均。我个人习惯的权重是:IC均值30%、IR 25%、单调性20%、多头收益15%、空头收益10%。
def factor_score(ic_mean, ir, monotonicity, top_return, bottom_return):
"""
因子综合评分
"""
score = 0
score += min(ic_mean / 0.05, 1) * 30 # IC均值
score += min(ir / 1.0, 1) * 25 # IR
score += monotonicity * 20 # 单调性
score += min(top_return / 0.1, 1) * 15 # 多头收益
score += min(abs(bottom_return) / 0.08, 1) * 10 # 空头收益
return score
总分超过80分的因子,我才会考虑纳入因子库。60-80分的,需要进一步观察。低于60分的,直接淘汰。
4.5 本章知识体系
下面这张图,是我自己总结的单因子测试全流程。每次做因子研究时,我都会对照着走一遍。
这套流程走下来,一个因子能不能用,基本就心里有数了。记住,因子测试不是一次性工作。市场在变,因子的表现也会变。我建议每季度重新跑一遍测试,及时淘汰失效的因子。
最后说一句: 单因子测试只是第一步。真正的好策略,往往是多个低相关因子的组合。下一节,我们会聊怎么把多个因子组合起来,构建多因子模型。但前提是——你得先把每个单因子测明白。
公众号:蓝海数据掘金营,微信deep3321