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 微幅倾斜到强势资产。
核心逻辑
- 标的池固定为 Benchmark Suite V1 共享的 6 只风险 ETF(沪深300/中证500/创业板/中证1000/证券/医疗),起点权重均为 1/6。
- 每个再平衡日(默认每 20 个交易日)计算各标的的动量得分(
MomentumReturn(lookback=20),可叠加MomentumReturn(lookback=60))。 - 把动量得分横截面 z-score 化,按 z-score linear tilt 公式生成目标权重:
w_i = 1/N + α * z_i / N。 - 应用上下限:
w_i ∈ [0.05, 0.40],再归一化使sum(w) == 1。 - 仍然 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 为负 → 不是参数问题,是信号本身在这个池子上失效。
这个策略教会我什么
- 池子分散度比因子参数更重要。6 只 A股 ETF 的横截面 σ 太小,z-score 的信息密度被相关性吃掉了。下次做 cross-section 动量先看池内 pairwise correlation。
- A股 ETF 上短期反转盖过中期动量。lookback=20 是最差的一档,符合传统经验中 A股一个月内的均值回复倾向。下次试动量先
skip=21。 - t-stat 不显著但方向稳定也是有用信号。|t|=1.08 < 2 不能拒绝 0,但 8/8 (5α + 3lb) 实验全输等权 → 该结论有 8 次独立的「方向一致」证据,足以判定 v1 配置不可上线。
- 「目标权重恒定」策略的 turnover 不能用目标权重差衡量。S3 目标周转=0 但实际有漂移再平衡,下次跨策略 turnover 比较直接读 vbt
pf.trades.records。 - 事前指标预期值是有用的。事前 IR 估计 0.2~0.5,实测 -0.49 → 偏离 ~1σ 以上 → 直接说明「我们对这个市场有错误先验」,比单看绝对收益更易识别问题。
关键图表




实现要点
展开完整实现记录
Implementation — A股 ETF 等权 + 动量倾斜
整体方案
派生自 Strategy 3 的 EqualRebalanceStrategy,仅覆盖 target_weights(date, prices_panel) 钩子。整体仍是「6 只 ETF 满仓 + 每 20 个交易日再平衡」,本策略的差异完全集中在「目标权重怎么算」:
- 在 rebalance 日,对面板里每个 symbol 计算
MomentumReturn(lookback=20)在date当天的值。 - 横截面 z-score 标准化(NaN 当作 0)。
- 线性倾斜:
w_raw_i = 1/N + alpha * z_i / N。 - 用「水位线迭代」处理
[w_min=0.05, w_max=0.40]上下限:每轮按 sum=remaining 缩放未钉死资产,识别最严重的越界并钉死到边界,重复直至收敛或 free 集为空。 - 输出权重严格满足
sum(w)=1、w_i ∈ [w_min, w_max]、非负。
可选:启用 secondary_lookback=60,主+次动量各自横截面 z-score 后等权融合。
因子清单
| Factor 类 | 文件 | 参数 | 方向 | 是新增还是复用 |
|---|---|---|---|---|
MomentumReturn | src/strategy_lib/factors/momentum.py | lookback=20 | +1 | 复用 |
MomentumReturn(可选) | src/strategy_lib/factors/momentum.py | lookback=60 | +1 | 复用 |
新增因子(如有)
无。notes.md 中提议了 MomentumRiskAdjusted(动量/波动率),暂不实现。
策略配置
- 配置文件:
configs/cn_etf_momentum_tilt.yaml - 类型:
momentum_tilt(自定义;与cs_rank等内置类型并列) - 关键参数:
tilt.method=zscore_linear、tilt.alpha=1.0、tilt.w_min=0.05、tilt.w_max=0.40、rebalance=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_etfloader 的前复权(akshareqfq)
与 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‰/天)
- 父类:
EqualRebalanceStrategystub(S3 subagent 并行落地中,以保证本策略可独立验证) - 入口脚本:
summaries/cn_etf_momentum_tilt/validate.py(python3 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) |
| 2 | alpha=0 | 退化为等权 (1/6=0.1667) | 全部 0.1667 |
| 3 | alpha=10(极端倾斜) | 上下限收住,top-2 顶到 0.40 | top 2 个都达到 0.40,bottom 4 个达到 0.05 |
| 4 | secondary_lookback=60 | sum=1、上下限生效 | sum=1,max=0.3966,min=0.05 |
| 5 | 数据不足 5 行(动量算不出) | 降级为等权 | 全部 1/6 |
| 6 | 单 symbol panel | 100% | {"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),akshareqfq前复权,缓存命中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(与 vbttotal_return略有差异,因前者不扣初始建仓的佣金/滑点) - 共享基线:100k RMB 起步,fees 万 0.5,slippage 万 5(V1 标准)
主结果(α=1.0, lookback=20, rebal=20)
| 策略 | Final NAV | Total Ret | CAGR | Vol | Sharpe | MaxDD | Calmar |
|---|---|---|---|---|---|---|---|
| S4 momentum_tilt | 0.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 BH | 1.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 |
| TE | 11.18% |
| 超额 t-stat | -0.21 (n=1209) |
S4 的绝对水平比 510300 BH 略差(-1% 年化),但波动/回撤都更高;从「绝对基准」看也没有跑赢。
分年度收益(2020 ~ 2024)
| 年份 | S4 momentum_tilt | S3 equal_rebal | 510300 BH | S4 − S3 | S4 − 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)
| α | CAGR | Sharpe | MaxDD | vs S3 α_ann | vs S3 IR | vs S3 t | mean|Δw| | 目标周转 |
|---|---|---|---|---|---|---|---|---|
| 0.0 | 2.38% | 0.217 | -45.18% | -0.00% | -0.10 | -0.23 | 0.000 | 0.00 |
| 0.5 | 0.54% | 0.141 | -47.48% | -1.79% | -0.48 | -1.06 | 0.061 | 2.98 |
| 1.0 | -0.30% | 0.108 | -48.73% | -2.56% | -0.49 | -1.08 | 0.092 | 4.26 |
| 2.0 | -0.51% | 0.100 | -49.04% | -2.72% | -0.49 | -1.07 | 0.106 | 4.59 |
| 5.0 | -0.36% | 0.107 | -48.77% | -2.54% | -0.44 | -0.97 | 0.112 | 4.65 |
α=0 是最优档位,意思是直接用 S3 等权——任何非零的动量倾斜都把 Sharpe 从 0.217 拖到 ~0.10。CAGR 与 IR 关于 α 单调递减并在 α≈2 触底,更大的 α 反而被边界 clip 救回一点,但仍显著落后等权。
Lookback 敏感性(α=1.0, rebal=20)
| lookback | CAGR | Sharpe | MaxDD | vs S3 α_ann | vs S3 IR | vs S3 t | mean|Δw| |
|---|---|---|---|---|---|---|---|
| 10 | +0.89% | 0.157 | -44.97% | -1.34% | -0.27 | -0.58 | 0.089 |
| 20 | -0.30% | 0.108 | -48.73% | -2.56% | -0.49 | -1.08 | 0.092 |
| 60 | +1.62% | 0.187 | -48.53% | -0.67% | -0.14 | -0.30 | 0.087 |
三档 lookback 全部 vs S3 IR 为负。最差的是 20 天(中期动量 + 短期反转交叠),10/60 天稍好但仍未超 S3。没有任何 lookback 使动量倾斜对等权产生正 alpha。
关键图表
artifacts/equity_curve.png:三条线,S4 蓝 vs S3 绿 vs BH 红。可见 S3 全程在 S4 之上,分歧主要发生在 2020Q3-Q4 与 2024H2artifacts/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()总收益与 vbtpf.total_return()在第一日 NAV 点的处理上略有差异(自洽即可,三条线统一口径)。
下一步
- 试
MomentumReturn(skip=21)(跳过最近 1 月,避开短期反转)后再跑一轮 - 试「波动率调整动量」(
mom / vol,risk-adjusted),可能比纯动量更稳健 - 把池子扩到 ≥ 12 只 ETF(加风格/行业)以提高横截面分散度
- 考虑把 S4 改成「动量 × 趋势过滤」的二元信号——只在大盘月度均线之上倾斜,跌破时退化为 S3 等权。这是 v2 的方向
- 当前结论:v1 配置(α=1.0、lookback=20)→ 搁置;以等权 S3 为 baseline,等更稳健的因子/信号再继续
源文件
- 想法 · idea.md
- 讨论笔记 · notes.md
- 结论 · conclusion.md
- 实现 · implementation.md
- 验证 · validation.md
- 可复跑脚本 · validate.py
- 本版本目录(含 artifacts)
本文由 scripts/sync_strategies.py 从 Strategy-Lib 同步生成。