4. 单因子分析框架
单因子分析,说白了就是给每个候选因子做「体检」。
我见过不少新手,一口气挖了几百个因子,直接扔进模型里跑。结果呢?过拟合严重,实盘一塌糊涂。你想想看,连单个因子有没有预测能力都不清楚,组合起来怎么可能稳定?
所以,这一章我们老老实实把单因子分析的框架搭起来。我个人习惯把它拆成四个步骤:因子构建 → IC/IR 分析 → 分组回测 → 因子收益率计算。每一步都有坑,咱们一个一个踩过去。
核心观点:单因子分析不是为了选最好的因子,而是为了理解每个因子的「脾气秉性」——什么时候有效,什么时候失效,跟哪些因子相关。
4.1 因子构建流程
因子构建不是简单的「拿数据除一下」。我刚开始做的时候,直接从数据库拉出市盈率、市净率,算个倒数就当成因子了。结果回测曲线漂亮得不行,一查才发现——前向偏差,用了未来数据。
一个规范的因子构建流程,至少包含这几步:
- 数据清洗:去极值、补缺失、处理复权。尤其是复权,用错了等于白干。
- 因子计算:按公式算出原始因子值。注意截面和时序的区别。
- 标准化处理:包括去极值(MAD法或百分位法)、中性化(行业、市值)、标准化(Z-score)。
- 滞后对齐:确保因子值和未来收益严格对齐。T 日因子对应 T+1 日收益。
我的习惯:每次构建完因子,先做一次「未来数据检查」。具体做法是:随机挑几只股票,手动核对因子值和下期收益的时间戳。虽然笨,但管用。
举个例子,一个简单的动量因子构建代码大概长这样:
import pandas as pd
import numpy as np
def calc_momentum_factor(price_df, window=20):
"""
计算动量因子:过去 window 天的累计收益率
输入:price_df - 股票价格矩阵 (日期 x 股票)
输出:factor_df - 因子值矩阵
"""
# 计算收益率
ret = price_df.pct_change()
# 滚动求和
momentum = ret.rolling(window=window).sum()
# 滞后一期,避免前向偏差
momentum = momentum.shift(1)
return momentum
# 去极值(MAD法)
def winsorize_mad(factor, n=5):
median = factor.median()
mad = (factor - median).abs().median()
upper = median + n * mad
lower = median - n * mad
return factor.clip(lower, upper)
# 标准化
def standardize(factor):
return (factor - factor.mean()) / factor.std()
注意:去极值和标准化一定要在截面(每个时间点)上做,而不是在整个时序上做。否则会引入未来信息。我曾经在这个问题上栽过跟头,回测收益凭空多了 3%。
4.2 因子 IC / IR 分析
因子构建完了,怎么知道它有没有预测能力?IC(Information Coefficient)就是干这个的。
IC 的定义很简单:因子值与未来一期收益的相关系数。常用的是 Spearman 秩相关系数(Rank IC),因为它对极端值不敏感。
IR(Information Ratio)则是 IC 的均值除以标准差,衡量的是因子预测能力的稳定性。
| 指标 | 公式 | 含义 | 经验阈值 |
|---|---|---|---|
| Rank IC | ρ(rank(factor), rank(return)) | 因子排序与收益排序的相关性 | |IC| > 0.02 视为有预测力 |
| IC_IR | mean(IC) / std(IC) | IC 的「性价比」 | IC_IR > 0.5 算不错 |
| 累计 IC | cumsum(IC) | IC 的长期趋势 | 趋势向上且稳定 |
计算 IC 的代码也很直接:
def calc_rank_ic(factor_df, return_df):
"""
计算每日 Rank IC
factor_df: 因子值 (日期 x 股票)
return_df: 未来一期收益 (日期 x 股票)
"""
# 确保对齐
assert factor_df.index.equals(return_df.index)
assert factor_df.columns.equals(return_df.columns)
# 每日计算 Spearman 相关系数
ic_series = pd.Series(index=factor_df.index, dtype=float)
for date in factor_df.index:
f = factor_df.loc[date]
r = return_df.loc[date]
# 去掉缺失值
mask = f.notna() & r.notna()
if mask.sum() < 30: # 太少股票就算了
continue
ic_series[date] = f[mask].corr(r[mask], method='spearman')
return ic_series
# 计算 IR
ic = calc_rank_ic(factor, forward_return)
ic_ir = ic.mean() / ic.std()
print(f"IC 均值: {ic.mean():.4f}, IR: {ic_ir:.4f}")
我个人习惯把 IC 画成时间序列图,一眼就能看出因子有没有「失效期」。如果 IC 在某个时间段持续为负,说明因子发生了翻转,这时候就要警惕了。
4.3 分组回测(分层回测)
IC 分析告诉你因子「有没有用」,分组回测告诉你「怎么用」。
具体做法是:在每个时间截面,按因子值把股票分成 N 组(通常是 5 组或 10 组),等权或市值加权持有下一期。然后观察各组收益是否单调。
为什么强调单调性?
- 如果第 1 组到第 5 组收益严格递减,说明因子有很好的区分度。
- 如果中间有交叉,比如第 3 组比第 2 组收益还高,那因子可能不是线性的。
- 如果多空组合(第 1 组 - 第 5 组)收益显著为正,说明因子有超额收益。
关键点:分组回测一定要做「多空组合」的净值曲线。单看多头组合的收益,你分不清是因子贡献还是市场 Beta 贡献。多空组合剥离了市场风险,才是因子的真实 Alpha。
分组回测的代码框架:
def group_backtest(factor_df, return_df, n_groups=5):
"""
分层回测
返回:各组每日收益、多空组合收益
"""
group_returns = pd.DataFrame(index=return_df.index,
columns=range(n_groups))
for date in factor_df.index:
f = factor_df.loc[date]
r = return_df.loc[date]
# 分组
groups = pd.qcut(f, n_groups, labels=False, duplicates='drop')
# 计算每组等权收益
for g in range(n_groups):
mask = (groups == g)
if mask.sum() > 0:
group_returns.loc[date, g] = r[mask].mean()
# 多空组合:第0组 - 最后一组
long_short = group_returns[0] - group_returns[n_groups-1]
return group_returns, long_short
# 计算累计净值
group_ret, ls_ret = group_backtest(factor, forward_return)
ls_cum = (1 + ls_ret).cumprod()
避坑指南:我曾经遇到一个因子,分组收益完全单调,但多空组合的夏普比只有 0.3。为什么?因为多头和空头都在亏钱,只是多头亏得少一点。这种因子其实没有实际价值。所以,一定要同时看多头组合的绝对收益。
4.4 因子收益率计算
分组回测给了我们直观的收益表现,但还不够严谨。因子收益率,指的是剥离了其他风险因子之后,该因子带来的纯收益。
常用的方法有两种:
- Fama-MacBeth 回归法:两步法,先截面回归得到因子溢价,再时序平均。
- 多空组合回归法:把多空组合收益对市场因子、风格因子做回归,截距项就是纯因子收益。
我个人更推荐第二种,因为它更直观,而且容易做风险调整。
import statsmodels.api as sm
def pure_factor_return(ls_returns, market_returns, factor_exposures):
"""
计算纯因子收益(经市场风险调整后)
ls_returns: 多空组合收益序列
market_returns: 市场收益序列
factor_exposures: 其他风格因子暴露(如市值、动量)
"""
X = pd.DataFrame({
'market': market_returns,
'size': factor_exposures['size'],
'value': factor_exposures['value']
})
X = sm.add_constant(X)
y = ls_returns
model = sm.OLS(y, X, missing='drop').fit()
# 截距项就是纯因子收益
alpha = model.params['const']
t_stat = model.tvalues['const']
return alpha, t_stat, model
注意:因子收益率计算对频率很敏感。日频因子收益率通常很小,年化时需要乘以 sqrt(252)。但要注意,如果因子换手率很高,日频年化会高估实际收益。我一般同时看日频和周频的结果,取个保守值。
4.5 小结:单因子分析的「体检报告」
做完上面四步,你应该能产出一份完整的因子体检报告:
| 检查项 | 合格标准 | 常见问题 |
|---|---|---|
| IC 均值 | |IC| > 0.02,且显著不为零 | IC 太小,或正负摇摆 |
| IC_IR | > 0.5 | IR 太低,因子不稳定 |
| 分组单调性 | 各组收益严格单调 | 中间组交叉,非线性 |
| 多空收益 | 年化 > 5%,夏普 > 1 | 多空收益来自空头亏损 |
| 纯因子 Alpha | t 统计量 > 2 | Alpha 被其他因子解释 |
嗯,这套框架我用了好几年,从 A 股到,从日频到高频,基本都能覆盖。你刚开始做因子研究的时候,建议每个因子都走一遍这个流程。虽然慢,但踏实。
记住一句话:没有经过单因子检验的因子,不配进入多因子模型。