❌ shelved · 最终化:2026-05-08
源码:ideas/S4_cn_etf_momentum_tilt/v1 + summaries/S4_cn_etf_momentum_tilt/v1
本策略的其他版本
  • v1 本文 ❌ — 搁置 v1。在 V1 共享基线(6 只宽基/行业 ETF、2020-2024、20 日 lookback、α=1.0、月频再平衡)上,横截面动量倾斜相对 S3 等权基线产生 -2.56% 年化超额、IR=-0.49、t-stat=-1.08,统计上未达到显著拒绝零假设,但所有尝试过的 α 与 lookback 组合方向上一致为负——属于经济意义上的系统性反向,不是噪音。
  • v2 ✅ — v2 的改进显著有效(CAGR +5.73pp / Sharpe ×3.7 / MaxDD 减半 vs v1,且首次跑赢 510300 BH +2.6%/yr),但收益主要来自「扩池 6 → 11」对 S3 baseline 的提升,动量 tilt 信号本身仍未带来 alpha(v2 vs S3-11 IR=-0.17,α 敏感性中 α=0 仍是最优档)。Ship v2 配置,但备注「价值在跨资产分散,而非动量」;status 为 shipped (pool value, not factor value)

想法(Why)

一句话概括

在 6 只 ETF 等权满仓的基础上,按动量得分把权重从 1/N 微幅倾斜到强势资产。

核心逻辑

  1. 标的池固定为 Benchmark Suite V1 共享的 6 只风险 ETF(沪深300/中证500/创业板/中证1000/证券/医疗),起点权重均为 1/6。
  2. 每个再平衡日(默认每 20 个交易日)计算各标的的动量得分(MomentumReturn(lookback=20),可叠加 MomentumReturn(lookback=60))。
  3. 把动量得分横截面 z-score 化,按 z-score linear tilt 公式生成目标权重:w_i = 1/N + α * z_i / N
  4. 应用上下限:w_i ∈ [0.05, 0.40],再归一化使 sum(w) == 1
  5. 仍然 100% 满仓、无现金缓冲,Rebalance 周期与 S3 默认一致。

假设与依据

  • A股板块指数中期存在动量延续(典型半年度反转之外的 1–3 个月窗口)。Asness 等的截面动量、ETF 动量轮动文献有支持。
  • 倾斜而不是「全仓 top-K」的好处:在 6 只池子里用 top-N 噪音太大、换手太高;轻倾斜既保留分散,又不至于完全无视截面信息。
  • 上下限保证最坏情况下任一资产权重不会过度集中(≤40%)或被清仓(≥5%),避免单点风险。

标的与周期

  • 市场:A股 ETF (market: cn_etf)
  • 标的池:510300, 510500, 159915, 512100, 512880, 512170
  • 频率:日线(1d
  • 数据起止:2020-01-01 ~ 2024-12-31

一句话结论

搁置 v1。在 V1 共享基线(6 只宽基/行业 ETF、2020-2024、20 日 lookback、α=1.0、月频再平衡)上,横截面动量倾斜相对 S3 等权基线产生 -2.56% 年化超额、IR=-0.49、t-stat=-1.08,统计上未达到显著拒绝零假设,但所有尝试过的 α 与 lookback 组合方向上一致为负——属于经济意义上的系统性反向,不是噪音。

在什么情况下有效,什么情况下失效

  • 失效(已观测):单边普涨年(2020:S4-S3 = -20.3%)、单边熊市(2022:-3.6%)、风格分化但池内重叠高的年份。
  • 看似有效但不稳健:2024 一年 +7.7% vs S3,主要靠抓住下半年证券(512880)反弹;这是单一年度单一资产驱动的「样本内胜利」,不能视为可重复的 alpha。
  • 三个 lookback (10/20/60) × 五个 α (0/0.5/1/2/5) 全部 vs S3 IR 为负 → 不是参数问题,是信号本身在这个池子上失效

这个策略教会我什么

  1. 池子分散度比因子参数更重要。6 只 A股 ETF 的横截面 σ 太小,z-score 的信息密度被相关性吃掉了。下次做 cross-section 动量先看池内 pairwise correlation。
  2. A股 ETF 上短期反转盖过中期动量。lookback=20 是最差的一档,符合传统经验中 A股一个月内的均值回复倾向。下次试动量先 skip=21
  3. t-stat 不显著但方向稳定也是有用信号。|t|=1.08 < 2 不能拒绝 0,但 8/8 (5α + 3lb) 实验全输等权 → 该结论有 8 次独立的「方向一致」证据,足以判定 v1 配置不可上线。
  4. 「目标权重恒定」策略的 turnover 不能用目标权重差衡量。S3 目标周转=0 但实际有漂移再平衡,下次跨策略 turnover 比较直接读 vbt pf.trades.records
  5. 事前指标预期值是有用的。事前 IR 估计 0.2~0.5,实测 -0.49 → 偏离 ~1σ 以上 → 直接说明「我们对这个市场有错误先验」,比单看绝对收益更易识别问题。

关键图表

equity_curve

drawdown

tilt_strength

weight_evolution

实现要点

展开完整实现记录

Implementation — A股 ETF 等权 + 动量倾斜

整体方案

派生自 Strategy 3 的 EqualRebalanceStrategy,仅覆盖 target_weights(date, prices_panel) 钩子。整体仍是「6 只 ETF 满仓 + 每 20 个交易日再平衡」,本策略的差异完全集中在「目标权重怎么算」:

  1. 在 rebalance 日,对面板里每个 symbol 计算 MomentumReturn(lookback=20)date 当天的值。
  2. 横截面 z-score 标准化(NaN 当作 0)。
  3. 线性倾斜:w_raw_i = 1/N + alpha * z_i / N
  4. 用「水位线迭代」处理 [w_min=0.05, w_max=0.40] 上下限:每轮按 sum=remaining 缩放未钉死资产,识别最严重的越界并钉死到边界,重复直至收敛或 free 集为空。
  5. 输出权重严格满足 sum(w)=1w_i ∈ [w_min, w_max]、非负。

可选:启用 secondary_lookback=60,主+次动量各自横截面 z-score 后等权融合。

因子清单

Factor 类文件参数方向是新增还是复用
MomentumReturnsrc/strategy_lib/factors/momentum.pylookback=20+1复用
MomentumReturn(可选)src/strategy_lib/factors/momentum.pylookback=60+1复用

新增因子(如有)

无。notes.md 中提议了 MomentumRiskAdjusted(动量/波动率),暂不实现。

策略配置

  • 配置文件:configs/cn_etf_momentum_tilt.yaml
  • 类型:momentum_tilt(自定义;与 cs_rank 等内置类型并列)
  • 关键参数:tilt.method=zscore_lineartilt.alpha=1.0tilt.w_min=0.05tilt.w_max=0.40rebalance=20
  • 与 S3 的关系:strategy.parent: cn_etf_equal_rebalance,明确派生关系;MomentumTiltStrategy(EqualRebalanceStrategy)

类签名

class MomentumTiltStrategy(EqualRebalanceStrategy):
    def __init__(
        self,
        *args,
        lookback: int = 20,
        secondary_lookback: int | None = None,
        alpha: float = 1.0,
        w_min: float = 0.05,
        w_max: float = 0.40,
        **kwargs,
    ): ...

    def target_weights(
        self,
        date: pd.Timestamp | datetime.date,
        prices_panel: dict[str, pd.DataFrame],
    ) -> dict[str, float]: ...

数据

  • 标的池来源:手工 6 只 ETF(与 Benchmark Suite V1 共享基线)
  • 数据范围:2020-01-01 ~ 2024-12-31
  • 数据预处理:依赖 data/cn_etf loader 的前复权(akshare qfq

与 S3 接口契约的差异(v1)

S3 的 subagent 与本任务并行运行;写本策略时 src/strategy_lib/strategies/cn_etf_equal_rebalance.py 还未落地。本实现按以下假设契约推进:

class EqualRebalanceStrategy:
    def target_weights(self, date, prices_panel) -> dict[str, float]:
        # 返回非负、和=1 的权重字典
        ...

MomentumTiltStrategy 仅覆盖 target_weights,其余初始化签名通过 *args, **kwargs 透传到父类。如果 S3 的真实实现签名不一致(比如返回 pd.Series 而非 dict、或者钩子叫别的名字),按要求优先采用本假设契约,必要时在 S3 落地后做一次小适配(修改 target_weights 的返回类型即可),其它逻辑无需改动。

源码里的 try/except ImportError 兜底是为了在 S3 模块缺失时本模块仍可被 import;正式回测必须以 S3 真实落地为前提。

踩过的坑

  • 第一版 _tilt_weights 用「单轮 clip + 归一化」,在 alpha 过大时所有资产都打到上限,归一化后又退化为等权(信号丢失)。改为水位线迭代后稳定。
  • z-score 计算要把 NaN 当 0(中性),否则少数资产没历史数据会污染整个截面。
  • compute_panel(panel) 返回 wide DataFrame,要按 date 取一行;如果 date 不在 index 里,回退到 <= date 的最后一行(典型场景:周末/停牌)。

相关 commits

  • 实现:<sha>(待提交)
  • 调参:未发生

验证过程

展开完整验证记录

Validation — A股 ETF 等权 + 动量倾斜

每次新一轮回测/验证就追加一个 ## YYYY-MM-DD <轮次主题> 小节,不要覆盖。


2026-05-08 Smoke test(合成数据)

配置 & 数据

  • 配置:configs/cn_etf_momentum_tilt.yaml @ commit <待提交>
  • 数据:合成 panel,6 个 symbol、250 个交易日(B 频率)、不同 drift(-0.10‰ ~ +2.00‰/天)
  • 父类:EqualRebalanceStrategy stub(S3 subagent 并行落地中,以保证本策略可独立验证)
  • 入口脚本:summaries/cn_etf_momentum_tilt/validate.pypython3 summaries/cn_etf_momentum_tilt/validate.py

结果

Case场景期望结果
1默认 alpha=1.0、6 资产sum=1,min≥0.05,max≤0.40,最高 drift 资产权重最高sum=1.000000,min=0.0500,max=0.3704;最高 drift 512170 最高 (0.3704)
2alpha=0退化为等权 (1/6=0.1667)全部 0.1667
3alpha=10(极端倾斜)上下限收住,top-2 顶到 0.40top 2 个都达到 0.40,bottom 4 个达到 0.05
4secondary_lookback=60sum=1、上下限生效sum=1,max=0.3966,min=0.05
5数据不足 5 行(动量算不出)降级为等权全部 1/6
6单 symbol panel100%{"510300": 1.0}
7空 panel{}{}

代表性权重(Case 1,6 资产、合成数据 drift 单调递增):

512170: 0.3704
512880: 0.2809
159915: 0.1362
510500: 0.0973
512100: 0.0653
510300: 0.0500

关键观察

  • 倾斜方向正确(高动量 → 高权重),上下限严格生效,归一化无误差。
  • 极端 alpha 下退化为「上限组 vs 下限组」的二分配置,与设计意图一致(极端不是 bug)。
  • 数据不足/空 panel 降级路径平稳。

解读 & 问题

  • v1 没有真实数据,只能确认数学正确性;alpha=1.0、w_min/w_max 选择需要真实数据复盘。
  • 动量 lookback 选择尚未在真实数据上比较,notes.md 已记录候选。

下一步

  • 等 S3 (cn_etf_equal_rebalance) 落地 → 替换 stub → 跑联合 import smoke test
  • data.get_loader("cn_etf") 拉真实数据,跑 2020-2024 完整回测
  • 与 S3 等权基线做差异指标分析(见下节)

与 S3 等权基线的预期对比指标(事前估计)

由于本策略只在「权重分配」上偏离 S3,用以下指标量化倾斜带来的择时贡献:

指标定义预期值(合成假设)解读
信息比率 (IR)mean(r_strat - r_s3) / std(r_strat - r_s3) * sqrt(252)0.2 ~ 0.5中等水平。如果 IR > 0.5 说明动量倾斜有显著择时;< 0.1 说明信号被成本/噪音吃掉
跟踪误差 (TE)std(r_strat - r_s3) * sqrt(252)1% ~ 3% 年化过低(<1%)说明 alpha 太小、退化为 S3;过高(>5%)说明倾斜过强、和 S3 相差太远不再是「派生」
超额收益 (alpha)CAGR(strat) - CAGR(s3)-1% ~ +3%动量年(2020/2021/2023H2)应贡献正 alpha;2022 单边熊市可能贡献负 alpha
主动权重均值 |Δw|mean over rebal dates of mean_i abs(w_strat_i - 1/N)5% ~ 12%验证倾斜幅度:太低说明 alpha=1 不够强;太高说明撞到边界饱和
换手率差额turnover(strat) - turnover(s3)+30% ~ +80%/年倾斜引入额外换手;对比成本敏感度
命中率rebalance 区间内「正 Δw 资产平均收益 > 负 Δw 资产平均收益」的比例50% ~ 60%动量倾斜的「方向准确率」;趋近 50% 说明动量信号无效
择时贡献分解r_strat - r_s3 拆成 Σ_i Δw_i * r_i 按资产/年度归因看哪个 ETF / 哪个年度最贡献 alpha;若集中在 1 个资产说明分散度不足

判定标准(草案):

  • 入选(继续推进):年化 IR ≥ 0.3、TE ∈ [1%, 4%]、超额 ≥ 0%、命中率 > 52%
  • 搁置(回到 S3):IR < 0.1 或 alpha 长期 < 0、且换手率成本 > 净超额收益

注:这些数值是事前估计,等真实数据回测出来后会在新一轮 ## YYYY-MM-DD 小节里用真实数对照修正。


2026-05-08 真实数据回测(2020-01-01 ~ 2024-12-31)

配置 & 数据

  • 入口:python summaries/cn_etf_momentum_tilt/validate.py real(主结果) / ... sweep(敏感性)
  • 数据:data.get_loader("cn_etf").load_many(...),6 只 ETF(不含货币 511990),akshare qfq 前复权,缓存命中
    • 510300, 510500, 159915, 512100, 512880, 512170
    • 每只 ~1211 个交易日(2020-01-02 ~ 2024-12-31)
  • 父类:S3 真实落地的 EqualRebalanceStrategy(不再是 stub),直接 import 跑 baseline,参数对齐 rebalance_period=20
  • 绩效计算:以策略 NAV 的日收益序列为口径,年化基数 252;总收益 = (1+r).cumprod()-1(与 vbt total_return 略有差异,因前者不扣初始建仓的佣金/滑点)
  • 共享基线:100k RMB 起步,fees 万 0.5,slippage 万 5(V1 标准)

主结果(α=1.0, lookback=20, rebal=20)

策略Final NAVTotal RetCAGRVolSharpeMaxDDCalmar
S4 momentum_tilt0.986-1.42%-0.30%24.16%0.108-48.73%-0.006
S3 equal_rebal (basis)1.120+11.96%2.38%23.77%0.217-45.18%0.053
510300 BH1.064+6.39%1.30%21.74%0.168-42.78%0.030

S4 显著输给 S3 等权基线,输的幅度也接近 510300 BH 的水平。

与 S3 等权基线的相对绩效(因子有效性核心

指标解读
年化超额(CAGR S4 − S3)-2.56%动量倾斜每年烧掉 ~2.5%
信息比率 (IR)-0.49显著为负;事前预期 0.2~0.5,反向
跟踪误差 (TE)5.18%在合理范围(事前预期 1%~3%,略偏高)
超额收益 t-stat-1.08 (n=1209)|t|<2,统计上未达到 5% 显著拒绝 0;但方向稳定为负
平均主动权重 mean|Δw|0.092≈ 9.2% 仓位偏离 1/N,倾斜幅度合适,不是因为信号太弱才没效果
年化目标权重周转(target turnover)4.26 (S4) vs 0.00 (S3)S3 目标权重恒为 1/N 因此目标周转 0;S4 ~426% 年化(每月单边换 ~35%)

与 510300 BH 的对比(绝对基准

指标
年化超额-1.05%
IR-0.09
TE11.18%
超额 t-stat-0.21 (n=1209)

S4 的绝对水平比 510300 BH 略差(-1% 年化),但波动/回撤都更高;从「绝对基准」看也没有跑赢。

分年度收益(2020 ~ 2024)

年份S4 momentum_tiltS3 equal_rebal510300 BHS4 − S3S4 − BH备注
2020+21.32%+41.64%+31.11%-20.32%-9.79%单边大牛市;S3 全押满仓占便宜,S4 倾斜反而踏不上
2021+6.13%+5.89%-2.89%+0.25%+9.02%中证500/创业板抱团年;S3/S4 都靠 510500/159915 跑赢;S4 未额外贡献
2022-26.96%-23.34%-21.20%-3.62%-5.75%单边熊市;S4 因倾斜方向错被双杀,符合事前担忧
2023-9.84%-10.33%-10.43%+0.49%+0.60%震荡下跌;S4 微胜
2024+16.24%+8.58%+18.39%+7.66%-2.15%9-10 月快速反弹叠加证券板块(512880)爆发;S4 倾斜抓住部分

关键观察:S4 的「正贡献年份」是 2024(+7.7% vs S3),但被 2020 一年的 -20% 完全压垮。20% 的年度 alpha 缺口几乎全部来自 2020 一年——彼时 6 只 ETF 全面普涨且分散度小,动量信号容易在板块轮动中买在高位、卖在低位。

Alpha 敏感性(lookback=20, rebal=20)

αCAGRSharpeMaxDDvs S3 α_annvs S3 IRvs S3 tmean|Δw|目标周转
0.02.38%0.217-45.18%-0.00%-0.10-0.230.0000.00
0.50.54%0.141-47.48%-1.79%-0.48-1.060.0612.98
1.0-0.30%0.108-48.73%-2.56%-0.49-1.080.0924.26
2.0-0.51%0.100-49.04%-2.72%-0.49-1.070.1064.59
5.0-0.36%0.107-48.77%-2.54%-0.44-0.970.1124.65

α=0 是最优档位,意思是直接用 S3 等权——任何非零的动量倾斜都把 Sharpe 从 0.217 拖到 ~0.10。CAGR 与 IR 关于 α 单调递减并在 α≈2 触底,更大的 α 反而被边界 clip 救回一点,但仍显著落后等权。

Lookback 敏感性(α=1.0, rebal=20)

lookbackCAGRSharpeMaxDDvs S3 α_annvs S3 IRvs S3 tmean|Δw|
10+0.89%0.157-44.97%-1.34%-0.27-0.580.089
20-0.30%0.108-48.73%-2.56%-0.49-1.080.092
60+1.62%0.187-48.53%-0.67%-0.14-0.300.087

三档 lookback 全部 vs S3 IR 为负。最差的是 20 天(中期动量 + 短期反转交叠),10/60 天稍好但仍未超 S3。没有任何 lookback 使动量倾斜对等权产生正 alpha

关键图表

  • artifacts/equity_curve.png:三条线,S4 蓝 vs S3 绿 vs BH 红。可见 S3 全程在 S4 之上,分歧主要发生在 2020Q3-Q4 与 2024H2
  • artifacts/drawdown.png:S4 在 2022 熊市与 2020 国庆后双双比 S3 多 ~3-5pp 回撤
  • artifacts/weight_evolution.png:堆积面积图显示证券板块(512880)经常顶到 0.40 上限,医疗(512170)2021 后期顶到下限 0.05,符合「赌赛道」直观印象
  • artifacts/tilt_strength.png:z-score 分布近似中心化,活跃权重 |Δw| 集中在 0~0.25(被 [0.05, 0.40] 边界拘束)
  • artifacts/alpha_sweep.csv / lookback_sweep.csv:敏感性原始数据

解读:动量在 A股 ETF 上是否有效

结论:在本组 6 只宽基/行业 ETF 池 + 2020-2024 窗口上,横截面 20 日动量倾斜在统计与经济意义上都不成立。

  • 方向不稳定:5 个年度只有 2024 一个明显正贡献年份,2020 一年抹掉所有 alpha。
  • 统计未显著但持续负:t-stat ≈ -1.08,经典阈值(|t|>2)下不能拒绝 0,但 5/5 个 α、3/3 个 lookback 全部输 → 不是「随机噪声」而是系统性的反向
  • 池内分散度太低:6 只 ETF 都是 A股宽基/行业,相关性本就高(截面 σ 小、z-score 信息量有限),加上单边市场(2020、2022)下 6 只齐涨齐跌,「相对动量」几乎等价于过去几周的 noise
  • 短期反转在 A股 ETF 上盖过中期动量:lookback=20 是最差的一档,10 天/60 天稍好——和 A股「短期均值回复 + 中长期趋势」的传统经验吻合。skip=21(1 个月)的「跳过最近一个月」改进未在本轮启用,是潜在补救方向。
  • 目标 turnover 426%/年的成本侵蚀:fees+slippage = 万 5.5,单边换手 4.26 倍 → 每年 ~0.23% 显式成本,不算大头但累计 5 年 ~1.2%,把 -1.05% 的 BH 缺口的一部分填回,但信号本身就是负的,省成本救不回来。

解读:Bug / 问题

  • 未发现源码 bug。S4 的 target_weights 输出严格非负、和=1、上下限生效(已经过 smoke 与 real 双重验证)。
  • 目标周转的口径选择:S3 因为目标权重恒为 1/N,“目标周转”=0;但 S3 实际仍因漂移触发再平衡换手——更精确的 turnover 需要从 vbt pf.trades 抽实际成交。当前指标只用于 S4 自身的相对比较是合理的,跨策略对照需谨慎。
  • 回测口径(1+r).cumprod() 总收益与 vbt pf.total_return() 在第一日 NAV 点的处理上略有差异(自洽即可,三条线统一口径)。

下一步

  • MomentumReturn(skip=21)(跳过最近 1 月,避开短期反转)后再跑一轮
  • 试「波动率调整动量」(mom / vol,risk-adjusted),可能比纯动量更稳健
  • 把池子扩到 ≥ 12 只 ETF(加风格/行业)以提高横截面分散度
  • 考虑把 S4 改成「动量 × 趋势过滤」的二元信号——只在大盘月度均线之上倾斜,跌破时退化为 S3 等权。这是 v2 的方向
  • 当前结论:v1 配置(α=1.0、lookback=20)→ 搁置;以等权 S3 为 baseline,等更稳健的因子/信号再继续

源文件


本文由 scripts/sync_strategies.pyStrategy-Lib 同步生成。