因子研究是多因子选股的核心环节:从原始数据里构造出对未来收益有预测力的变量,然后检验它是否真的有效。它的数学门槛不高——IC 就是一个相关系数——但统计陷阱极多。一个研究员在一年里可能测试上千个因子,其中纯粹因为运气而「显著」的就有几十个。
真正困难的地方是在噪声中区分信号。市场数据的信噪比极低:一个真实有效的因子,IC 可能只有 0.03~0.05,而随机噪声的 IC 在 0 附近波动 ±0.02。这意味着你需要大量的样本和严格的检验流程,才能确认一个因子不是运气。
本文按「构造 → 预处理 → 评估 → 陷阱 → 合成 → 工程化」的顺序展开。回测框架本身的正确性问题在 回测框架设计与前视偏差 中讨论,本文聚焦因子层面的方法论。
目录
- 因子的分类与来源
- 数据预处理:去极值与标准化
- 中性化:剥离行业与市值
- IC 与 RankIC 评估
- 分层回测与单调性
- 因子衰减与换手率
- 多重检验与因子挖掘陷阱
- 正交化与因子合成
- 因子库工程化
1. 因子的分类与来源
因子按数据来源分四类,每类的信噪比和衰减速度差异很大:
| 类别 | 例子 | 衰减速度 | 拥挤度 |
|---|---|---|---|
| 量价因子 | 动量、反转、波动率 | 快(天~周) | 极高 |
| 基本面因子 | 估值、成长、盈利质量 | 慢(月~季) | 高 |
| 分析师因子 | 预期调整、评级变化 | 中 | 中 |
| 另类因子 | 舆情、供应链、卫星 | 中 | 低 |
量价因子最容易构造也最拥挤,一个经典的动量因子在 A 股的有效性已经被大量资金磨平。另类因子的门槛在数据获取,而不是计算。
因子构造的基本形式是某个量对另一个量的归一化:
def momentum(close, window=20, skip=1):
return close.shift(skip) / close.shift(window) - 1 # 跳过最近 1 日避免反转污染
def volatility(close, window=20):
ret = close.pct_change()
return ret.rolling(window).std() * (252 ** 0.5) # 年化波动率
def turnover_ratio(volume, float_share, window=20):
return (volume / float_share).rolling(window).mean()
构造因子时最重要的是可解释性:如果说不清这个因子为什么有效,它大概率是过拟合的产物。
2. 数据预处理:去极值与标准化
原始因子值不能直接使用,必须经过三步预处理:
| 步骤 | 目的 | 常用方法 |
|---|---|---|
| 去极值 | 消除异常值影响 | MAD、3σ、分位数 |
| 标准化 | 统一量纲 | z-score、排序 |
| 缺失值 | 处理停牌/新股 | 行业中位数填充 |
MAD 去极值(中位数绝对偏差)比 3σ 更稳健,因为它不受极端值本身影响:
import numpy as np
def mad_winsorize(s, n=5):
med = s.median()
mad = (s - med).abs().median()
if mad == 0:
return s
upper = med + n * 1.4826 * mad # 1.4826 使 MAD 与标准差可比
lower = med - n * 1.4826 * mad
return s.clip(lower, upper)
def zscore(s):
return (s - s.mean()) / s.std()
factor_clean = zscore(mad_winsorize(raw_factor)) # 完整流程:去极值 → 标准化
1.4826 这个常数是 MAD 到标准差的换算系数(正态分布下 σ ≈ 1.4826 × MAD),用它可以沿用「几倍标准差」的直觉。
3. 中性化:剥离行业与市值
几乎所有量价因子都带有市值暴露。小市值因子在 A 股长期有效,但它同时也是「小市值风险」而不是纯 alpha。中性化的目的是把因子中可被行业和市值解释的部分剔除。
import statsmodels.api as sm
def neutralize(factor, industry_dummies, log_mktcap):
X = pd.concat([industry_dummies, log_mktcap], axis=1) # 行业哑变量 + 对数市值
X = sm.add_constant(X)
mask = factor.notna() & X.notna().all(axis=1)
model = sm.OLS(factor[mask], X[mask]).fit()
return model.resid.reindex(factor.index)
中性化后的因子才是「纯 alpha」。检验中性化效果的标准是:中性化前后的 IC 差值不应超过 30%。如果中性化后 IC 腰斩,说明原来的因子主要靠市值暴露赚钱。
| 中性化维度 | 何时需要 |
|---|---|
| 行业 | 几乎所有因子 |
| 市值 | 量价因子必做 |
| 风格(BETA/成长) | 组合层面做 |
| 不做中性化 | 明确要做风格暴露时 |
4. IC 与 RankIC 评估
IC(信息系数)是因子值与未来收益的截面相关系数,是因子有效性的第一指标:
def calc_ic(factor, forward_return, method='spearman'):
ic = factor.corrwith(forward_return, axis=1, method=method)
return ic # factor 与 forward_return 同形状:index=日期,columns=股票
ic_series = calc_ic(factor_clean, fwd_ret_5d, method='spearman') # RankIC
print(f"IC 均值: {ic_series.mean():.4f}")
print(f"ICIR: {ic_series.mean() / ic_series.std():.3f}")
print(f"IC 胜率: {(ic_series > 0).mean():.2%}")
评估标准(日频因子):
| 指标 | 合格线 | 优秀 |
|---|---|---|
| IC 均值 | > 0.02 | > 0.05 |
| ICIR | > 0.3 | > 0.5 |
| IC 胜率 | > 55% | > 60% |
| t 统计量 | > 2 | > 3 |
用 RankIC(Spearman)而不是 Pearson 的原因:收益分布有厚尾,秩相关对异常值不敏感。
t 统计量的计算要注意自相关调整:日频 IC 序列高度自相关,直接用 mean/std × sqrt(n) 会严重高估显著性。
def newey_west_t(ic_series, lags=5): # 用 Newey-West 调整自相关后的 t 值
from statsmodels.regression.linear_model import OLS
import statsmodels.api as sm
x = np.ones(len(ic_series))
model = OLS(ic_series.values, x).fit(cov_type='HAC', cov_kwds={'maxlags': lags})
return model.tvalues[0]
5. 分层回测与单调性
把股票按因子值分成 N 组,看各组的收益是否有单调关系。这是比 IC 更直观的检验:
def layered_backtest(factor, fwd_ret, n_groups=5):
ranks = factor.rank(axis=1, pct=True)
layers = np.ceil(ranks * n_groups).clip(1, n_groups)
result = {}
for g in range(1, n_groups + 1):
mask = (layers == g)
result[f'G{g}'] = (fwd_ret * mask).sum(axis=1) / mask.sum(axis=1)
return pd.DataFrame(result) # 输出 G1~G5 年化收益,理想情况单调递增
合格的因子应该满足:
- 单调性:G1 < G2 < G3 < G4 < G5(或反向),不能是 U 形。
- 多空收益:G5 − G1 的年化收益 > 10%,Sharpe > 1。
- 分组差异:相邻两组的差异稳定,不能只有首尾两组有差异。
U 形分布往往意味着因子捕捉的是「极端值效应」而不是线性关系,直接用线性组合会失效,需要做非线性变换。
6. 因子衰减与换手率
因子的预测力随时间衰减,衰减速度决定了换仓频率:
def ic_decay(factor, close, max_lag=20):
decay = {}
for lag in range(1, max_lag + 1):
fwd = close.shift(-lag) / close - 1
decay[lag] = calc_ic(factor, fwd).mean()
return pd.Series(decay) # 输出 1~20 日 IC,观察半衰期
| 因子类型 | IC 半衰期 | 建议换仓 |
|---|---|---|
| 短期反转 | 1~3 天 | 日频 |
| 动量 | 5~20 天 | 周频 |
| 估值 | 1~3 月 | 月频 |
| 质量 | 3~12 月 | 季频 |
换手率是因子实用性的隐性约束。一个 IC 很高但每天换手 200% 的因子,在扣除成本后可能完全不赚钱。计算因子的「成本后收益」:
def net_alpha(ic_series, turnover, cost_bps=15):
gross = ic_series.mean() * 252 # 粗略的年化 IC 贡献
cost = turnover * cost_bps / 10000 * 252
return gross - cost
7. 多重检验与因子挖掘陷阱
这是因子研究最容易翻车的地方。假设你测试 1000 个随机因子,用 t > 2 的标准筛选,会有约 50 个「显著」——纯属偶然。
| 陷阱 | 表现 | 后果 |
|---|---|---|
| 多重检验 | 测了 1000 个选最好的 | 假阳性率极高 |
| p-hacking | 微调参数直到显著 | 过拟合 |
| 幸存者偏差 | 只报告成功的因子 | 高估整体 |
| 数据窥探 | 反复看同一段数据 | 样本外失效 |
| 因子拥挤 | 已知因子被大量资金使用 | 收益衰减 |
多重检验的校正是必须的。Bonferroni 最简单但过于保守,**FDR(错误发现率)**更实用:
from statsmodels.stats.multitest import multipletests
p_values = np.array([test_factor(f)[1] for f in factors])
reject, p_adj, _, _ = multipletests(p_values, alpha=0.05, method='fdr_bh')
print(f"原始显著: {(p_values < 0.05).sum()}, 校正后显著: {reject.sum()}")
经验数据:一个团队一年测 1000 个因子,经 FDR 校正后「真显著」的通常只有 5~20 个。这意味着因子的发现效率远低于直觉。
8. 正交化与因子合成
多个相关因子直接相加会重复计算同一个信号。两种处理方式:
def orthogonalize(factor, base_factors):
X = sm.add_constant(base_factors)
return sm.OLS(factor, X).fit().resid # 取残差作为新因子
def symmetric_orthogonalize(factors): # 对称正交:结果与处理顺序无关
cov = factors.cov()
vals, vecs = np.linalg.eigh(cov)
D = np.diag(1 / np.sqrt(vals))
W = vecs @ D @ vecs.T
return factors @ W
对称正交优于顺序正交,因为它的结果不依赖因子处理顺序(顺序正交的第一个因子保持原样,后面的被改造)。
合成方式对比:
| 方式 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| 等权 | 标准化后平均 | 稳健、无需估计 | 未考虑 IC 差异 |
| IC 加权 | 按 IC 均值加权 | 突出有效因子 | IC 不稳定 |
| ICIR 加权 | 按 IC/σ 加权 | 兼顾稳定性 | 需要长样本 |
| 最大化 ICIR | 数值优化 | 理论最优 | 易过拟合 |
实践中等权 + ICIR 加权是较稳的起点。最大化 ICIR 的优化结果往往在样本外表现糟糕,因为它把噪声也拟合了。
9. 因子库工程化
当因子数量增长到几十上百个,就需要工程化管理:
factor_lib/
├── definitions/ # 因子定义(代码即文档)
│ ├── momentum_20d.py
│ └── turnover_std_20d.py
├── computed/ # 计算结果(Parquet 分区存储)
│ └── 2026/10/
├── metadata.yaml # 因子元信息
└── tests/ # 因子单元测试
元信息表要记录每个因子的血缘与状态:
momentum_20d:
category: price_volume
formula: "close.shift(1) / close.shift(20) - 1"
ic_mean: 0.031
icir: 0.42
turnover: 0.35
last_updated: 2026-10-01
status: active
owner: research_team
因子计算结果的存储量随「因子数 × 股票数 × 天数」增长,一个 200 因子 × 5000 股票 × 10 年的因子库约 10^9 个浮点数,必须用列式存储。查询与聚合的性能优化思路可以参考 数据库查询优化 ;如果需要更快的分析型查询,可以评估 NewSQL 数据库对比 里的列存引擎。
权衡取舍
| 维度 | 简单因子 | 复杂因子 |
|---|---|---|
| 可解释性 | 高 | 低 |
| 过拟合风险 | 低 | 高 |
| 拥挤度 | 高 | 低 |
| 稳定性 | 高 | 不确定 |
因子数量不是越多越好。一个有效的因子库通常有 20~50 个低相关因子,而不是 500 个高度相关的因子。增加一个与现有因子相关性 0.9 的新因子,对组合的边际贡献接近于零,但增加了维护成本和过拟合风险。
中性化上也有取舍:完全中性化会剥离掉真实的风格收益(如小市值溢价),不中性化则无法判断因子的纯 alpha 贡献。折中做法是研究阶段中性化,组合阶段显式决定风格暴露。
常见坑清单
- 不做去极值:一个 100 倍 PE 的股票会主导标准化结果,必须 MAD 或分位数处理。
- 用 Pearson IC:收益厚尾,应该用 Spearman 的 RankIC。
- 忽略自相关算 t 值:日频 IC 高度自相关,必须用 Newey-West 调整。
- 不做行业市值中性化:量价因子的收益大多来自市值暴露而非 alpha。
- 只看 IC 不看换手:高换手因子扣成本后可能为负。
- 测完因子就扔进组合:未做正交化,重复计算同一信号。
- 不做多重检验校正:测 1000 个因子必然有几十个假阳性。
- 用全样本统计量:滚动窗口,否则前视偏差。
- 因子值缺失填 0:停牌股填 0 会被误判为极端值,应填行业中位数或剔除。
- 不记录因子血缘:三个月后没人说得清某个因子怎么算出来的。
小结
因子研究的方法论可以浓缩成三句话:构造要有逻辑、检验要有纪律、合成要有去重。IC 和分层回测是基础工具,但真正决定研究质量的是对统计陷阱的警惕——多重检验、前视偏差、样本外失效,每一个都能让漂亮的回测变成亏损的实盘。
判断一个因子是否值得投入生产,最快的自检是问三个问题:为什么它应该有效(经济学逻辑)?它的 IC 在多个市场/时段是否稳定?它和现有因子的相关性是多少? 三个问题有一个答不上来,就再等等。
下一步建议阅读 组合优化与风险模型 ,看多个因子如何被组合成一个可执行的投资组合。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。