动态因子赋权实战:从等权基准到 ICIR 驱动的完整投研历程
由xuxiaoyin创建,最终由bqhnclli 被浏览 27 用户
本文记录了一套多因子动态赋权策略的完整研发过程,包括因子筛选、方法论设计、踩坑实录,以及一个意外发现。回测区间 2019-2026,基准沪深 300。
先看结论
做投研,先给结论,再讲过程。\n
我回测了四组策略,区间 2019 年初到 2026 年 4 月,基准沪深 300:\n
| 策略 | 累计收益 | 年化收益 | 夏普 | 波动率 | 最大回撤 | 贝塔 | 胜率 |
|---|---|---|---|---|---|---|---|
| 等权 TOP10 | 258.37% | 20.03% | 0.69 | 27.69% | 37.04% | 0.89 | 50.81% |
| 等权 TOP20 | 338.00% | 23.52% | 0.83 | 25.88% | 33.70% | 0.88 | 53.81% |
| 滚动权重 TOP10 | 239.76% | 19.12% | 0.70 | 25.30% | 36.61% | 0.82 | 54.04% |
| 滚动权重 TOP20 | 407.59% | 26.15% | 0.97 | 23.89% | 37.64% | 0.79 | 55.91% |
一句话总结:动态 IC 赋权在 TOP20 持仓规模下,是四组策略中风险调整后表现最优的。
注意这里说的是"风险调整后"——不是单纯比谁收益高。滚动 TOP20 的年化 26.15%,比等权 TOP20 的 23.52% 高了 2.6 个百分点,但更重要的是,夏普从 0.83 提到了 0.97,波动率低了 2%,贝塔从 0.88 降到了 0.79。在赚更多钱的同时,承担了更少的系统风险。
TOP10 的情况更有意思。动态赋权的绝对收益略低于等权(19.12% vs 20.03%),但波动率更低、胜率更高、贝塔也更低。当持仓很集中时,因子权重的噪声对组合影响更大,动态赋权通过 ICIR 阈值机制过滤掉了低质量信号,牺牲了一点点收益,换来了更稳健的风险特征。
\
为什么做这个
我本身是学统计的,量化选股这套底层全是统计学,跟我背景很对路。但业内常见的选股路子是"硬筛":市值小于多少的不要,市盈率大于多少的不要,ROE 低于多少的剔除……这种策略效果可以做得很好,但非常考验你对企业和市场的理解,你得知道什么阈值是合理的。
我想探索另一条路:不依赖那么多主观判断,仅仅通过对因子值排序、选 top N 的方式去做投资。这条路对刚入量化的新人特别友好——简单粗暴,回归数据本身。
核心问题是:因子动态赋权,能不能打败最朴素的等权基准?
\
第一步:因子从哪来
平台里最全的因子表将近 6500 个字段。全丢进去显然不现实,一方面计算量爆炸,另一方面因子之间相关性太高,合成时信息冗余。
我的做法是让 AI 做第一轮筛选:
1.先用 arxiv 的 Python 接口做方法论调研,结合 AI 出调研报告,确定用IC动态权重和因子独立性原则两种思路结合。
2.把 6500 个字段导出成 10 份 markdown 文件,一份一份喂给 AI,让它从每份里挑 10 个"主观上相关性弱、互不相关"的因子。分而治之是为了防幻觉——一次性读 6500 个字段,AI 一定会瞎编。
3.光有 AI 还不够,因子在精不在多,还得做二次筛选:
- 第一,缺失值处理。无穷大补成缺失值,然后剔除所有缺失值。
- 第二,截面缩尾(Winsorize)。这步特别重要,不去极值的话,选出来的 top N 全是异常值,策略会被几个极端值带偏:
from scipy.stats import mstats
def winsorize_section(df, col, limits=(0.01, 0.01)):
return df.groupby('date')[col].transform(
lambda x: mstats.winsorize(x, limits=limits)
)
- 第三,按月采样看截面相关性,把相关性大于 0.5 的因子剔除。
- 第四,因子分析。每个因子做四种预处理——市值行业中性化、市值中性化、行业中性化、仅标准化,通过因子分析工具看分层效果。
- 第五,经济含义验证。不能只看分层图好看,还得验证这个因子的经济含义是否站得住脚。同时用两个 AI 工具做交叉验证,避免单一工具的偏差。
最终入库的 8 个因子:
因子是策略的根基,根基不稳,后面再 fancy 的赋权方法都是空中楼阁。
第二步:方法论——从等权基准到 IC 赋权
先搭等权基准
做任何改进之前,必须先有一个基准。没有基准,你根本不知道后面的改进是真的有效还是运气。
等权合成:把每个因子的截面百分位排名直接加减——正向因子加,负向因子减,合成综合得分,选 top N,每只票等权持有,每 22 个交易日调仓一次。
score = rank(f1) - rank(f2) + rank(f3) - rank(f4) + rank(f5) + ...
这个基准的好处是逻辑极其透明,没有任何需要调的参数,跑出来的结果就是"纯因子选股能力"的下限。
IC 赋权的核心思想
等权的问题在于,它假设每个因子在任何时候贡献相同。但现实中,不同因子在不同市场环境下的预测能力差异很大。
IC 赋权的思路很直观:用因子过去一段时间的预测能力来决定它当期的权重,预测能力强的多给权重,弱的少给。
这里用的是 Rank IC,即因子值的截面排名与下期收益率排名之间的 Spearman 相关系数。
注意一个关键细节——时间对齐。因子值用 t 日的值,收益率用 t 日之后 22 天的涨跌幅。但在回测里,为了避免未来数据泄露,实际操作是反过来的:在 t 日,用 t-22 日的因子值去预测 t 日的收益率。这样任何时刻都只用已知信息。
单日 IC 波动很大,所以用 ICIR 来度量信噪比:
ICIR 越高,说明这个因子在过去一段时间里不仅预测能力强,而且稳定。
权重分配:用 ICIR 的绝对值作为原始权重,归一化后使总和为 1,再对每个因子施加上限(60%)防止垄断,最后再次归一化:
\
第三步:踩坑实录
代码写出来容易,跑通并且跑对,完全是两回事。
坑一:负向因子被"静默丢弃"
第一版 IC 赋权跑完之后,回测结果看起来还行,但总觉得哪里不对劲。后来仔细看代码才发现一个隐蔽的 bug:权重计算用的是 icir.clip(lower=0),把负的 ICIR 直接截断成 0。
这意味着什么?负向因子——比如换手率、应收账款周转天数——它们的 ICIR 本身是负的,因为高换手率的股票通常跑输,这符合经济学直觉。但 clip(lower=0)
把它们的权重归零了,5 个因子实际上只有 3 个在工作。
这个 bug 特别容易被忽略,因为回测结果不会崩盘,它只是"悄悄"损失了两个因子的信息。
修复方案是把权重大小和因子方向分开处理:用 icir.abs() 决定权重大小,用 np.sign(icir) 决定打分时是加还是减:
signs = np.sign(icir)
score = sum(signs[f] * weights[f] * today_df[f] for f in FACTOR_COLS)
这样负向因子也能参与合成,只是打分时方向相反,与等权版本的减号逻辑完全一致。
坑二:用噪声驱动权重变化
文献指出 A 股因子的真实 IC 值通常非常接近零。也就是说,在很多时段,ICIR 根本没有统计意义,它本质上就是噪声。
如果每次调仓都无条件更新权重,实际上是在用噪声驱动权重变化,引入不必要的不稳定性。
解决方案是加一个启动阈值:只有当所有因子中 ICIR 绝对值的最大值超过 0.3 时,才更新权重,否则沿用上期权重,"冻结"不动:
if icir.abs().max() < ICIR_THRESHOLD:
return prev_weights # 冻结,不更新
这个机制在市场信号混乱的时候能起到保护作用,在 2022 年熊市里体现得特别明显。
超额从哪来——哪个因子在主导
策略打赢了基准,但更值得追问的是:这套策略的 alpha,到底是哪个因子贡献的?
等权策略里每个因子权重固定在 12.5%,动态赋权则让市场自己决定权重。7 年下来,市场把筹码押在了哪里?
全区间因子强势度排名
等权基准是每因子 12.5%,超过这条线的只有 f2、f3、f6 三个。
换手率(f2)是 8 个因子里最强势的,平均权重 22.8%,是等权基准的 1.8 倍,44% 的期数是当期第一因子。这个结果在直觉上是成立的——换手率是 A 股最经典的反转信号之一,高换手的股票往往是短期被过度交易的,后续容易均值回归跑输;低换手的股票反而更稳健。这个规律在 A 股散户主导的市场结构下长期有效。
净利润增速、市盈率倒数、总资产周转率这三个因子,平均权重都不到等权基准的 70%。这不是说这三个因子没有经济含义——恰恰相反,它们都是教科书级别的基本面因子。问题在于,A 股 2019-2026 这段区间,基本面因子的预测能力本来就偏弱,市场定价经常脱离基本面,ICIR 信号不稳定,动态赋权自然把它们的权重压下去了。这是数据给出的答案,不是主观判断。
逐年权重复盘
几个值得关注的年份:
2020 年(疫情冲击):换手率权重骤降至 12.6%(全区间最低),股息率升至 21.6% 接管主导权。疫情冲击下市场换手率信号失真,反转效应短暂失效,策略自动切换到股息率和财务质量因子——这是动态赋权最直接的价值体现:市场环境变了,权重跟着变,不需要人工干预。
2022 年(熊市防御):换手率重回主导(24.3%),应收账款天数升至 17.7%(全区间最高)。熊市中市场更在意企业的现金流质量,应收账款周转天数短的公司抗跌性更强。
2025 年(资金驱动极端化):换手率飙至 33%(全区间最高),主力资金净流入升至 19.7%,两者合计 52.7%,几乎垄断了权重。财务质量和价值因子全面失效。动态赋权精准识别了这一结构,把筹码全押在了动量类因子上。
一句话总结:这套策略的 alpha,主要由换手率和股息率贡献,前者是 A 股反转效应的体现,后者是价值风格的体现。基本面类因子在这段区间整体偏弱,动态赋权通过降低它们的权重,客观上做了一次"因子质量过滤"。
一个意外的发现:我"无意间"跟踪了沪深 300
讲一个很有意思的意外发现。
由于早期的失误,我没有适配 ICIR 的符号,导致因子合成变成了全部累加的形式:
也就是说,负向因子本来应该减去,但我把它加进去了。然后这个"错误"的策略跑出来的净值曲线,竟然意外地吻合沪深 300。
做了定量分析之后,发现事情没那么简单。
在正常策略中,f2(换手率)的 ICIR 负向占比高达 98.8%,f6(主力资金净流入)负向占比 93.8%,f4(应收账款周转天数)负向占比 87.7%。但在"跟踪"策略里,这三个因子的 |ICIR| 权重分别是 24.0%、13.5%、12.5%,合计占了 50% 的权重。
反过来,f3(股息率)正向占比 90.1%,在跟踪策略中权重仅 14.3%;f1(户均持股变化)正向占比 88.9%,权重仅 10.5%。
几个值得关注的事实:
- f2 是 8 个因子中 ICIR 负向占比最高的(98.8%),同时也是 |ICIR| 权重最大的(24.0%)——符号和权重的"错配"在跟踪策略中表现得最突出。
- 正常策略里被 clip 掉的三个因子(f2、f6、f4),在跟踪策略中合计占了 50% 的权重,而它们恰好是负向因子中 ICIR 信号最稳定的一批。
为什么方向全部抹平后,策略反而贴着沪深 300 走?我的猜测是:当正负方向被全部抹平后,多因子合成不再偏向任何特定风格,而是收敛到了市场的"平均态"。f3 和 f7 把组合拉向大盘蓝筹,f2 又把它拉回小盘成长,各种力量相互对冲后,持仓的市值分布、行业分布反而接近了 A 股最"中庸"的宽基指数。
这件事至少告诉我们:因子方向的处理,有时候比因子权重本身更重要。
数据准备的几个关键细节
这一步是整个回测中最容易出错的地方。
SQL 查询的时间对齐。 在 initialize 阶段一次性把所有数据拉下来,避免在 handle_data 里逐日查询。lag_fi 和 ret 的对齐关系是:在日期 t,lag_fi 是 t-22 日的因子值,ret 是 t-22 到 t 日的涨跌幅。用 t-22 日的因子预测 t-22 到 t 的收益,没有未来数据泄露,这是底线。
预计算每日 IC。 为了避免每次调仓重复计算,initialize 阶段会把整个回测区间内每一天的 IC 值全部算好,存成一个 DataFrame。IC 的计算手动实现 Spearman,而不是直接调 scipy,因为需要按日期分组批量算,手动实现效率更高:
x_mean = grp[lag_f].transform("mean")
y_mean = grp["ret_rank"].transform("mean")
dx, dy = x - x_mean, y - y_mean
numer = (dx * dy).groupby(date).sum()
denom = sqrt((dx²).groupby(date).sum() * (dy²).groupby(date).sum())
IC_t = numer / denom # 当日 Rank IC
当某日有效股票数不足 10 只时,该日 IC 记为 NaN,不参与窗口统计。
数据按日期索引。 handle_data 每天都会被调用,但只在调仓日执行逻辑。为了快速取当日因子数据,initialize 阶段把数据按日期做成字典,调仓时直接取,避免每次都做 DataFrame 过滤。
动态赋权适合谁
动态 IC 赋权不是银弹。它在结构性行情和因子轮动剧烈的年份是主场,在风格极速切换的年份会吃亏。但全区间来看,对于 TOP20 这个持仓规模,它在提升收益的同时有效降低了波动和贝塔,风险调整后的表现是四组策略里最好的。
这套方法最适合已经有一组因子、但不知道如何科学赋权的量化从业者。它不需要你对每个因子有极深的理解,而是通过数据驱动的方式,让市场自己告诉你哪个因子最近"更灵"。
当然,这条路还远没有走完。IC 窗口能不能自适应?阈值能不能根据市场环境动态调整?多因子之间的交互效应能不能建模?这些都是可以继续深挖的方向。
回测区间:2019-01-01 至 2026-04-12 | 基准:沪深 300 | 调仓频率:每 22 个交易日