【魔码量化工程实战进阶 #26】横截面 IC 与分层回测:给你的因子做一次体检
【魔码量化工程实战进阶 #26】横截面 IC 与分层回测:给你的因子做一次体检
作者:魔码数服 | 系列:魔码量化工程实战进阶
一、因子不是永动机
量化策略的核心是因子,但因子不是永动机——均线因子在震荡市失效,动量因子在熊市反转。如果不做定期"体检",策略可能在不知不觉中退化成随机游走。
上一篇我们跑通了多因子工程化(去极值、标准化、中性化、等权合成)。但合成出打分之后,还有一个更根本的问题没回答:这个因子到底有没有预测能力?
本文带你用两个工具给因子做体检:
- IC 分析(信息系数):衡量因子值与未来收益的相关性;
- 分层回测(Stratified Backtest):把股票按因子值分成五组,检验收益区分度。
全程使用魔码量化行情 API 的纯 HTTP 接口获取真实数据,零 SDK 依赖。
二、本文你将得到什么
- 一段可直接运行的代码,用魔码 HTTP 接口拉取真实日线并计算因子;
- IC 的准确定义与 Spearman 实现;
- 一个被很多人忽略的现象:因子会衰减——短期 IC 很高,持有期一长就归零;
- 五分位分层回测的完整实现与真实输出;
- 因子体检的若干避坑点(别用 Pearson 评估非线性因子、持有期要匹配 IC 衰减周期等)。
三、数据:魔码真实接入
和上一篇一样,先用纯 HTTP GET 拉取真实日线。下面这段拉取 600519 并计算 20 日动量(真实数据):
import requests
BASE = "https://api.momaapi.com"
# 演示证书,请换成你自己的正式证书:https://www.momaapi.com/certificate-apply.html
LIC = "TEST-API-TOKEN-MOMA-836089C22111"
bars = requests.get(f"{BASE}/hsstock/history/600519/d/n/{LIC}?st=20250101&et=20260830",
timeout=20).json()
df = __import__("pandas").DataFrame([{"date": b["t"], "close": float(b["c"])} for b in bars])
df = df.sort_values("date").reset_index(drop=True)
df["mom20"] = df["close"].pct_change(20) * 100
print(df.dropna().tail(6)[["date", "close", "mom20"]].round(3))
真实返回(近 6 个交易日 20 日动量,单位 %):
| date | close | mom20(%) |
|---|---|---|
| 2025-11-24 | 11.60 | 0.694 |
| 2025-11-25 | 11.80 | 2.877 |
| 2025-11-26 | 11.69 | 2.634 |
| 2025-11-27 | 11.71 | 2.900 |
| 2025-11-28 | 11.61 | 2.562 |
| 2025-12-01 | 11.69 | 2.275 |
注意:演示证书(免费版)返回的是每只股票形态一致的样本,截面因子没有差异,因此下面的 IC 与分层回测用一个结构等价的示意截面来演示算法。换成你的正式证书拉取真实全市场数据时,下面这段代码直接产出你的真实 IC。
四、第一步:IC 到底是什么
IC(Information Coefficient,信息系数)衡量的是本期因子值与下期收益率之间的秩相关系数:
IC = SpearmanR(factor_t, return_{t+1})
为什么用秩相关(Spearman)而不是皮尔逊(Pearson)?因为因子分布厚尾、非线性,秩相关对异常值更鲁棒。通用判读标准:
IC > 0.05:因子有显著预测能力;0.02 < IC < 0.05:有一定预测力,但要看稳定性;|IC| < 0.02:基本无效;IC < -0.02:反向有效(需反向使用或舍弃)。
除了 IC 均值,还要看正值占比(方向稳定性)和滚动 IC(捕捉因子衰减)。
import pandas as pd
def calc_ic(factor, fwd_ret):
mask = factor.notna() & fwd_ret.notna()
if mask.sum() < 5:
return float("nan")
return factor[mask].corr(fwd_ret[mask], method="spearman")
# 逐交易日计算截面 IC
ics = df.groupby("date").apply(lambda g: calc_ic(g["factor"], g["fwd_ret"]))
print(f"IC均值={ics.mean():+.3f} 正值占比={(ics > 0).mean():.1%}")
五、第二步:IC 衰减——因子会"过期"
最容易被忽视的一点:因子信号的预测能力随持有期拉长而衰减。我们同时算 1 日、5 日、20 日三个持有期的 IC:
| 持有期 | IC 均值 | 正值占比 | 样本日 |
|---|---|---|---|
| 1 日 | +0.928 | 100.0% | 30 |
| 5 日 | +0.564 | 100.0% | 30 |
| 20 日 | -0.066 | 40.0% | 30 |
这条曲线讲了一个非常真实的故事:
- 1 日 IC 高达 +0.93,且 30 个交易日全部为正——这是一个强而稳定的短期信号;
- 到 5 日衰减到 +0.56,方向仍然稳定;
- 到 20 日已经跌到 -0.07、正值占比仅 40%——信号基本蒸发,甚至轻微反转。
结论很明确:这个因子的"半衰期"很短,持有周期必须匹配它的衰减周期。如果你拿着 1 日信号持有 20 天,多出来的 19 天全是噪声,回测里看到的"收益"不过是把短期 alpha 错误地摊到了长期。
实务建议:根据 IC 衰减曲线确定持有期。短期因子(如反转、资金流)适合周频甚至日频调仓;长期因子(如价值、质量)才适合月频。把快衰因子用慢调仓频率去交易,是最常见的"回测好看、实盘拉跨"来源之一。
六、第三步:分层回测——因子有没有区分度
IC 是单变量统计,分层回测是多变量验证:把股票按因子值从低到高等分五组,看每组未来的平均收益是否单调。好的因子应该表现为单调性——最高组收益显著高于最低组,中间依次过渡。
按因子值五分位后的各组平均前向收益(单位 %,收益为正表示上涨、为负表示下跌,遵循 A 股红涨绿跌习惯):
| 分组 | h=1 日 | h=5 日 | h=20 日 |
|---|---|---|---|
| G1低 | -1.515 | -1.005 | 0.203 |
| G2 | -0.868 | -0.525 | -0.014 |
| G3 | 0.055 | 0.017 | 0.277 |
| G4 | 0.853 | 0.480 | 0.071 |
| G5高 | 1.680 | 0.910 | -0.382 |
| 形态 | 单调↑ | 单调↑ | 非单调 |
多空价差(G5高 − G1低,h=1 日):+3.195%。
解读:
- h=1 与 h=5 单调上升,且多空价差为正——因子在短持有期下有清晰的区分能力,和 IC 结论一致;
- h=20 不再单调,G5高组反而录得负收益——再次印证了第五步的衰减结论:长期持有下,因子的区分度消失了。
分层回测的价值,正是把"IC 有没有"变成"组合能不能赚到"。当两者结论冲突时(比如 IC 为正但分层不单调),往往意味着样本期存在结构性断裂,需要进一步按市场状态分段体检。
七、因子体检的避坑清单
- 优先用秩相关(Spearman):非线性或厚尾因子用 Pearson 会严重低估真实关系。
- 持有期要匹配 IC 衰减:这是本文最核心的一课,别让快衰因子套慢调仓。
- 必须做样本外 / Walk-Forward 验证:只用全历史算一个漂亮 IR 是过拟合温床。滚动窗口、留出未来区间,看样本外 IC 是否还能保持样本内的 50% 以上。
- 警惕前视偏差:因子只能用 t 时刻及之前的数据,前向收益必须严格用 t+1 之后,绝对不能把当日收盘价同时用于因子和收益。
- 涨跌停 / 停牌日标记处理:这类样本的收益不可交易,应标记为缺失、不参与因子与 IC 计算。
- IC 不是收益:IC 衡量的是排序能力,不是实际盈亏。样本外 IC 显著不代表实盘能赚到钱,中间还隔着交易成本、冲击成本和容量。
八、下篇预告
因子体检合格之后,下一步就是怎么把因子变成可执行的持仓。下一篇【魔码量化工程实战进阶 #27】组合优化器:从排序选股到约束型二次规划,我会带你搭建带行业偏离、单票上限、换手率约束的组合优化器,把"高分股票"升级为"风控优先的真实组合"。
免责声明
本文所有代码示例与数据均用于技术方法演示,不构成任何投资建议。市场有风险,投资需谨慎。文中因子与回测结论不预示未来收益,据此操作风险自担。
想要稳定、即取即用的行情数据,给自己的因子做一次严谨体检?魔码数服提供纯 HTTP 行情接口,免 SDK、免运维,覆盖日线 / 分钟级(Pro 包)多周期数据。免费证书即可开始:https://www.momaapi.com/certificate-apply.html
