单因子测试框架:IC分析、分组回测、多空组合收益、因子收益率计算
单因子测试,说白了就是给每个候选因子做一次「体检」。
我做了这么多年量化,见过太多人拿着一个看起来不错的因子直接上实盘,结果亏得底裤都不剩。为什么?因为没有经过严格的单因子测试。
这一章,我们就来拆解这个体检流程。它包含四个核心模块:IC分析、分组回测、多空组合收益、因子收益率计算。
核心观点:单因子测试不是为了证明因子有效,而是为了找出它什么时候失效。
1. IC分析:因子的预测能力有多强?
IC,全称是Information Coefficient,信息系数。它衡量的是因子值与未来收益之间的相关性。
我个人习惯用Rank IC,也就是秩相关系数。为什么?因为它对极端值不敏感。你想想看,A股市场经常有妖股,一个涨停就把普通相关系数拉偏了,Rank IC就不会。
小技巧:IC的绝对值在0.03以上就算有预测能力了。别追求0.1以上,那种因子要么是过拟合,要么是数据出错了。
具体计算很简单。每个月末,我们计算因子值与下个月收益率的Spearman相关系数。然后得到一个IC序列。
import pandas as pd
import numpy as np
from scipy.stats import spearmanr
def calc_rank_ic(factor_series, return_series):
"""
计算截面Rank IC
factor_series: 某期因子值
return_series: 对应下期收益率
"""
# 去掉缺失值
mask = factor_series.notna() & return_series.notna()
f = factor_series[mask]
r = return_series[mask]
if len(f) < 30:
return np.nan
ic, p_value = spearmanr(f, r)
return ic
# 月度IC计算示例
monthly_ic = []
for date in factor_data.index.get_level_values('date').unique():
factor_slice = factor_data.xs(date, level='date')['factor']
return_slice = return_data.xs(date, level='date')['ret']
ic = calc_rank_ic(factor_slice, return_slice)
monthly_ic.append({'date': date, 'ic': ic})
ic_df = pd.DataFrame(monthly_ic)
print(f"IC均值: {ic_df['ic'].mean():.4f}")
print(f"IC标准差: {ic_df['ic'].std():.4f}")
print(f"ICIR: {ic_df['ic'].mean() / ic_df['ic'].std():.4f}")
这里有个关键指标——ICIR,也就是IC的均值除以标准差。它衡量的是IC的稳定性。ICIR大于0.5算合格,大于1.0算优秀。
我曾经踩过的坑:有一次我测了一个因子,IC均值0.06,看起来不错。但ICIR只有0.2,说明IC波动极大。后来回测发现,这个因子在牛市中有效,熊市中完全失效。如果只看IC均值,就会被误导。
2. 分组回测:因子能不能区分好坏股票?
IC分析告诉你因子有没有预测能力,但没告诉你它能不能用来选股。分组回测就是干这个的。
做法很简单:每个月把所有股票按因子值从大到小排序,分成5组或10组。然后计算每组下个月的等权或市值加权收益。
def group_backtest(factor_df, return_df, n_groups=5):
"""
分组回测
factor_df: 因子值,index=[date, stock]
return_df: 收益率,index=[date, stock]
"""
results = []
for date in factor_df.index.get_level_values('date').unique():
# 获取当期因子
f = factor_df.xs(date, level='date')['factor']
# 获取下期收益
next_date = get_next_trading_date(date)
r = return_df.xs(next_date, level='date')['ret']
# 合并
combined = pd.DataFrame({'factor': f, 'ret': r}).dropna()
# 分组
combined['group'] = pd.qcut(combined['factor'], n_groups,
labels=range(n_groups))
# 计算每组平均收益
group_ret = combined.groupby('group')['ret'].mean()
group_ret['date'] = date
results.append(group_ret)
result_df = pd.DataFrame(results).set_index('date')
return result_df
# 运行回测
group_returns = group_backtest(factor_df, return_df, n_groups=5)
# 计算累计收益
cum_returns = (1 + group_returns).cumprod()
好的因子,分组收益应该是单调的。也就是说,第1组收益最高,第5组收益最低,中间依次排列。
判断标准:
- 单调性明显:优秀因子
- 部分单调:可用,但需注意
- 完全无序:放弃吧
3. 多空组合收益:对冲掉市场风险
分组回测有个问题:它包含了市场整体的涨跌。比如牛市里所有组都涨,熊市里所有组都跌,你很难判断因子本身的能力。
多空组合就是解决这个问题的。我们做多第1组(因子值最大的组),做空第5组(因子值最小的组)。这样市场涨跌的影响就被对冲掉了。
def long_short_portfolio(group_returns):
"""
计算多空组合收益
group_returns: 分组回测结果,列名为0,1,2,3,4
"""
# 做多第0组,做空第4组
ls_returns = group_returns[0] - group_returns[4]
# 统计指标
annual_return = ls_returns.mean() * 252
annual_vol = ls_returns.std() * np.sqrt(252)
sharpe = annual_return / annual_vol
return ls_returns, sharpe
ls_ret, sharpe = long_short_portfolio(group_returns)
print(f"多空组合年化收益: {annual_return:.2%}")
print(f"多空组合夏普比: {sharpe:.2f}")
多空组合的夏普比如果超过1.0,说明这个因子有很强的选股能力。我见过最好的因子夏普比能到2.0以上,但那种因子通常很快就会被市场发现,然后失效。
注意:多空组合在实盘中很难完全复制,因为做空有成本,而且很多小股票融不到券。但作为因子评价工具,它非常有效。
4. 因子收益率计算:Fama-MacBeth回归
最后一个环节,也是最学术化的一个——计算因子收益率。
这里用的是Fama-MacBeth两步法。第一步,每期做截面回归,得到因子载荷;第二步,对因子载荷的时间序列做统计推断。
import statsmodels.api as sm
def fama_macbeth(factor_df, return_df, control_factors=None):
"""
Fama-MacBeth回归
factor_df: 待测因子
return_df: 收益率
control_factors: 控制变量,如市值、估值等
"""
# 第一步:截面回归
betas = []
for date in factor_df.index.get_level_values('date').unique():
# 准备数据
f = factor_df.xs(date, level='date')
r = return_df.xs(date, level='date')
X = sm.add_constant(f)
if control_factors is not None:
c = control_factors.xs(date, level='date')
X = pd.concat([X, c], axis=1)
# 回归
model = sm.OLS(r, X, missing='drop')
result = model.fit()
betas.append(result.params)
beta_df = pd.DataFrame(betas)
# 第二步:时间序列统计
factor_return = beta_df['factor'].mean()
t_stat = beta_df['factor'].mean() / beta_df['factor'].std() * np.sqrt(len(beta_df))
return factor_return, t_stat, beta_df
# 运行
factor_ret, t_stat, _ = fama_macbeth(factor_df, return_df)
print(f"因子收益率: {factor_ret:.4%}")
print(f"t统计量: {t_stat:.2f}")
t统计量的绝对值大于2,说明因子收益率显著不为零。我个人习惯看t值大于3才放心,因为A股的数据质量参差不齐,t值2可能只是运气。
我曾经犯过的错:刚开始做因子测试时,我直接跑Fama-MacBeth,没加控制变量。结果发现一个因子t值高达5,兴奋得不行。后来加了市值和估值作为控制变量,t值直接掉到1.2。原来这个因子只是市值因子的代理变量。
总结:四个环节缺一不可
单因子测试不是走流程,而是帮你理解因子的本质。
- IC分析:看因子有没有预测能力
- 分组回测:看因子能不能区分股票
- 多空组合:看因子能不能独立赚钱
- 因子收益率:看因子是不是真的显著
这四个环节做完,一个因子能不能用,你心里就有数了。
我的建议:每个因子至少做3年的回测,覆盖不同的市场环境。牛市、熊市、震荡市都跑一遍,你才能知道这个因子到底靠不靠谱。
嗯,单因子测试就讲到这里。下一章我们会聊多因子组合,那才是真正上强度的部分。