3、单因子测试框架:IC分析、分组回测、多空组合收益、因子收益率序列构建
做量化投资的朋友都知道,因子研究是策略的基石。但说实话,很多人拿到一个因子,直接就跑回测,结果过拟合得一塌糊涂。我个人习惯是,先老老实实把单因子测试走一遍。今天我就把这套框架掰开揉碎了讲给你听。
3.1 为什么需要单因子测试?
你想想看,市场上成千上万个因子,哪个是真金,哪个是垃圾?单因子测试就是那个试金石。它的核心目的就三个:
- 判断因子是否有效——能不能稳定预测收益
- 判断因子是否独立——别跟已有因子高度相关
- 判断因子是否稳健——换个时间段、换个股票池还行不行
我在项目中遇到过最坑的事,就是有个因子在2015-2017年表现极好,结果2018年直接崩了。后来一查,原来是跟小市值因子高度相关,风格一切换就完蛋。所以,单因子测试不是走过场,是保命的步骤。
3.2 IC分析:因子的预测能力
IC(Information Coefficient)是衡量因子预测能力的核心指标。说白了,就是看因子值和未来收益之间的相关性。
3.2.1 什么是IC?
IC有两种主流计算方式:
| IC类型 | 计算方法 | 适用场景 |
|---|---|---|
| Pearson IC | 因子值与收益的线性相关系数 | 因子值呈正态分布时 |
| Spearman IC(Rank IC) | 因子值排名与收益排名的秩相关系数 | 因子值有极端值或非线性关系时 |
我个人更偏爱Rank IC。为什么?因为A股市场极端值太多了,一个涨停板就能把Pearson IC拉偏。Rank IC只看排序,稳健得多。
3.2.2 IC的计算与解读
来看一段代码,这是我在实际项目中用的IC计算函数:
import pandas as pd
import numpy as np
from scipy.stats import spearmanr
def calc_ic(factor_series, return_series, method='spearman'):
"""
计算截面IC
factor_series: 某期因子值,index为股票代码
return_series: 对应未来N期收益,index为股票代码
"""
# 对齐数据
common_idx = factor_series.index.intersection(return_series.index)
f = factor_series.loc[common_idx]
r = return_series.loc[common_idx]
# 去极值、中性化处理(略)
if method == 'spearman':
ic, p_value = spearmanr(f, r)
else:
ic = f.corr(r, method='pearson')
p_value = None
return ic, p_value
# 滚动计算IC序列
def rolling_ic(factor_df, return_df, window=12):
"""
计算滚动IC均值与标准差
factor_df: 多期因子值,index为时间,columns为股票
return_df: 多期收益,index为时间,columns为股票
"""
ic_list = []
for dt in factor_df.index:
ic, _ = calc_ic(factor_df.loc[dt], return_df.loc[dt])
ic_list.append(ic)
ic_series = pd.Series(ic_list, index=factor_df.index)
return ic_series
解读IC时,我一般看这几个维度:
- IC均值:绝对值大于0.02才算有点意思,大于0.05算不错
- IC标准差:越小越好,说明预测稳定
- ICIR(IC信息比率):IC均值/IC标准差,大于0.5算合格,大于1算优秀
- IC胜率:IC为正的期数占比,大于60%说明方向判断靠谱
核心经验:IC均值高但ICIR低,说明因子时灵时不灵,这种因子做组合时容易翻车。我曾经吃过这个亏,一个IC均值0.06的因子,ICIR只有0.3,实盘时回撤大得吓人。
3.3 分组回测:直观验证因子效果
IC分析告诉你因子有没有预测能力,但分组回测告诉你这个能力能不能转化成收益。说白了,就是看按因子值分组后,各组收益是不是单调的。
3.3.1 分组方法
我常用的分组方式有两种:
- 等分法:按因子值从小到大分成N组(通常5组或10组)
- 分位数法:按因子值分位数分组,比如前20%、后20%
分组时有个坑要注意——行业中性化。如果不做行业中性化,分组结果可能只是行业配置的体现。比如你分出的第1组全是银行股,第5组全是科技股,那收益差异可能来自行业而非因子本身。
3.3.2 分组回测代码示例
def group_backtest(factor_df, return_df, n_groups=5):
"""
分组回测主函数
factor_df: 因子值矩阵,index为时间,columns为股票
return_df: 收益矩阵,index为时间,columns为股票
"""
group_returns = {}
for dt in factor_df.index:
# 获取当期因子值
f = factor_df.loc[dt].dropna()
r = return_df.loc[dt]
# 按因子值分组
f_rank = f.rank()
group_labels = pd.qcut(f_rank, n_groups, labels=range(n_groups))
# 计算每组等权收益
for g in range(n_groups):
stocks_in_group = group_labels[group_labels == g].index
if len(stocks_in_group) > 0:
ret = r[stocks_in_group].mean()
group_returns.setdefault(g, []).append(ret)
# 汇总为DataFrame
result = pd.DataFrame(group_returns)
result.index = factor_df.index
result.columns = [f'Group_{i+1}' for i in range(n_groups)]
# 计算累计收益
cum_returns = (1 + result).cumprod()
return result, cum_returns
3.3.3 分组结果解读
好的分组回测应该满足:
- 单调性:Group_1到Group_N的收益严格递增或递减
- 区分度:最高组和最低组的收益差距明显
- 稳定性:换仓期数越多,单调性保持得越好
避坑指南:我曾经遇到一个因子,分组收益看起来单调性很好,但仔细一看,最高组和最低组都跑赢基准,中间组反而跑输。这就是典型的U型分布,说明因子可能捕捉到了某种非线性特征,需要进一步分析。
3.4 多空组合收益:因子的纯收益
分组回测看的是绝对收益,但多空组合看的是相对收益。做多最高组、做空最低组,得到的收益曲线剔除了市场整体走势的影响,更能反映因子本身的选股能力。
3.4.1 多空组合构建
构建多空组合时,我一般会注意几个细节:
- 等权配置:多空两边都等权,避免个别股票主导
- 市值中性化:确保多空两边的市值暴露一致
- 行业中性化:确保多空两边的行业暴露一致
def long_short_portfolio(factor_df, return_df, top_pct=0.2, bottom_pct=0.2):
"""
构建多空组合
top_pct: 做多比例(前20%)
bottom_pct: 做空比例(后20%)
"""
ls_returns = []
for dt in factor_df.index:
f = factor_df.loc[dt].dropna()
r = return_df.loc[dt]
# 取前20%和后20%
n = len(f)
n_long = int(n * top_pct)
n_short = int(n * bottom_pct)
sorted_f = f.sort_values()
long_stocks = sorted_f.index[-n_long:]
short_stocks = sorted_f.index[:n_short]
# 等权收益
long_ret = r[long_stocks].mean()
short_ret = r[short_stocks].mean()
# 多空收益 = 做多收益 - 做空收益
ls_ret = long_ret - short_ret
ls_returns.append(ls_ret)
ls_series = pd.Series(ls_returns, index=factor_df.index)
return ls_series
3.4.2 多空组合的评估指标
评估多空组合时,我主要看:
- 年化收益率:一般5%以上算合格,10%以上算优秀
- 夏普比率:大于1算不错,大于2算顶级
- 最大回撤:多空组合理论上应该低回撤,超过10%就要警惕
- 胜率:月度胜率大于60%说明因子稳定
注意:多空组合收益高不代表因子一定能赚钱。因为实盘中做空成本很高,融券费率、冲击成本都会吃掉收益。我一般会在多空收益基础上扣除1-2%的年化成本,再判断因子是否值得实盘。
3.5 因子收益率序列构建
因子收益率序列是因子投资中最核心的概念之一。它把因子看作一个可交易的资产,用回归的方法剥离出因子的纯收益。
3.5.1 什么是因子收益率?
因子收益率,说白了就是单位因子暴露带来的超额收益。它通过横截面回归得到:
Ri = α + βi × fi + εi
其中Ri是股票i的超额收益,fi是股票i的因子值,βi就是因子收益率。
3.5.2 因子收益率计算
import statsmodels.api as sm
def calc_factor_return(factor_df, return_df, risk_free_rate=0.0):
"""
计算每期因子收益率
使用横截面回归:R_i = alpha + beta * factor_i + epsilon_i
"""
factor_returns = []
for dt in factor_df.index:
f = factor_df.loc[dt].dropna()
r = return_df.loc[dt] - risk_free_rate / 252 # 日度无风险利率
# 对齐数据
common = f.index.intersection(r.index)
X = sm.add_constant(f[common])
y = r[common]
# OLS回归
model = sm.OLS(y, X).fit()
factor_ret = model.params.iloc[1] # 因子收益率
factor_returns.append(factor_ret)
return pd.Series(factor_returns, index=factor_df.index)
3.5.3 因子收益率序列的应用
有了因子收益率序列,我们可以做很多事情:
- 构建因子组合:直接按因子收益率做多空
- 因子择时:根据因子收益率的历史走势判断当前是否适合使用该因子
- 因子归因:分析因子收益率的来源,是来自市场、行业还是风格
个人经验:因子收益率序列比多空组合收益更纯粹。因为多空组合受分组方式影响,而因子收益率是通过回归得到的,理论上更接近因子的真实收益。我一般两个都看,互相验证。
3.6 本章知识体系
下面这张图总结了单因子测试的完整流程,你可以把它当作一个检查清单:
这张图把单因子测试的流程串起来了。从原始因子出发,经过预处理,然后从IC分析、分组回测、多空组合三个维度验证,最后汇总成因子收益率序列和评价报告。每一步都有它存在的意义,缺一不可。
好了,单因子测试框架就讲到这里。这套方法我用了好几年,踩过不少坑,也积累了一些心得。希望你能在实际项目中用起来,少走弯路。