第四节:单因子测试框架:IC分析、Rank IC与分组回测
好,我们进入正题。
因子构建出来之后,第一件事不是急着上策略,而是问自己一个问题:这个因子到底有没有预测能力?
我见过太多人,写了几十行代码算出个因子,直接扔进回测引擎,结果曲线漂亮得不行——一查,全是未来函数。或者更隐蔽的:因子本身跟收益根本没关系,纯粹是过拟合。
所以,我们需要一套标准化的检验流程。今天聊的三个工具:IC分析、Rank IC分析、分组回测,就是这套流程的基石。
核心逻辑一句话:IC看线性相关性,Rank IC看单调性,分组回测看实际赚钱能力。三者缺一不可。
4.1 IC分析:信息系数
IC,全称 Information Coefficient,信息系数。说白了就是因子值与未来收益之间的相关系数。
我习惯用 Pearson相关系数 来计算。公式很简单:
IC = corr(因子值, 未来一期收益率)
具体到代码实现,我一般这么写:
import pandas as pd
import numpy as np
from scipy.stats import pearsonr
def calc_ic(factor_series, return_series):
"""
计算截面IC
factor_series: 某一天的因子值(所有股票)
return_series: 对应股票的未来收益率
"""
# 去极值、标准化处理后的数据
clean_factor = winsorize(factor_series, limits=[0.01, 0.99])
clean_return = winsorize(return_series, limits=[0.01, 0.99])
# 剔除缺失值
mask = clean_factor.notna() & clean_return.notna()
ic, p_value = pearsonr(clean_factor[mask], clean_return[mask])
return ic, p_value
我的经验:IC值在 -0.1 到 0.1 之间是常态。别指望IC能到0.5以上——那基本意味着你的因子跟未来收益有某种线性关系,但现实中很少见。我见过最夸张的一次,IC跑到0.3,后来发现是数据对齐出了问题。
IC分析的核心指标有两个:
- IC均值:看因子整体方向。正数表示因子值越大收益越高,负数则相反。
- IC标准差:看因子稳定性。标准差越小,因子表现越稳定。
还有一个常用指标叫 IR(信息比率):
IR = IC均值 / IC标准差
IR大于0.5算不错,大于1.0就算很好了。我一般用IR来筛选因子——IR太低的因子,就算IC均值还行,实际用起来也容易翻车。
4.2 Rank IC分析:秩相关系数
Pearson IC有个问题:它对异常值特别敏感。你想想看,如果某只股票因为停牌或者数据错误,因子值突然飙到天上,Pearson相关系数立马就被带偏了。
所以,我更常用的是 Rank IC,也就是Spearman秩相关系数。
Rank IC的计算逻辑:
- 把因子值排序,变成秩(rank)
- 把未来收益率也排序,变成秩
- 计算这两个秩之间的Pearson相关系数
代码实现:
from scipy.stats import spearmanr
def calc_rank_ic(factor_series, return_series):
"""
计算截面Rank IC
"""
mask = factor_series.notna() & return_series.notna()
rank_ic, p_value = spearmanr(
factor_series[mask],
return_series[mask]
)
return rank_ic, p_value
为什么Rank IC更靠谱?
因为秩只关心相对顺序,不关心具体数值。哪怕某只股票的因子值是100倍于其他股票,它的秩也只是第1名而已。这就天然过滤掉了极端值的影响。
我个人的习惯是:同时看IC和Rank IC。如果两者方向一致,说明因子关系比较稳健。如果IC为正但Rank IC为负,那就要小心了——很可能存在非线性关系或者异常值干扰。
举个例子,我曾经遇到一个因子,IC算出来0.08,看起来不错。但Rank IC只有0.01,几乎为零。后来一查,原来是某只小市值股票因子值异常高,拉动了整个相关性。去掉这只股票后,IC直接掉到0.02。嗯,这就是典型的「伪相关」。
4.3 分组回测法
IC和Rank IC说到底都是统计量。但做量化的人都知道:统计显著不等于交易赚钱。
分组回测,就是直接模拟交易,看因子在实际操作中能不能赚钱。
做法很简单:
- 在每个调仓日,把所有股票按因子值从大到小排序
- 分成N组(通常5组或10组)
- 每组等权或市值加权买入,持有到下一个调仓日
- 计算每组的累计收益
代码框架:
def group_backtest(factor_df, return_df, n_groups=5):
"""
分组回测
factor_df: 日期×股票的因子值矩阵
return_df: 日期×股票的收益率矩阵
"""
results = {}
for date in factor_df.index:
# 获取当天的因子值和未来收益
factors = factor_df.loc[date]
returns = return_df.loc[date]
# 分组
groups = pd.qcut(factors, n_groups, labels=False)
# 计算每组收益
for g in range(n_groups):
mask = (groups == g)
group_return = returns[mask].mean()
results.setdefault(g, []).append(group_return)
# 计算累计收益
cum_returns = {}
for g, ret_list in results.items():
cum_returns[g] = (1 + pd.Series(ret_list)).cumprod()
return cum_returns
注意:分组回测最容易踩的坑是「幸存者偏差」。我刚开始做的时候,直接用当前存续的股票做回测,结果曲线漂亮得不像话。后来才发现,那些退市的股票全被我忽略了——而它们恰恰是亏钱的主力。
正确的做法是:使用历史快照数据,也就是每个时间点实际存在的股票池。
分组回测的核心看什么?
- 单调性:第1组收益 > 第2组 > ... > 第N组?如果是,说明因子有很好的区分度。
- 多空收益:第1组收益 - 第N组收益。这个差值越大越好。
- 分组稳定性:各组收益是否随时间稳定,还是忽高忽低?
我一般会画一张分组收益曲线图,把5条线叠在一起看。如果第1组始终在最上面,第5组始终在最下面,中间各组依次排列——嗯,这个因子基本稳了。
但如果曲线交叉得一塌糊涂,比如第3组突然跑到最上面去了——那说明因子存在明显的非线性关系,或者干脆就是噪声。
4.4 三者如何配合使用?
我自己的流程是这样的:
| 步骤 | 方法 | 目的 | 通过标准 |
|---|---|---|---|
| 1 | IC分析 | 快速筛查,看因子是否有线性预测能力 | IC均值绝对值 > 0.02,IR > 0.5 |
| 2 | Rank IC分析 | 排除异常值干扰,验证单调性 | Rank IC方向与IC一致,绝对值 > 0.02 |
| 3 | 分组回测 | 模拟真实交易,验证实际赚钱能力 | 分组单调性明显,多空收益显著 |
三步都通过了,我才会把这个因子放进候选池。但凡有一项不过,我都会标记为「待观察」——不急着用,也不急着扔,等更多数据出来再说。
一个小技巧:我习惯把IC和Rank IC画成时间序列图,按月滚动计算。这样能直观看到因子的「保质期」——有些因子前两年IC很高,后面突然失效了。这种因子我一般不会用,因为不知道它什么时候会再次失效。
好了,单因子测试框架的核心内容就这些。IC看线性关系,Rank IC看秩相关,分组回测看实战效果。三者配合,基本能筛掉90%的垃圾因子。
剩下的10%,才是真正值得深入研究的。