单因子测试框架:IC分析、分组回测、多空组合收益、换手率分析
做量化选股的朋友都知道,因子选股的核心就一句话:找到能预测未来收益的特征。但问题来了——你怎么知道这个因子真的有效?
我见过太多人,拿着一个因子回测曲线漂亮得不行,实盘一跑就崩。为什么?因为没做严格的单因子测试。今天我就把这一整套框架掰开揉碎了讲给你听。
核心观点:单因子测试不是跑一个回测就完事,而是从IC分析、分组回测、多空组合、换手率四个维度交叉验证。缺一个,结论都可能翻车。
一、IC分析:因子与收益的相关性检验
IC(Information Coefficient)说白了就是因子值和未来收益之间的相关系数。我习惯用Spearman秩相关系数,因为它对极端值不敏感。
具体做法很简单:
- 在每个截面期(比如每月末),计算所有股票的因子值
- 计算因子值与下一期收益的秩相关系数
- 得到一条IC时间序列
为什么要看IC?因为它告诉你因子有没有预测能力。IC为正,说明因子值越大,未来收益越高;IC为负,说明因子值越小,未来收益越高。
我的经验:IC的均值最好在0.02以上,且IC的IR(信息比率,即IC均值/IC标准差)要大于0.5。我在项目中遇到过IC均值0.05但IR只有0.2的因子,实盘效果很差——因为它的预测能力不稳定。
代码实现也不复杂:
import pandas as pd
import numpy as np
from scipy.stats import spearmanr
def calc_ic(factor_df, return_df):
"""
factor_df: 因子值,index为股票代码,columns为时间
return_df: 未来收益,index为股票代码,columns为时间
"""
ic_series = pd.Series(index=factor_df.columns, dtype=float)
for date in factor_df.columns:
# 剔除缺失值
valid = factor_df[date].notna() & return_df[date].notna()
if valid.sum() < 30: # 样本太少不计算
continue
ic_series[date] = spearmanr(
factor_df[date][valid],
return_df[date][valid]
)[0]
return ic_series
# 使用示例
ic = calc_ic(factor_values, forward_returns)
print(f"IC均值: {ic.mean():.4f}")
print(f"IC标准差: {ic.std():.4f}")
print(f"IR: {ic.mean()/ic.std():.4f}")
二、分组回测:看因子在不同区间的表现
IC分析只能告诉你因子和收益有没有线性关系。但实际中,因子的预测能力可能不是线性的——比如只有极端值才有预测力。
分组回测就是解决这个问题的。我通常把股票按因子值分成10组(Decile),然后看每组的表现。
具体步骤:
- 在每个截面期,按因子值从小到大排序
- 等分成10组,第1组因子值最小,第10组最大
- 计算每组在持有期内的等权收益
- 时间序列上累加,得到每组净值曲线
你想想看,如果因子真的有效,那么第1组和第10组的收益应该有明显差异。而且组间收益应该是单调的——因子值越大,收益越高(或越低)。
注意:分组回测最怕的是「幸存者偏差」。我曾经犯过这个错——用当前还在交易的股票做回测,结果因子表现好得离谱。后来才发现,那些退市的股票因子值都很差,但被我剔除了。所以一定要用历史全样本,包括已经退市的股票。
分组回测的净值曲线长这样:
def group_backtest(factor_df, return_df, n_groups=10):
"""
分组回测主函数
"""
group_returns = pd.DataFrame(index=return_df.index,
columns=range(n_groups))
for date in factor_df.columns:
# 排序分组
ranked = factor_df[date].rank(method='first')
group = (ranked - 1) // (len(ranked) // n_groups)
group = group.clip(0, n_groups - 1)
# 计算每组收益
for g in range(n_groups):
mask = group == g
group_returns.loc[date, g] = return_df[date][mask].mean()
# 计算累计净值
nav = (1 + group_returns).cumprod()
return nav
三、多空组合收益:剥离市场影响
分组回测有个问题——它包含了市场整体的涨跌。如果某段时间市场大涨,所有组都涨,你很难判断因子是否真的有效。
多空组合就是解决这个问题的。做法很简单:做多第10组(因子值最大),做空第1组(因子值最小)。这样市场涨跌的影响就被对冲掉了,剩下的就是因子本身的选股能力。
我个人习惯把多空组合的年化收益、夏普比率、最大回撤都算出来。一个优秀的因子,多空组合年化收益应该在5%以上,夏普比率大于1。
关键指标:
- 年化收益:多空组合的几何平均年化收益
- 夏普比率:年化收益 / 年化波动
- 最大回撤:从峰值到谷底的最大跌幅
- 胜率:多空组合收益为正的月份占比
代码实现:
def long_short_analysis(group_nav):
"""
分析多空组合表现
group_nav: 分组净值,shape为(时间, 组别)
"""
# 多空组合净值
ls_nav = group_nav.iloc[:, -1] / group_nav.iloc[:, 0]
ls_returns = ls_nav.pct_change().dropna()
# 计算指标
annual_return = ls_nav.iloc[-1] ** (252 / len(ls_nav)) - 1
annual_vol = ls_returns.std() * np.sqrt(252)
sharpe = annual_return / annual_vol
# 最大回撤
cummax = ls_nav.cummax()
drawdown = (ls_nav - cummax) / cummax
max_dd = drawdown.min()
return {
'年化收益': f"{annual_return:.2%}",
'年化波动': f"{annual_vol:.2%}",
'夏普比率': f"{sharpe:.2f}",
'最大回撤': f"{max_dd:.2%}",
'胜率': f"{(ls_returns > 0).mean():.2%}"
}
四、换手率分析:因子的交易成本
很多人在单因子测试中忽略换手率,这是个大坑。一个因子即使IC很高、多空收益很好,但如果换手率太高,交易成本会吃掉大部分收益。
换手率的计算方式:
- 在每个调仓日,计算当前持仓与上一期持仓的差异
- 换手率 = 买入股票数量 + 卖出股票数量 / 总持仓数量
- 通常按双边计算,即买入和卖出都算
我建议把换手率分成两部分看:
- 绝对换手率:因子本身的换手率,反映因子的稳定性
- 净换手率:多空组合的换手率,反映交易成本
避坑指南:我曾经测试过一个高频因子,IC高达0.08,多空年化收益15%。但一算换手率,月均双边换手率超过200%。按单边千分之三的交易成本算,一年下来交易成本就把收益吃光了。所以后来我养成了习惯——先看换手率,再看收益。
换手率分析的代码:
def turnover_analysis(factor_df, n_groups=10, top_pct=0.1):
"""
分析因子换手率
"""
turnover = []
prev_top = None
for date in factor_df.columns:
# 取因子值最大的10%作为持仓
threshold = factor_df[date].quantile(1 - top_pct)
current_top = set(factor_df[date][factor_df[date] >= threshold].index)
if prev_top is not None:
# 计算换手率
turnover_rate = len(current_top - prev_top) / len(current_top)
turnover.append(turnover_rate)
prev_top = current_top
turnover = pd.Series(turnover, index=factor_df.columns[1:])
return {
'月均换手率': f"{turnover.mean():.2%}",
'换手率标准差': f"{turnover.std():.2%}",
'最大月换手率': f"{turnover.max():.2%}"
}
五、综合评估:四个维度缺一不可
好了,四个维度都讲完了。在实际项目中,我是怎么用的?
我会做一个综合评分表:
| 维度 | 优秀 | 一般 | 差 |
|---|---|---|---|
| IC均值 | > 0.05 | 0.02 ~ 0.05 | < 0.02 |
| IR | > 1.0 | 0.5 ~ 1.0 | < 0.5 |
| 多空年化 | > 10% | 5% ~ 10% | < 5% |
| 多空夏普 | > 1.5 | 1.0 ~ 1.5 | < 1.0 |
| 月均换手率 | < 30% | 30% ~ 60% | > 60% |
只有四个维度都达标,我才会考虑把这个因子纳入组合。嗯,这里要注意——不要只看单一指标。我见过IC很高但换手率爆炸的因子,也见过多空收益很好但分组不单调的因子。这些都有问题。
最后,用一张图总结整个单因子测试框架:
说白了,单因子测试就是给因子做「体检」。IC分析看相关性,分组回测看单调性,多空组合看超额收益,换手率看交易成本。四个维度都过了,这个因子才值得你花时间去优化和组合。
记住一句话:没有经过严格单因子测试的因子,都是耍流氓。
公众号:蓝海资料掘金营,微信deep3321