一句话版本
做好了一个因子,怎么判断它有没有用?标准的检验有三件套:
- IC(信息系数):每一期,因子值和下一期收益的(秩)相关系数。它衡量"排序预测得准不准";
- 分层回测:每一期按因子值把股票分成 5 组(或 10 组),看各组的未来收益是否单调;
- 换手率与衰减:因子排名变化得多快,预测力能维持多久。这决定了它在扣除交易成本后还剩多少。
一个好因子的典型样子是:IC 不大(0.03 到 0.05 就已经很好),但稳定(ICIR 高);分组收益单调;换手适中。
免责
本文仅为技术科普,不构成任何投资建议。文中代码使用模拟数据。
第一层:通俗理解
1.1 天气预报员的成绩单
评价一个天气预报员,不是看他某一天有没有说对,而是看:
- 平均有多准:长期来看,预报和实际有多相关;
- 稳不稳定:是每天都差不多准,还是偶尔神准、经常离谱;
- 分档是否合理:他说"70% 概率下雨"的那些天,是否真的比说"30%"的那些天下雨更多。
因子检验做的是同样的事:IC 衡量"平均有多准",ICIR 衡量"稳不稳定",分层回测检查"分档是否合理"。
1.2 为什么 0.05 的相关就算好
日常生活中,相关系数 0.05 几乎等于没有关系。但在股票市场:
- 每一期有几百上千只股票,可以同时下注;
- 每年有十几到几百期,可以反复下注;
- 只要这个微弱的优势稳定存在,乘上广度和次数,就能积累可观的收益。
这就是 Grinold 提出的"主动管理基本定律"的直觉:
信息比率约等于"每次预测的准确度"乘以"独立下注次数的平方根"。
第二层:三件套详解
2.1 IC
每一期 :
用 Spearman 秩相关(而不是 Pearson),对极端值更稳健。
| 指标 | 含义 | 参考 |
|---|---|---|
| IC 均值 | 平均预测力 | 绝对值 0.02–0.03 可以考虑,0.05 以上很少见 |
| IC 标准差 | 波动 | — |
| ICIR = 均值 / 标准差 | 稳定性 | 年化后 0.5 以上不错 |
| IC > 0 的比例 | 胜率 | 60% 以上较好 |
| IC 的 t 值 | 统计显著性 | 注意多重检验问题(见《多重检验与因子动物园》) |
2.2 分层回测
每一期按因子值分成 5 组,等权持有到下一期,记录每组收益:
- 好的因子:第 1 组到第 5 组收益单调递增(或递减);
- 如果只有两端有效、中间乱序,说明因子可能只在极端值上有信息;
- 多空组合(第 5 组 − 第 1 组)的收益和夏普比率是最直观的"因子收益"。
2.3 换手率与衰减
| 概念 | 计算 | 意义 |
|---|---|---|
| 换手率 | 每期组合成分的变化比例 | 换手越高,交易成本越高(见《交易成本建模》) |
| 因子自相关 | 本期因子值与上期的秩相关 | 越高说明因子越"慢",换手越低 |
| IC 衰减 | 因子与未来第 1、2、3…期收益的 IC | 衰减越慢,越可以降低调仓频率 |
第三层:代码实现
3.1 构造模拟面板并计算 IC
模拟 120 个月、每月 300 只股票,因子和下一期收益之间藏着大约 0.04 的真实相关:
import numpy as np, pandas as pd
rng = np.random.default_rng(1)
T, N = 120, 300
dates = pd.period_range("2015-01", periods=T, freq="M")
rows = []
for t in dates:
f = rng.normal(size=N)
r = 0.04 * f + rng.normal(size=N) # 真实 IC 约 0.04
rows.append(pd.DataFrame({"date": t, "factor": f, "fwd_ret": r * 0.08}))
panel = pd.concat(rows, ignore_index=True)
ic = panel.groupby("date").apply(lambda g: g["factor"].corr(g["fwd_ret"], method="spearman"))
print("IC均值", round(ic.mean(), 4), "IC标准差", round(ic.std(), 4),
"ICIR(年化)", round(ic.mean() / ic.std() * np.sqrt(12), 2), "IC>0占比", round((ic > 0).mean(), 2))
输出:
IC均值 0.0465 IC标准差 0.06 ICIR(年化) 2.69 IC>0占比 0.77
3.2 五分组回测
panel["q"] = panel.groupby("date")["factor"].transform(lambda x: pd.qcut(x, 5, labels=False) + 1)
qret = panel.groupby(["date", "q"])["fwd_ret"].mean().unstack()
print("各分组月均收益(%)", (qret.mean() * 100).round(3).to_dict())
ls = qret[5] - qret[1]
print("多空年化收益(%)", round(ls.mean() * 12 * 100, 2), "多空夏普", round(ls.mean() / ls.std() * np.sqrt(12), 2))
输出:
各分组月均收益(%) {1: -0.573, 2: -0.373, 3: -0.063, 4: -0.026, 5: 0.53}
多空年化收益(%) 13.24 多空夏普 2.74
3.3 解读与现实校准
- 五组收益严格单调,多空组合夏普 2.7。这是一个"教科书式"的好因子;
- 但这是模拟数据:因子在每一期都独立地有 0.04 的预测力,没有交易成本,没有风格轮动,没有因子拥挤;
- 真实世界中,一个 IC 在 0.03 左右的因子,扣除成本后多空夏普常常在 0.5 到 1 之间,而且会有很长的回撤期;
- 还要警惕:你检验过多少个因子才找到这个?(见《多重检验与因子动物园》)
3.4 换手率的计算
def top_quintile_turnover(panel):
tops = panel[panel["q"] == 5].groupby("date")["stock_id"].apply(set)
prev, out = None, []
for s in tops:
if prev is not None:
out.append(1 - len(s & prev) / len(s)) # 本期新进入的比例
prev = s
return np.mean(out)
(这段需要面板里有 stock_id 列;上面的模拟数据每期股票独立生成,没有身份延续,所以换手率会接近 80%,这正是"因子没有自相关"时的情况。)
3.5 一份检验报告应包含什么
| 项目 | 内容 |
|---|---|
| 基本统计 | 覆盖率、分布、与已有因子(市值、行业、动量……)的相关性 |
| IC 分析 | 均值、ICIR、胜率、IC 时间序列图、分年度 IC |
| 分层回测 | 各组净值曲线、单调性、多空收益与回撤 |
| 稳定性 | 不同市场环境(牛市、熊市、高波动)、不同股票池(大盘、小盘)下的表现 |
| 换手与衰减 | 换手率、IC 衰减曲线、扣除成本后的收益 |
| 增量信息 | 对已有因子做回归,看新因子是否提供了新的信息 |
总结
| 工具 | 回答的问题 |
|---|---|
| IC / ICIR | 平均预测力有多强、多稳定 |
| 分层回测 | 排序是否单调、多空收益如何 |
| 换手与衰减 | 扣除成本后还剩多少、调仓多快合适 |
| 主动管理基本定律 | IR ≈ IC × √广度:微弱但稳定的优势可以积少成多 |