4. 因子有效性检验:IC/IR分析、分组回测、多空组合收益分析
因子选出来了,也做完预处理了。接下来最关键的一步——判断这个因子到底有没有用。
说白了,因子有效性检验就是回答三个问题:
- 这个因子跟未来收益有没有关系?
- 关系稳定吗?
- 真拿去交易能赚钱吗?
我个人习惯把检验分成三个层次:相关性检验(IC/IR)、单调性检验(分组回测)、收益性检验(多空组合)。一层层往下走,就像筛子一样,把伪因子筛掉。
核心观点:一个真正有效的择时因子,必须同时通过这三关。只过一关的,大概率是过拟合。
4.1 IC/IR分析:因子的“信号强度”测试
IC(Information Coefficient)衡量的是因子值与未来收益之间的相关性。我习惯用Spearman秩相关系数,因为它对极端值不敏感。
公式很简单:
IC_t = corr(因子值_t, 未来收益_{t+1})
IC为正,说明因子值越大,未来收益越高。IC为负,说明因子值越小,未来收益越高。绝对值越大,信号越强。
但光看IC还不够。你想想看,一个因子今天IC=0.8,明天IC=-0.7,这你敢用吗?所以我们需要IR(Information Ratio)——IC的均值除以标准差。
IR = mean(IC) / std(IC)
IR越高,说明因子表现越稳定。我个人经验:
- IR > 0.5:不错的因子,可以考虑实盘
- IR > 1.0:优秀因子,值得重仓
- IR < 0.2:基本是噪音,别浪费时间
避坑指南:我曾经遇到一个因子,IC均值0.12,看起来还行。但IR只有0.15,因为IC波动极大。后来分组回测发现,它只在特定月份有效,其他月份完全随机。嗯,这就是典型的“伪因子”。
代码实现也很直接:
import pandas as pd
import numpy as np
from scipy.stats import spearmanr
def calc_ic_ir(factor_series, forward_returns):
"""
计算IC和IR
factor_series: 因子值序列
forward_returns: 未来收益序列
"""
ic_list = []
for date in factor_series.index.unique():
f = factor_series.loc[date]
r = forward_returns.loc[date]
# 取共同股票
common = f.index.intersection(r.index)
if len(common) < 30: # 样本太少不计算
continue
ic, _ = spearmanr(f[common], r[common])
ic_list.append(ic)
ic_series = pd.Series(ic_list)
mean_ic = ic_series.mean()
std_ic = ic_series.std()
ir = mean_ic / std_ic if std_ic != 0 else 0
return {
'mean_ic': mean_ic,
'std_ic': std_ic,
'ir': ir,
'ic_series': ic_series
}
4.2 分组回测:因子的“单调性”检验
IC/IR通过了,接下来看分组回测。为什么?因为IC只能告诉你线性相关性,但因子和收益之间可能是非线性的。
举个例子:因子值最小的10%股票收益最差,中间80%都差不多,最大的10%收益最好。这种“U型”关系,IC算出来可能很低,但实际有交易价值。
分组回测的做法:
- 每个时间点,按因子值从小到大分成N组(通常5组或10组)
- 计算每组未来的平均收益
- 观察各组收益是否单调递增或递减
我一般看三个指标:
- 单调性:组1到组N的收益是否严格递增/递减
- 区分度:组1和组N的收益差距是否显著
- 稳定性:这种单调关系是否持续存在
判断标准:如果分组收益呈现“锯齿状”——比如组3收益最高,组4反而最低——那这个因子大概率是噪音。真正有效的因子,分组收益应该是平滑的阶梯状。
代码示例:
def group_backtest(factor_df, return_df, n_groups=5):
"""
分组回测
factor_df: 因子值,index=日期,columns=股票
return_df: 未来收益,index=日期,columns=股票
"""
results = []
for date in factor_df.index:
f = factor_df.loc[date].dropna()
r = return_df.loc[date]
common = f.index.intersection(r.index)
if len(common) < n_groups * 10:
continue
# 分组
f_common = f[common]
labels = pd.qcut(f_common, n_groups, labels=False)
# 计算每组收益
group_returns = []
for g in range(n_groups):
stocks = f_common[labels == g].index
group_returns.append(r[stocks].mean())
results.append(group_returns)
result_df = pd.DataFrame(results, columns=[f'Group_{i+1}' for i in range(n_groups)])
return result_df.mean() # 返回各组平均收益
4.3 多空组合收益分析:真金白银的检验
IC和分组都过了,最后一步——多空组合。这是最接近实盘的检验。
做法很简单:
- 做多因子值最高的10%股票
- 做空因子值最低的10%股票
- 计算这个多空组合的收益曲线
我重点关注几个指标:
| 指标 | 含义 | 合格线 |
|---|---|---|
| 年化收益率 | 多空组合的年化收益 | > 5% |
| 夏普比率 | 收益/风险 | > 1.0 |
| 最大回撤 | 组合从高点回落的最大幅度 | < 15% |
| 胜率 | 正收益月份占比 | > 60% |
注意:多空组合收益高不代表因子好。我曾经见过一个因子,多空年化收益20%,但最大回撤40%。这种因子在实盘中根本拿不住——你想想看,亏了40%的时候,你还敢继续持有吗?
代码实现:
def long_short_analysis(factor_df, return_df, top_pct=0.1):
"""
多空组合分析
"""
long_returns = []
short_returns = []
for date in factor_df.index:
f = factor_df.loc[date].dropna()
r = return_df.loc[date]
common = f.index.intersection(r.index)
if len(common) < 50:
continue
f_common = f[common]
n_top = int(len(f_common) * top_pct)
# 多头:因子值最高的10%
long_stocks = f_common.nlargest(n_top).index
long_ret = r[long_stocks].mean()
# 空头:因子值最低的10%
short_stocks = f_common.nsmallest(n_top).index
short_ret = r[short_stocks].mean()
long_returns.append(long_ret)
short_returns.append(short_ret)
long_series = pd.Series(long_returns)
short_series = pd.Series(short_returns)
spread = long_series - short_series
# 计算指标
annual_return = spread.mean() * 252
sharpe = spread.mean() / spread.std() * np.sqrt(252)
max_dd = (spread.cumsum().cummax() - spread.cumsum()).max()
win_rate = (spread > 0).mean()
return {
'annual_return': annual_return,
'sharpe': sharpe,
'max_drawdown': max_dd,
'win_rate': win_rate,
'long_returns': long_series,
'short_returns': short_series,
'spread': spread
}
4.4 综合判断:如何下结论?
三个检验都做完了,怎么综合判断?我个人的打分体系:
- IC/IR:IC绝对值 > 0.05,IR > 0.5 → 得1分
- 分组回测:分组收益严格单调,组1和组N收益差显著 → 得1分
- 多空组合:夏普 > 1.0,最大回撤 < 15% → 得1分
3分:优秀因子,放心用。
2分:可用,但需要进一步优化。
1分及以下:建议放弃,别浪费时间。
最后说一句:因子有效性检验不是一次性工作。市场在变,因子的有效性也在变。我建议每季度重新跑一遍这三层检验,及时淘汰失效的因子。毕竟,在量化交易里,唯一不变的就是变化本身。