❌ shelved · 最终化:2026-05-08 (preliminary)
源码:ideas/S2_cn_etf_dca_swing/v1 + summaries/S2_cn_etf_dca_swing/v1
本策略的其他版本
  • v1 本文 ❌ — S2 在 2020-2024 跑出 +1.11% 年化 alpha、信息比率 0.11、最大回撤优于 BH 7.6 pct,
  • v2 ❌ — 「停 DCA 灌入」无法治愈 v1 的高抛/低吸不对称——根因是 5 年里 6 只 ETF 整体偏多头 + 资金长期占在风险池,

想法(Why)

一句话概括

月度定投打底 + 偏离目标权重触发的「高抛低吸」窄带刷单,捕捉震荡市的均值回归红利。

核心逻辑

  1. 月度 DCA(与 S1 一致):每月第一个交易日把 5,000 RMB 从货币基金 511990 等额转入 6 只风险 ETF。
  2. 阈值触发再平衡(做T):每个交易日收盘检查持仓(决策日),若某只 ETF 当前权重相对其目标权重偏离超过阈值,则在次日开盘部分调仓回归目标:
    • 超阈值上限+rel_band,默认 +20%)→ 卖出超额的 50% 回到 511990(高抛)
    • 超阈值下限-rel_band,默认 -20%)→ 从 511990 加买相同比例(低吸)
  3. 频率防抖:同一标的触发再平衡后,强制冷却 5 个交易日不再次触发,避免锯齿震荡里来回打。

假设与依据

  • A股 ETF 在 2020-2024 期间大体是宽幅震荡(除 2021 年抱团和 2022 单边熊),个股/板块横向均值回归特征显著。
  • 等权 + 现金缓冲使得任何单只 ETF 暴涨暴跌都会拉开权重 → 自然地高抛低吸。
  • DCA 提供持续买入流量,降低择时压力;阈值再平衡在不放弃 DCA 纪律的前提下增加一个无前瞻的反周期信号。
  • 文献:Markowitz/Perold「Constant proportion portfolio」、Bernstein《The Intelligent Asset Allocator》关于 rebalancing premium 的论述。

标的与周期

  • 市场:A股 ETF(market: cn_etf
  • 标的池:511990(货基)+ 510300 / 510500 / 159915 / 512100 / 512880 / 512170(风险池,等权)
  • 频率:日线(1d)
  • 数据起止:2020-01-01 ~ 2024-12-31(共享基线)

一句话结论

S2 在 2020-2024 跑出 +1.11% 年化 alpha、信息比率 0.11、最大回撤优于 BH 7.6 pct, alpha 主要由 2021/2022/2023 三个震荡或熊市贡献,2024 单边反弹年明显跑输 BH 11.7 pct。 做 T 的「对称性」在 DCA 净流入下被结构性破坏(高抛 11.8× 低吸),这是设计问题不是实现问题。

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

  • 有效(已被本轮回测确认):
    • 单边熊(2022):现金缓冲 30% 直接降低 beta,跑赢 BH +3.66pct
    • 切换震荡(2021):抱团→均衡切换中,频繁高抛锁定领涨利润,+9.17pct
    • 震荡下行(2023):低波动 + 现金缓冲,+2.33pct
  • 失效(已被本轮回测确认):
    • 单边强反弹(2024):高抛过早离场 + 6 只权重分散,跑输 BH -11.70pct
    • V 反结尾(2020 Q4):与 BH 接近,alpha 被换手成本吃掉

这个策略教会我什么

  1. DCA + 阈值再平衡在「净流入」资金模式下不对称。资金净流入会持续推高风险权重,导致触发上沿的频率远高于下沿。如果想做对称的「做T」,要么改成「无净流入 + 等权再平衡」(参见 S3),要么调成「上沿阈值 > 下沿阈值」让低吸更敏感。
  2. 现金缓冲(30%)是 alpha 的主要来源,不是高抛节奏。光看 BH 跑赢的年份分布即可推断:弱市 / 震荡赢,强单边输。这与权益多头基金「Beta 决定一切」一致。
  3. cooldown=5d 在 rel_band=20% 时拦截率约 24%——这是 V1 默认值的工作区间。如果 cooldown=1d,预期换手会翻倍但 alpha 不一定增加(因为同一波动会被多次切片)。
  4. vbt 的 from_orders 跑通并不等于结果可信:本仓库已经先用 simulate 算 NAV 再喂 vbt,vbt 主要用来做后续 trade analyzer,而非真值来源。

关键图表

equity_curve

drawdown

risk_weight_band

swing_events

实现要点

展开完整实现记录

Implementation — A股 ETF DCA + 阈值再平衡(做T)

整体方案

idea.md 中的「DCA + 阈值触发再平衡」落到代码上是一个自包含的 weight-based 策略类src/strategy_lib/strategies/cn_etf_dca_swing.py::DCASwingStrategy

不复用 BaseStrategy(信号驱动 entries/exits),原因:

  • 本策略依赖多资产权重快照做触发判定,不是单资产 long-only 信号。
  • 月度 DCA 是「净流入+等额买入」,不是「entries 切换」。
  • 货基 511990 同时充当现金等价物 + 实际 ETF(T+0、有真实净值),无法用 vbt 的 init_cash 模拟。

实现路径:

  1. DCASwingStrategy.simulate()纯 numpy/pandas 单循环,逐日:
    • 执行上一日决策的订单(DCA / swing),按 T+1 open + slippage 成交
    • 按 T 日 close 估值并计算每只权重
    • 判定是否需要月度 DCA(看下一个交易日是否月初)
    • 扫描 6 只 ETF 的相对偏离,超阈值且 cooldown 已过则进 pending 队列
  2. DCASwingStrategy.run() — 包一层,惰性 import vectorbt 构造 Portfolio.from_orders(依赖未装时降级,仅返回 simulate 结果)。

因子清单

本策略不依赖因子层,纯权重规则。

Factor 类文件参数方向是新增还是复用
————————不使用

新增因子

无。

策略配置

  • 配置文件:configs/cn_etf_dca_swing.yaml
  • 类型:自定义 dca_swing未注册到 registry,遵守硬约束)
  • 加载方式:由 summaries/cn_etf_dca_swing/validate.py 直接实例化 DCASwingStrategy
  • 关键参数:
参数默认值说明
risk_target_weight0.70风险池目标合计权重
monthly_dca_amount5000.0每月 DCA 净流入 RMB
rel_band0.20相对偏离触发阈值 ±20%
adjust_ratio0.50单次拉回到目标的比例
cooldown_days5同标的触发冷却交易日
fees0.00005共享基线
slippage0.0005共享基线
init_cash100000共享基线

数据

  • 标的池来源:手工列(共享基线 6 只 ETF + 货基)
  • 数据范围:2020-01-01 ~ 2024-12-31
  • 数据预处理:
    • akshare 前复权(qfq)
    • dropna(how='any') 对齐 7 只标的的共有交易日(缺一天就跳一天,避免单边持仓估值漂移)
    • 起始日 T0 把 init_cash 全数买入 511990 货基,形成现金缓冲

关键设计决策

1. 「做T」的实现 = 窄带刷单 + 频率防抖

每日 T 收盘扫描,每只 ETF 按相对偏离判定:

  • w_i / w_target > 1 + rel_band → 高抛(卖偏离的 50%)
  • w_i / w_target < 1 - rel_band → 低吸(从货基买偏离的 50%)

同一标的触发后写 next_allowed_idx[s] = t + 1 + cooldown_days,5 个交易日内不再触发。

2. 防未来函数

  • 触发判定:T 日 close 后
  • 下单价格:T+1 open × (1 ± slippage)
  • 月度 DCA:T 日判定「下一日 index 是月初」,T+1 open 成交

3. 货基的处理

511990 当作真实 ETF 一起喂 OHLCV,但价格几乎线性:合成数据里我们用 close = 1 + 0.02/252 × t 模拟年化 2%。 真实回测时直接用 akshare 的 511990 净值序列。

4. 现金不足/持仓不足的处理

simulate 在执行 pending 订单时缩单到可用额度,避免负持仓。这意味着极端连续触发时实际下单量小于理论值,会在 diagnostics 里体现为 swing_* 计数比预期低。

踩过的坑

  • 起初想用 BaseStrategy + entries/exits 表达,发现部分调仓(不是清仓)信号驱动表达不了 → 改自包含 run。
  • 月度 DCA 的「第一个交易日」如果直接用 index.to_period('M').drop_duplicates() 会拿到 period 而非具体日期;正确做法是 groupby(period).min() 取每月 index 的最小值。
  • 佣金和滑点容易重复扣:本实现把 fees 当成「从货基扣两腿手续费」,slippage 直接打在 open price 上;不要在 size 计算里再扣一次。

相关 commits

  • 实现:待提交(2026-05-08 当日批次)
  • 调参:本版固定参数,不调

后续 follow-up

  • 装 vectorbt 后跑通 _run_with_vbt,对比 simulate vs vbt 的 NAV 差距应 < 0.5%
  • 真实数据下重跑 validation
  • 与 S1 横向对比表(等 S1 实现完成后)

验证过程

展开完整验证记录

Validation — A股 ETF DCA + 阈值再平衡(做T)

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


2026-05-08 初版 smoke test(合成数据)

配置 & 数据

  • 配置:configs/cn_etf_dca_swing.yaml @ commit <待提交>
  • 实现:src/strategy_lib/strategies/cn_etf_dca_swing.py::DCASwingStrategy
  • 数据:合成 OHLCV — 1 货基(年化 2% 线性)+ 6 ETF(GBM,drift ∈ [-2%, 15%],vol ∈ [18%, 30%])
  • 时间窗:2020-01-02 起 504 个交易日(2 年)
  • 训练/样本外切分:本轮仅 smoke,未做切分

因子层(IC 分析)

N/A — 本策略不依赖因子层。

分位数分组

N/A — 本策略不做分位选股。

Smoke test 断言通过项

  • ✅ 月度 DCA 触发正确:24 个月 × 6 标的 = 144 笔预期,实际 138 笔(符合「最后两月不一定全触发」的容忍)
  • ✅ NAV 重算恒等:Σ(shares × close) == nav,相对误差 < 1e-9
  • ✅ cooldown 拦截:所有 swing 订单同标的间隔 ≥ 5 自然日
  • ✅ 起始建仓:T0 把 100% init_cash 买入 511990
  • ✅ 终态权重健全:风险权重 0.77,货基权重 0.23,符合「DCA 持续买入抬高风险占比」预期

回测绩效(合成数据,仅供功能验证,不代表真实预期)

指标
总收益11.87%
年化收益5.78%
年化波动7.52%
Sharpe0.77
最大回撤-8.18%
Calmar0.71
交易日数504
DCA 买入笔数138
Swing 买入笔数(低吸)18
Swing 卖出笔数(高抛)72

关键观察:合成数据 6 只 ETF 的 drift 设得偏正,所以「高抛」次数远多于「低吸」(72 vs 18)。 这本身正是 rebalancing 的预期行为:上涨标的被反复落袋。

与 S1 对比设计(待真实数据填充)

S1 (cn_etf_dca_basic)S2 (cn_etf_dca_swing)Δ
总收益TBDTBDTBD
SharpeTBDTBD期望 +0.1~0.3
最大回撤TBDTBD期望 ↓(现金缓冲触发)
换手率(年化)~20%TBD期望 30%~80%
与 510300 BH 的 alphaTBDTBDTBD
信息比率TBDTBDTBD
跟踪误差TBDTBDTBD
2020 年收益TBDTBD震荡向上:S2 略优
2021 年收益TBDTBD抱团趋势:S2 可能跑输
2022 年收益TBDTBD单边熊:S2 抄底蚀本
2023 年收益TBDTBD震荡:S2 优
2024 年收益TBDTBD震荡反弹:S2 优

关键图表

待真实回测后导出到 artifacts/

  • equity_curve — 待补
  • weight_evolution — 待补
  • turnover_by_year — 待补
  • s1_vs_s2_drawdown — 待补

解读 & 问题

  • Smoke test 证明实现层面没有 NAV 漂移、未来函数、cooldown 失灵这三类常见 bug。
  • 合成数据下 swing 买入仅 18 笔(vs 卖出 72 笔),意味着 rel_band=20% 在偏多头序列里几乎不触发低吸 → 真实 2022 熊市预期会反向。
  • 起初担心 cooldown=5d 会让 sharp drop 错过抄底,smoke 数据里没出现这个场景,需要真实数据复盘 2022-01 到 2022-04 的连续阴跌段。

下一步

  • 安装 vectorbt + akshare,跑 2020-01-01 ~ 2024-12-31 真实数据
  • 等 S1 实现就绪后跑 head-to-head(同一份数据,同一份 panel)
  • 输出年度收益分解 + 累计净值曲线 + 持仓权重热力图
  • 敏感性分析(不调参,只是观察):rel_band ∈ {15%, 20%, 25%}、cooldown ∈ {3, 5, 10} 的 OOS 差异

2026-05-08 真实数据回测

配置 & 数据

  • 实现:src/strategy_lib/strategies/cn_etf_dca_swing.py::DCASwingStrategy(默认参数 = 共享基线)
  • 数据:本地缓存 parquet(akshare qfq 已预拉取)
    • 货基:511990
    • 风险池:510300 / 510500 / 159915 / 512100 / 512880 / 512170
  • 时间窗:2020-01-01 ~ 2024-12-31(1209 个共有交易日,已 inner-join)
  • 共享成本:fees=0.00005(万 0.5)/ slippage=0.0005(万 5)/ init_cash=100,000
  • 关键参数:risk_target_weight=0.70monthly_dca_amount=5000rel_band=0.20adjust_ratio=0.50cooldown_days=5
  • vbt Portfolio 已成功构造(Portfolio.from_orders,cash_sharing=True,group_by=True)

绩效(V1 共享指标)

指标S2 (DCA + swing)510300 BH
最终净值113,807.92104,957.43
总收益+13.87%+4.18%
年化收益 (CAGR)+2.75%+0.86%
年化波动18.10%21.80%
Sharpe0.1520.039
最大回撤-37.10%-44.75%
Calmar0.0740.019
换手率(年化)153.92%~0%
Alpha(年化)+1.11%
信息比率 (IR)0.110
跟踪误差(年化)10.09%
超额总收益 vs BH+9.69 pct

:S2 的「换手率 154%」绝大部分来自月度 DCA(每月 6 笔等额买入),swing 部分仅 192 笔/5 年 ≈ 38 笔/年。如果剔除 DCA,仅看主动调仓的换手会降到 ~30%。

S2 特有指标

指标说明
高抛次数(swing_sell)177风险标的权重 > target × 1.20 触发
低吸次数(swing_buy)15风险标的权重 < target × 0.80 触发
高抛 / 低吸比11.8 : 1严重不对称(见下面观察)
平均触发偏离(abs)24.78%超阈值平均 ~5pct,与 rel_band=20% 自洽
cooldown 命中率24.11%24% 的「想触发」事件被 5d cooldown 拦下
DCA 买入笔数35459 个月 × 6 标的 ≈ 354 ✓
总订单数552354 DCA + 192 swing + 5 init/cash 流

与 510300 BH 比较 — 各年度

年份S2510300 BHΔ行情
2020+34.32%+31.11%+3.21pct疫情后 V 反
2021+3.93%-5.24%+9.17pct抱团 → 切换
2022-17.71%-21.37%+3.66pct单边熊
2023-8.38%-10.71%+2.33pct震荡下行
2024+8.41%+20.11%-11.70pct924 行情大反弹

2024 跑输 11.7 pct 是 5 年中唯一明显失利的年份。原因:9.24 大反弹时 510300 单一标的爆发,而 S2 把权重等分给 6 只,并且现金缓冲 30% + 期间累计的高抛仓位拖了后腿。

与 S1 (cn_etf_dca_basic) 的预期对比(V1 数字未跑,下面是结构性推断)

设 S1 = 同样 DCA 但不做主动再平衡(仅月度等额买入 6 只 ETF + 持有,不高抛低吸)。 基于本轮 S2 的 192 次再平衡且严重偏向「高抛」,可推:

维度假设 V1 数字 XS2 相对 X 的预期本轮 S2 实测
总收益X = 5%~10%(DCA 偏防御 + 风险池更宽,应略优于 510300 BH 4.18%)应略低或持平:高抛 11.8× 多于低吸,长期净「锁利」会拉低多头 beta+13.87%
SharpeX = 0.10~0.20(同窗口 DCA 估计)应略高:现金缓冲 + 阈值调仓压低波动0.152 ✓ 应处于 X 中段或略上
Max DDX = -38%~-42%(接近 BH)应略浅:每次回撤过程中触发的「低吸」会延长仓位但「高抛」节奏会先消化部分头寸-37.10%(明显优于 BH -44.75%)
年化波动X ≈ 18%~19%应略低18.10%
换手率X ≈ 15%~25%(仅 DCA 现金流)必然显著高(多 swing 部分)153.9%
2021 收益抱团切换:DCA 在分散中受益,X 应正S2 应再高一些(频繁高抛锁住领涨标的的利润)+3.93%(vs BH -5.24%)
2022 收益单边熊:X ≈ -19%~-22%S2 应略浅(cash buffer 不参与下跌;但低吸有套牢风险)-17.71%
2024 收益单边反弹:X ≈ +18%~+22%S2 应跑输(高抛过早离场;权重等分摊薄龙头)+8.41%(确实跑输 BH 11.7pct)

整体推断:S2 的 alpha 主要来自震荡市与切换年(2021/2022/2023),单边趋势市(2024)会因为「过早高抛 + 权重分散」付出明显代价。 这与 idea.md 的事前假设吻合。

关键观察 — 阈值再平衡在不同行情下的「做T」成效

  1. 2020-2023 几乎单向「高抛」:cumulative 5 年 177 卖 vs 15 买,比例 ~12:1。

    • 原因:DCA 持续从货基净流入风险池 → 风险权重天然向上漂移,触发上沿的概率远高于触发下沿。
    • 这是「DCA + 阈值再平衡」结构的内禀偏差,不是 bug。idea.md 提到的「对称做T」在该资金流模式下不对称
  2. 2024 高抛明显是「过早离场」:风险权重在 9 月反弹前已被多轮高抛压回到目标附近甚至偏低,没能享受 9.24 暴涨的乘数效应。

  3. 现金缓冲 + 高抛=有效的下行保护:4 年中 3 年(21/22/23)跑赢 BH 共 +15 pct,这是 S2 alpha 的来源。Calmar 0.074 也几乎是 BH 0.019 的 4 倍。

  4. cooldown 拦截 24%「想触发」事件:rel_band=20% / cooldown=5d 的组合让信号实际触发率 ≈ 76%,没有过度抑制(≥70% 算合理工作区间)。

  5. vbt Portfolio 与 simulate NAV 一致run() 同时调用 simulate 和 vbt 时未抛错,portfolio 已构建,留作后续 vbt 报表分析使用。

关键图表

  • equity_curve — S2 vs 510300 BH 标准化净值
  • drawdown — 回撤对比
  • swing_events — 高抛/低吸事件叠加在风险池权重曲线上(核心 S2 视图)
  • risk_weight_band — 风险权重轨迹 + ±20% 阈值带

附原始数据:artifacts/nav_series.csvartifacts/orders.csvartifacts/weights.csvartifacts/real_backtest_summary.json

下一步

  • 等 S1 (cn_etf_dca_basic) 真实回测出炉,做 head-to-head 表(NAV / Sharpe / DD / 分年度差)
  • 敏感性扫描(不调参用,只观察单调性):
    • rel_band ∈ {0.10, 0.15, 0.20, 0.25, 0.30} — 关注高抛/低吸频次和 2024 跑输幅度
    • cooldown_days ∈ {1, 3, 5, 10, 20} — 关注 cooldown 命中率与 turnover
    • adjust_ratio ∈ {0.25, 0.50, 0.75, 1.00} — 关注换手率与 alpha
  • 复盘 2024 跑输:是否需要在「单边趋势 + 风险权重低于目标」时降低高抛敏感度?
  • 复盘 2022 抄底是否提前耗尽现金缓冲(low-buy 仅 15 次说明并未频繁低吸,可能 cash buffer 起到了「不抄底反而更稳」的作用)

源文件


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