4、单因子构建与测试:因子定义与计算、分组回测框架、多空组合收益分析、因子IC/IR分析
好,咱们进入正题。单因子测试,是整个因子投资体系的基石。说白了,就是你要先证明你手里这把“刀”是锋利的,才能拿去上战场。我见过太多人,因子逻辑讲得天花乱坠,一跑回测就露馅。所以,这一章咱们把地基打牢。
4.1 因子定义与计算:从想法到数字
一个因子,本质上就是一个数学公式。它把股票的各种原始数据(价格、成交量、财务数据)加工成一个有预测能力的数字。
因子的三大要素:
- 逻辑性:为什么这个因子能预测收益?你得有个故事。比如“低波动异象”,逻辑是散户喜欢追高,导致高波动股票被高估。
- 可计算性:数据要能拿到,计算要能复现。别搞个需要手工整理的数据,那没法量化。
- 单调性:因子值越大,未来收益应该越高(或越低),而不是忽高忽低。
举个例子,一个经典的“反转因子”:
# 因子定义:过去20个交易日的累计收益率
# 因子值越大,代表过去涨得越多,预期未来会下跌(反转)
import pandas as pd
import numpy as np
def calc_reversal_factor(close_price, window=20):
"""
close_price: DataFrame, 行是日期,列是股票代码
"""
# 计算过去window天的累计收益率
ret = close_price.pct_change(window)
# 这里我习惯取负值,让因子值越大,预期收益越高
factor = -ret
return factor
# 使用示例
# factor_df = calc_reversal_factor(df_close, 20)
4.2 分组回测框架:把股票装进篮子里
因子算出来了,怎么验证它有效?最直观的方法就是分组回测。把股票按因子值从大到小排序,分成10组(Decile)或5组(Quintile),然后看每组未来的收益表现。
我画了个流程图,帮你理解这个框架:
框架不复杂,但细节决定成败。我分享一段核心代码:
def group_backtest(factor_df, ret_df, groups=10):
"""
分组回测核心函数
factor_df: 因子值,行日期,列股票
ret_df: 未来N日收益,行日期,列股票
"""
# 对齐日期和股票
common_dates = factor_df.index.intersection(ret_df.index)
common_stocks = factor_df.columns.intersection(ret_df.columns)
factor_df = factor_df.loc[common_dates, common_stocks]
ret_df = ret_df.loc[common_dates, common_stocks]
# 逐日分组
group_returns = []
for date in common_dates:
# 获取当天的因子值和收益
factor_today = factor_df.loc[date].dropna()
ret_today = ret_df.loc[date]
# 按因子值排序,分成groups组
sorted_factors = factor_today.sort_values()
group_size = len(sorted_factors) // groups
for g in range(groups):
start_idx = g * group_size
end_idx = (g + 1) * group_size if g < groups - 1 else len(sorted_factors)
group_stocks = sorted_factors.index[start_idx:end_idx]
# 计算该组等权收益
group_ret = ret_today[group_stocks].mean()
group_returns.append({
'date': date,
'group': g + 1,
'return': group_ret
})
result = pd.DataFrame(group_returns)
return result
4.3 多空组合收益分析:真正的Alpha在哪?
分组回测看的是单调性。但真正衡量因子选股能力的,是多空组合收益。也就是:做多因子值最高的组,做空因子值最低的组,看这个组合能赚多少钱。
为什么这个指标重要?因为它剥离了市场整体涨跌的影响。你想想看,如果市场涨了20%,你分组全都涨,那说明不了什么。但多空组合的收益,才是因子真正的Alpha。
计算多空收益很简单:
# 假设group_returns是分组回测的结果
# 计算多空组合收益
long_short = group_returns[group_returns['group'] == groups].copy()
long_short['return'] = long_short['return'] - group_returns[group_returns['group'] == 1]['return'].values
long_short['cumulative'] = (1 + long_short['return']).cumprod()
# 年化收益率
annual_ret = long_short['cumulative'].iloc[-1] ** (252 / len(long_short)) - 1
# 年化波动率
annual_vol = long_short['return'].std() * np.sqrt(252)
# 夏普比率
sharpe = annual_ret / annual_vol
print(f"多空组合年化收益: {annual_ret:.2%}")
print(f"多空组合夏普比率: {sharpe:.2f}")
我个人习惯,多空组合的夏普比率至少要大于1,我才会考虑把这个因子纳入模型。低于0.5的,基本就是噪音。
4.4 因子IC/IR分析:从收益到相关性
分组回测有个缺点:它把连续变量离散化了。有时候分组收益单调,但组内差异很大。这时候就需要IC(信息系数)和IR(信息比率)来帮忙。
IC:因子值与未来收益的截面相关系数。常用Spearman秩相关系数,因为它对异常值不敏感。
IR:IC的均值除以IC的标准差。衡量因子预测能力的稳定性。
计算代码:
def calc_ic_ir(factor_df, ret_df):
"""
计算每日IC和IR
"""
common_dates = factor_df.index.intersection(ret_df.index)
ic_series = []
for date in common_dates:
factor_today = factor_df.loc[date].dropna()
ret_today = ret_df.loc[date]
# 取共同股票
common = factor_today.index.intersection(ret_today.index)
if len(common) < 30: # 股票太少,IC不稳定
continue
factor_values = factor_today[common]
ret_values = ret_today[common]
# 计算Spearman相关系数
from scipy.stats import spearmanr
ic, _ = spearmanr(factor_values, ret_values)
ic_series.append(ic)
ic_series = pd.Series(ic_series, index=common_dates[:len(ic_series)])
# 计算IR
ic_mean = ic_series.mean()
ic_std = ic_series.std()
ir = ic_mean / ic_std if ic_std != 0 else 0
return ic_series, ic_mean, ir
# 使用示例
# ic_series, ic_mean, ir = calc_ic_ir(factor_df, ret_df)
# print(f"IC均值: {ic_mean:.4f}, IR: {ir:.2f}")
最后,我把这四个维度的评价标准整理成一张表,方便你对照:
| 评价维度 | 优秀 | 及格 | 不合格 |
|---|---|---|---|
| 分组单调性 | 严格单调递增/递减 | 基本单调,有1-2组偏离 | 杂乱无章,无规律 |
| 多空夏普 | > 1.5 | 0.8 - 1.5 | < 0.5 |
| IC均值 | > 0.05 | 0.02 - 0.05 | < 0.02 |
| IR | > 1.0 | 0.5 - 1.0 | < 0.3 |
嗯,到这里,单因子测试的完整流程就讲完了。从因子定义,到分组回测,再到多空分析和IC/IR分析,每一步都有坑。我当年刚开始做的时候,光数据对齐就折腾了两天。但只要你把框架搭好,后面就是流水线作业了。