📚 前置已完成第 1、2 课 + 作业 1~6(见 lessons/01_pandas入门.md、lessons/02_多标的回测.md)📄 代码03_单因子测试.py(本课新增)

第 3 课:单因子测试 —— 从"选时"升级到"选股"

前置:已完成第 1、2 课 + 作业 1~6(见 lessons/01_pandas入门.mdlessons/02_多标的回测.md) 代码文件:03_单因子测试.py(本课新增) 学完本课 → 打开 lessons/04_参数敏感性分析.md 开新会话学第 4 课


0. 新会话背景速览(给"接手的 AI"看的 30 秒)

  • 项目位置:D:/code/量化交易
  • 学生已学完:pandas 基础、回测框架(signal.shift(1) 防前视)、作业 1~5、 多标的回测(50 只股票批量统计、超额收益、赢面、平均 vs 中位数)
  • 本课已交付:03_单因子测试.py 可正常运行
  • 运行方式:项目根目录下 python 03_单因子测试.py(数据走缓存,几秒出结果)
  • 输出:output/factor_panel.csv(因子面板,2648 行观测)、 output/factor_ladder_compare.png(分组收益阶梯对比图)、 output/factor_ic_overview.png(IC 时序 + 分布图)
  • 框架要点:因子统一"值越大 → 预期未来收益越高";未来收益用 close.shift(-horizon) / close.shift(-1)(隔一天生效,无前视,和第 2 课 shift(1) 同理); IC 是每个调仓日横截面的 Spearman 相关,03_单因子测试.py 里手写实现,不依赖 scipy

1. 本课目标

前两课都在干同一件事:对"一只股票"择时(什么时候买、什么时候卖)。 这一课换一个赛道:对"全市场"选股——这是量化研究员最日常的工作。

  1. 学会因子测试的核心套路:按因子分组 → 看高分组是否真的跑赢低分组
  2. 学会 IC(信息系数):因子值和未来收益的相关系数,衡量因子预测力
  3. 亲手测 3 个简单因子(动量、反转、低波动)
  4. 诚实面对结果:大部分简单因子单独用都不可靠——这本身就是最重要的收获

2. 概念先讲透

概念 一句话解释
因子 一个能给每只股票打分的指标。如动量=过去20天涨跌幅、低波动=过去20天波动率取负
分组检验 每个调仓日把股票按因子从低到高分成 5 组,看"因子最高组"未来收益是否真的更高
IC(信息系数) 每个调仓日,因子值和未来收益的 Spearman 相关。>0 且稳定 = 因子有预测力
ICIR IC 均值 / IC 标准差(IC 的"夏普")。>0.5 才算靠谱,<0.3 基本都是噪音
横截面 同一个时刻比较 50 只股票的相对高低(和"时间序列"只盯一只股票相对)
Spearman vs Pearson Spearman 只比"排名",不怕离群值——一只股票暴涨 30% 不会把 IC 拉飞

为什么用 Spearman 而不是 Pearson? 因子测试只看"相对排名":排名第 1 的股票比排名第 50 的强,就够了。 Pearson 关心数值本身,被一两只暴涨股(离群值)一拉就失真; Spearman 换成排名再算相关,稳健得多。03_单因子测试.py 里的 spearman() 就是"两列各自 rank() 后再 np.corrcoef",三行代码,不依赖 scipy。

为什么因子值要"值越大 → 预期收益越高"? 统一方向后,检验就变成一件简单的事:IC > 0 说明因子有效。 比如"反转因子"取负的过去 5 天涨跌幅——跌得越多的股票分越高。 如果你喜欢,也可以定义成"值越大 → 预期收益越低",但那样看图容易绕晕。

3. 代码讲解(对着 03_单因子测试.py 看)

3.1 三个待测因子

FACTORS = {
    "动量":   momentum_factor,   # 过去 20 天涨跌幅:强者恒强?
    "反转":   reversal_factor,   # 过去 5 天涨跌幅取负:跌多了要反弹?
    "低波动": low_vol_factor,    # 过去 20 天波动率取负:稳的走得远?
}

每个因子函数接收一只股票的 K 线 df,返回一个等长的打分序列。 (前 window 天算不出因子值,是 NaN,后面 dropna() 会处理。)

3.2 因子面板(build_panel

把"50 只股票 × 53 个调仓日"的因子值和未来收益拼成一张大表,一行 = 一个截面观测:

f["future_ret"] = (close.shift(-horizon) / close.shift(-1) - 1).values

关键就在这行(和第 2 课 signal.shift(1) 是同一个道理): - 因子值:截至今天收盘的信息 - 未来收益:明天收盘买入 → 持有 20 天 → 卖出 - 中间隔了一天 → 今天的因子偷看不到明天的结果,没有前视偏差

每隔 STEP=20 天取一个截面:相邻交易日的因子值高度重复,全取没意义, 而且真实调仓也是一个月一次。

3.3 分组检验(analyze_factor 前半段)

p["group"] = p.groupby("date")["factor"].transform(
    lambda s: pd.qcut(s.rank(method="first"), N_GROUPS, labels=False) + 1)
ladder = (p.groupby(["date", "group"])["future_ret"].mean()
           .groupby("group").mean())
  • qcut(..., 5):每个调仓日把 50 只股票按因子切成 5 组(1=最低 … 5=最高)
  • 先对 (日期, 组) 求平均,再对日期求平均:每个调仓日等权, 不会被某些天股票数量多带偏
  • 结果就是"分组收益阶梯":因子有用 → 第 5 组 > 第 4 组 > … > 第 1 组(单调爬升)

3.4 IC 计算(analyze_factor 后半段)

ics = pd.Series({
    date: spearman(g["factor"], g["future_ret"])
    for date, g in p.groupby("date")
})

每个调仓日算一个横截面 IC → 得到 53 个 IC,再统计:

统计量 含义 判断标准
IC 均值 平均每个调仓日因子方向对不对 绝对值 > 0.05 才有一点点信号
ICIR IC 均值 / IC 标准差,IC 的"夏普" > 0.5 靠谱,< 0.3 基本是噪音
|t| = ICIR×√n 显著性检验(n=调仓日数 53) |t| > 2 才算不是运气
IC>0 占比 多少调仓日因子方向是对的 稳定因子应 > 60%

3.5 两张图

  • 分组收益阶梯图:三个因子并排,一眼看出"高分组到底高不高"——本课最核心的图
  • IC 时序 + 分布图:IC 是不是随时间稳定 > 0,还是围绕 0 乱跳

4. 真实运行结果(2021-01 ~ 2025-06,50 只股票,53 个调仓日)

这是本课实际跑出来的结果(因子值越大 → 预期收益越高,所以 IC > 0 = 因子有效):

指标 动量(20日) 反转(5日) 低波动(20日) 组合(低波动+动量)
IC 均值 −0.015 −0.049 +0.040 +0.012
IC 标准差 0.243 0.251 0.250 0.244
ICIR −0.063 −0.197 +0.159 +0.049
|t| 0.46 1.43 1.16 0.36
IC>0 占比 47% 43% 58% 53%
高分组−低分组 −0.59% −0.51% −0.21% −0.68%
阶梯单调? ❌ 5组最低 ❌ 不单调 ❌ 不单调 ❌ 不单调

四个因子的阶梯全部走平、|t| 全部 < 2(统计上都不显著)——这不是代码 bug,这正是本课要教的事。

逐个看:

  1. 动量(IC −0.015,阶梯反向):过去 20 天涨得最猛的一组,未来 20 天反而平均跌 0.37%——"强者恒强"在这 4 年半不成立。
  2. 反转(IC −0.049,方向反了):抄底"跌得多"的股票不但不反弹,还继续跌(短期惯性占上风)。 注意噪音大(|t|=1.43 < 2),别把这个解读成"短期动量铁律"。
  3. 低波动(IC +0.040,ICIR +0.159,58% 的日子方向对)三个里最不差的—— 和全球学术界的"低波动异象"一致(低波动股票长期风险调整后收益更好)。 但 |t|=1.16 仍不显著,单独用依然不可靠。
  4. 组合(低波动+动量,ICIR 0.049):无脑等权平均两个因子,反而比低波动单独用还差——两个因子方向不一致(动量近似反向),排名一平均互相抵消。

注:此结果会随数据缓存和参数变化而不同,重点看"数量级和方向"而不是精确数字。

分组收益阶梯对比图(三因子并排,看高分组是否真的跑赢低分组):

IC 时序 + 分布图(IC 是否稳定 > 0):

5. 作业

作业 6:对比三个因子,写一句"哪个最强、为什么"

对照上面结果表回答: 1. 三个因子里,哪个 IC 均值最高?哪个 ICIR 最高?IC>0 的占比最高的是谁? 2. 结论应该写什么?(提示:单因子体检的标准答案是"低波动最不差,但都不显著、不可单独用") 3. 为什么说"阶梯单调"比"IC 均值"更直观?

作业 6 参考答案

  1. 低波动因子:IC 均值 +0.040(唯一为正的)、ICIR +0.159(唯一像样的)、 IC>0 占比 58%(唯一过半的)——三项全胜,是三个里最强(最不差)的。
  2. 但诚实结论是:三个因子单独用都不可靠——|\t| 全 < 2、 阶梯全部不单调,动量/反转的方向甚至和理论预期相反。
  3. 阶梯图把"因子最高组 vs 最低组"的未来收益摆在一起,20 天收益直接能看懂; IC 是一个相关数,不熟悉统计的人看不出 0.04 意味着什么。 好的阶梯是单调爬升(1 组低 → 5 组高),一眼就能看出因子有没有区分度。

作业 7(进阶):把双因子合起来,看是否比单因子强

03_单因子测试.py 里的 COMBO 列表,比如:

COMBO = ["低波动", "反转"]     # 或 ["反转", "动量"],或三个全加

重跑,对比"组合"那一行的 IC / ICIR / 阶梯。 预测一下再跑:反转因子方向是反的(IC 为负),把它混进组合,结果会更好还是更差?

作业 7 参考答案(真实数据跑出来的,供对照)

组合 IC 均值 ICIR |t| IC>0 占比 高分组−低分组
低波动+动量 +0.012 +0.049 0.36 53% −0.68%
低波动+反转 +0.007 +0.031 0.22 47% −0.75%
反转+动量 −0.056 −0.229 1.67 32% −1.71%
三个全加 −0.011 −0.049 0.36 47% −0.72%
(单因子基准)低波动 +0.040 +0.159 1.16 58% −0.21%

三个结论: 1. 无脑等权组合反而更差:最好的组合(低波动+动量 ICIR 0.049)也比低波动单因子(0.159)差一大截。 2. 把方向反的因子混进来是灾难:反转+动量的组合 ICIR −0.229、IC>0 只有 32%—— 一个方向错乱的因子会污染整个组合。 3. 正确姿势:组合前先做单因子体检(就是作业 6 干的事),方向不对的先取反或剔除, 方向对且显著的才合成。合成时最好用排名差/排名加权而不是无脑平均—— 等权平均等于默认"两个因子一样强、方向一致",这两条在这批数据里都不成立。

这一课最重要的收获:把 3 个因子的结果(IC≈0、阶梯走平)当成"数据体检报告", 而不是"代码没写对"。量化研究 90% 的时间都在得到"没用"的结果—— 能从一堆没用里筛出"低波动稍微有点用",并且知道它有多不可靠,就是合格的第一步。

6. 学完自检清单

  • [ ] 能说清 IC 和分组收益阶梯图各表达什么
  • [ ] 能说清为什么 Spearman 比 Pearson 更适合因子检验
  • [ ] 能说清为什么"单因子测试"比"单股择时"更接近量化研究的日常
  • [ ] 复跑过 python 03_单因子测试.py,结果和上面的表数量级一致
  • [ ] 能解释"为什么四个因子都不显著"不是 bug(样本量、噪音、数据挖掘)
  • [ ] 做完作业 6、7,能说出组合因子的坑(方向不一致互相抵消)

7. 下一课预告

第 4 课:参数敏感性分析lessons/04_参数敏感性分析.md) 网格搜索 MA_FAST × MA_SLOW,画热力图,真正理解"参数过拟合": 为什么历史最优参数换段时间就失效。 前两课我们一直在和"策略/因子到底有没有用"搏斗,下一课要回答: 就算看起来有用,你敢不敢信它?