/~learn/s/fe08d80c
> 2026-08-11 · v2 重大调整:回测是第一公民。v1 把回测弱化成两个"验证任务",v2 改为回测驱动开发(Backtest-Driven Development)——先量化现状差距(基线回测)→ 实现 → 回测对比 → 不达标不进下一阶段。 > > 配套探讨文档:2026-08-11-牛熊判断与牛市仓位优化探讨.md(v3,讲"为什么这么设计") > > 本文回答三个问题: > 1. 迁移成本有多大? —— 零迁移成本,纯加法 overlay(详见第一章)。 > 2. 回测怎么做? —— 这是本文重头戏:基线回测先行、Walk-Forward 防过拟合、分市场状态拆解、图表化报告(详见第二章)。 > 3. 怎么一步步落地? —— 四阶段 15 任务,每个阶段以"回测达标"为进入下一阶段的硬门槛(详见第三章)。
---
timing_config.py (硬编码注册30+策略)
↓
show_marker.py L2263 for code in codes_to_process:
└─ load_mult_market_strategy(...) # L2278 ★ 唯一核心入口
├─ calculate_portfolio_metrics() # 逐策略信号→加权日收益
├─ calculate_average_holding_ratio() # ★ position% = Σ(stock_value_ratio × weight),0~100
├─ 存 last_timing_positions[code] # L161-185 {predicted/today/yesterday}
└─ 返回推送文本
↓
推送消息 L2297~2951 / 网页"今日仓位" / 持久化DB / 回测图表 ← 全部读 last_timing_positions
position 是 0~100 百分比(market_data_processer.py L475);last_timing_positions 这一个快照读取 → overlay 只需在"position 算完→快照存储"之间做后处理,下游全部自动受益。
| 模块 | 改动 |
| --- | --- |
30+ 策略池 / Strategy 基类 / timing_config.py | ❌ 零改动 |
| 截面选基 / 推送链路 / 每日调度(run_pushd.sh) | ❌ 零改动 |
MarketAnalysis.load_mult_market_strategy | ⚠️ 插入 1 个受开关保护的调用(默认关) |
新增 timing_overlay/ 包 | 🆕 L1/L2/L3/合成器 + 回测引擎 + 报告生成 |
| 测试 | 🆕 unittest(项目现状),补齐策略零测试空白 |
---
这套优化要回答的是数据问题,不是代码问题:
不先做基线回测,就没有"改之前的真相";不对比回测,就没有"改之后的有效证明"。
data_cache/ 里是全历史日线缓存(run_data_fetcher.py 每日 08:45/23:10 全量刷新),不是近期窗口:
| 品种 | 缓存行数 | 约覆盖 |
| --- | --- | --- |
| 399102.SZ 创业板综 | 3935 行 | ≈2010 至今(16 年) |
| 510880 红利ETF | 4745 行 | ≈2007 至今(19 年) |
| 其余 23 个已配置品种 | 各有 tushare/adata 双源 | 多数覆盖 2010+ |
结论:Walk-Forward 回测所需 2015-01~2026-08 数据已就位,无需新拉数据(走 DataCacheManager.load_best,见 data_cache.py)。阶段三涉及的 QQQ/GLD(alpha 源)在数据准备任务中单独核查。
| 项 | 口径 |
| --- | --- |
| 数据 | DataCacheManager.load_best(code, start, end),与生产同一缓存 |
| 策略 | timing_config.build_allocation_manager() 现成策略池,参数不动(只验证 overlay,不重调策略参数) |
| 信号 | 直接用现有 position 列(内部已有 signal_offset shift 机制,避免未来函数),overlay 只改这一列 |
| overlay | 逐日状态机:apply_overlay_series(code, df, pos_raw) 每日用"当日及以前"数据更新状态 |
| 品种 | 全部 23 个已配置品种(含 short- 空头变体) |
| 周期 | 2015-01 ~ 2026-08(11.5 年,覆盖 2019-21 牛市 / 2022 熊市 / 2023-24 震荡 / 2025-26 牛市) |
每品种、每窗口、现状 vs overlay 两套,输出同一套指标:
| 类别 | 指标 |
| --- | --- |
| 收益 | 总收益、年化收益、超额收益(vs 大盘) |
| 风险 | 最大回撤、平均回撤、最长回撤持续期(关键!验证低回撤特质) |
| 效率 | 夏普、索提诺、卡玛(收益/最大回撤) |
| 择时 | 择时增值(策略收益−大盘收益×平均仓位)、平均仓位、牛市参与度(牛市期平均仓位) |
| 行为 | 状态切换次数/年(验证"震荡市不被磨"的核心证据)、trailing stop 触发次数、触发后 30 日恢复率 |
| 市场状态拆解 | 分牛市/熊市/震荡三态的收益与回撤(直接检验质疑 1、2) |
每阶段回测生成一份报告,放在 learn/投资复盘/(与实盘复盘报告同级,自动收录文档库):
格式沿用实盘复盘报告风格(深色主题、内嵌 SVG、可分享截图)。
---
> 节奏:每阶段 = 先建回测基线 → 再实现 → 回测对比 → 达标才进下一阶段。所有任务 TDD(先写失败测试→实现→跑通→commit),unittest 风格。
---
Files:
adata/aohan/quant/timing_overlay/backtest/engine.pyadata/aohan/quant/timing_overlay/backtest/metrics.pytests/adata_test/quant/test_backtest_metrics.pyStep 1: 写失败测试(指标计算的纯函数)
# tests/adata_test/quant/test_backtest_metrics.py
import unittest
import numpy as np
from adata.aohan.quant.timing_overlay.backtest.metrics import (
compute_drawdown, sharpe, sortino, calmar, timing_value_added)
class TestMetrics(unittest.TestCase):
def test_drawdown(self):
# 净值 1.0 → 0.9 → 1.0:最大回撤 10%
nav = np.array([1.0, 1.1, 0.99, 1.05]) # 高点1.1 → 低点0.99 = 10%
dd = compute_drawdown(nav)
self.assertAlmostEqual(dd.max(), 0.10, places=2)
def test_sharpe_annualized(self):
# 每日收益率恒定 0.001,年化夏普应 > 0
daily_ret = np.full(252, 0.001)
s = sharpe(daily_ret)
self.assertGreater(s, 0)
# 无波动率时应返回 0(不炸)
self.assertEqual(sharpe(np.zeros(100)), 0.0)
def test_timing_value_added(self):
# 策略收益 10%,大盘 15%,平均仓位 50% → 择时增值 = 10-7.5 = 2.5
v = timing_value_added(strat_ret=0.10, bench_ret=0.15, avg_pos=0.5)
self.assertAlmostEqual(v, 0.025)
Step 2: 跑失败 — env -u PYTHONHOME -u PYTHONPATH .venv/bin/python -m unittest tests.adata_test.quant.test_backtest_metrics -v
Expected: FAIL(ModuleNotFoundError)
Step 3: 实现 — metrics.py:drawdown(rolling max)、sharpe(日收益×√252)、sortino(下行波动)、calmar(年化/最大回撤)、timing_value_added(择时增值定义);engine.py:load_backtest_df(code, start, end) 封装 DataCacheManager.load_best + run_baseline(code) 调 load_mult_market_strategy 取 processor.df['position']。
Step 4: 跑通过
Step 5: Commit — git commit -m "feat: 回测引擎骨架与指标计算"
---
Files:
adata/aohan/quant/timing_overlay/backtest/walk_forward.pyscripts/run_backtest_baseline.py(或放 timing_overlay/backtest/,遵项目习惯)Step 1: 写失败测试(walk_forward runner:给定窗口列表,输出每窗口指标 dict)
# tests/adata_test/quant/test_walk_forward.py
import unittest
from adata.aohan.quant.timing_overlay.backtest.walk_forward import (
WINDOWS, build_windows)
class TestWalkForward(unittest.TestCase):
def test_seven_windows(self):
self.assertEqual(len(build_windows()), 7)
# 每窗口:训练集早于验证集
for w in build_windows():
self.assertLess(w['train_end'], w['valid_start'])
Step 2: 跑失败 → Step 3: 实现 — 7 窗口(2015-18 训练/2019 验证 … 2015-24 训练/2025-26 验证,见探讨文档第八节);对 23 品种 × 7 窗口跑 run_baseline,输出每窗口的指标表。
Step 4: 跑通 — 性能注意:23 品种 × 30+ 策略 × 10 年,单次全库可能数分钟到数十分钟;用 ThreadPoolExecutor(max_workers=6)(参考 fetch_data_batch)并行品种,指标可先只跑 3 个代表品种(399102 创业板/510880 红利/510300 沪深300)快速迭代,全量最后跑。
Step 5: Commit — git commit -m "feat: 现状基线Walk-Forward回测(7窗口×23品种)"
---
Files:
adata/aohan/quant/timing_overlay/backtest/report.pylearn/投资复盘/2026-08-11-择时基线回测.html
Step 1: 报告生成器(净值曲线/回撤曲线/分市场柱状图/指标表,复用实盘复盘报告的内嵌 SVG 风格)。
Step 2: 跑全量基线,产出基线报告。
Step 3: 分析并量化三个差距:
- 牛市踏空幅度:23 品种 10 年牛市期平均仓位 vs 大盘收益;
- 震荡磨损量级:状态切换次数/年 + 震荡期择时增值;
- 低回撤特质:最大回撤/平均回撤 vs 大盘(应与实盘复盘结论一致)。
Step 4: Commit — git commit -m "docs: 择时现状基线回测报告(10年Walk-Forward)"
> 里程碑 M0(硬门槛):基线报告产出,三个差距量化成数字。不达标不写 overlay 一行代码。
---
timing_overlay 包骨架 + 开关配置config.py 三层开关默认全关、参数表)
layer1_trend_state.py,含 TrendStateMachine.update(df, consensus, rs_pass) 与 compute_consensus)
layer3_drawdown_protect.py,含指数 ATR 触发 + 净值硬止损 + 分 3 日再建仓)
position_overlay.py 的 apply_overlay,公式见探讨文档第六节)
Files:
adata/aohan/quant/timing_overlay/position_overlay.py(加 apply_overlay_series)tests/adata_test/quant/test_overlay_series.pyStep 1: 写失败测试(序列版与逐点版等价:同一输入序列,逐日推进状态机,输出与手动逐点调用一致;特别测:状态一旦进入 attack,回调 5% 不出局(滞回),回撤 8% 触发减半等)
class TestOverlaySeries(unittest.TestCase):
def test_series_equals_pointwise(self):
# 同一 df 上,apply_overlay_series 的输出 == 手动循环 apply_overlay
...
def test_hysteresis_keeps_attack_on_5pct_pullback(self):
# 进攻态下回调5%(<缓冲带),状态不变
...
Step 2: 跑失败 → Step 3: 实现(apply_overlay_series:遍历 df 行,维护 L1/L3 状态对象,逐日调用 apply_overlay,返回新 position 序列)→ Step 4: 跑通
Step 5: Commit — git commit -m "feat: overlay逐日序列版(回测/实盘共用)"
Files:
adata/aohan/quant/timing_overlay/backtest/engine.py(run_overlay(code):基线 position 过 apply_overlay_series)Step 1: 打开 L1+L3 开关,7 窗口 × 23 品种重跑。 Step 2: 对比基线报告,检查硬指标:
| 指标 | 硬门槛 |
| --- | --- |
| 状态切换次数/年 | ↓ 60%+(震荡市不被磨的实证) |
| 最大回撤(vs 基线) | ≤ 基线(不能为加仓牺牲低回撤) |
| 牛市参与度(牛市期平均仓位) | 40% → 60~80% |
| 择时增值(每窗口) | 不得比基线差(至少持平) |
| trailing 触发后 30 日恢复率 | > 70% |
| 分市场拆解 | 震荡市择时增值 ≥ 基线(磨耗下降) |
Step 3: 生成 learn/投资复盘/2026-08-11-择时L1L3回测对比.html,与基线报告并排对比。
Step 4: Commit — git commit -m "docs: L1+L3回测对比报告"
> 里程碑 M1(硬门槛):切换次数 -60% 且最大回撤不恶化。不达标→回到 Task 1.2/1.3 调参,禁止进入阶段二。
Files:
adata/aohan/quant/chart/MarketAnalysis.py(calculate_average_holding_ratio 后、存 last_timing_positions 前插入 apply_position_overlay_for_code)run_push.sh(加 AH_OVERLAY=1 环境变量透传)
Step 1: 写失败测试(回归:开关全关时 load_mult_market_strategy 行为与改动前逐字节一致)
Step 2: 实现(受 config.set_overlay_config 环境变量控制)
Step 3: 跑通回归测试
Step 4: 手动 AH_OVERLAY=1 bash run_push.sh,对照"消息推送.txt"今日仓位差异。
Step 5: Commit — git commit -m "feat: overlay接入生产链路(默认关,回归安全)+A/B旁路"
---
Files:
adata/aohan/quant/timing_overlay/layer2_valuation_brake.pyadata/aohan/quant/data_process/valuation_fetcher.py(_postprocess_valuation_df 增加 5 年滚动分位选项,默认行为不变,新增参数 rolling_window)tests/adata_test/quant/test_layer2_valuation_brake.pyStep 1: 写失败测试
valuation_cap(code, pe_pct, pb_pct):宽基用 PE、金融周期用 PB、分歧取更紧、红利放宽 80%、GLD/QQQ 返回 None(禁用);
Step 2: 跑失败 → Step 3: 实现(数据走 ValuationFetcher.fetch(code) 的 pe_pct/pb_pct;注意缓存文件已有 399006.SZ_valuation_1d_valuation.csv、000300.SH_valuation_1d_valuation.csv 等)→ Step 4: 跑通
Step 5: Commit — git commit -m "feat: L2估值档位(品种适配+5年滚动分位)"
Files:
adata/aohan/quant/timing_overlay/position_overlay.pytests/adata_test/quant/test_position_overlay.py
Step 1: 失败测试(L2 返回 None → 不封顶;红利 → 80% 上限)
Step 2~4: 实现 + 跑通
Step 5: Commit — git commit -m "feat: L2接入overlay合成器"
Files:
adata/aohan/quant/timing_overlay/backtest/engine.py(run_overlay 打开 L2)learn/投资复盘/2026-08-11-择时三层回测对比.htmlStep 1: 三层全开重跑 7 窗口。 Step 2: 检查硬指标:
| 指标 | 硬门槛 |
| --- | --- |
| 牛市品种超额收益 | 收敛到 -5pp 内(vs 基线 -15~-20pp) |
| 最大回撤 | ≤ 基线 |
| 夏普 | 牛市品种翻倍,其他不下降 |
| 参数稳定性 | 7 窗口选出的参数不跳变(K_ATR/估值档位阈值) |
| 择时增值 | 每窗口为正(稳定性) |
Step 3: 生成三层对比报告,与基线/L1L3 报告三线并排。
Step 4: Commit — git commit -m "docs: 三层全开Walk-Forward复测报告"
> 里程碑 M2(硬门槛):牛市超额收敛到 -5pp 且回撤不恶化。达标后开启生产 L2 开关进行实盘 A/B。
---
timing_overlay/relative_strength.pyshow_marker.py L2263 主循环前一次算完 23 品种 60 日涨幅排名,传入 overlay。run_overlay 增加 rs 输入,7 窗口复测,验证 RS 过滤是否提升牛市超额收敛速度。
moneyflow_mkt_dc 接口数据质量(tushare 积分);stock_market_activity_legu 或自建 advance/decline),落 data_cache/;ValuationFetcher 同级(5 天)。
---
| 里程碑 | 内容 | 硬门槛 | 卡口 |
| --- | --- | --- | --- |
| M0 | 基线回测报告 | 三个差距量化成数字 | 未量化不写 overlay |
| M1 | L1+L3 回测 | 切换次数 -60% 且回撤 ≤ 基线 | 未达标调 L1/L3 参数 |
| M2 | 三层全开回测 | 牛市超额 -5pp 内 + 回撤 ≤ 基线 + 参数稳定 | 未达标调 L2 参数/口径 |
| M3 | 增强项复测 | 各增强项相对三层不恶化 + 有量化收益 | 未达标砍掉该项 |
回测是门槛,不是装饰:每阶段回测报告 + 指标对比表都要落到 learn/投资复盘/,成为可追溯的决策依据。
---
| 依赖/风险 | 说明 | 缓解 |
| --- | --- | --- |
| 回测耗时 | 23 品种×30+策略×10年×7窗口 | 先 3 代表品种快速迭代;ThreadPool 并行;指标可缓存 |
| 策略参数是否要在回测中重调 | 不重调!只调 overlay 参数 | overlay 与策略解耦,才能归因"改进来自 overlay" |
valuation_fetcher 分位是全历史 | 探讨文档要求 5 年滚动 | Task 2.1 加 rolling_window 参数,默认行为不变 |
| QQQ/GLD 历史估值 | 阶段三才需要 | 阶段一/二 L2 对它们禁用,不阻塞 |
| show_marker.py 3181 行 | 阶段三 RS 需改主循环 | 只加"算排名传参"几行,不重构 |
| 项目无 pytest | unittest 现状 | 新测试全 unittest,与 tests/ 一致 |
---
| 里程碑 | 产物 | 位置 |
| --- | --- | --- |
| M0 | 基线回测报告 | learn/投资复盘/2026-08-11-择时基线回测.html |
| M1 | L1+L3 对比报告 | learn/投资复盘/2026-08-11-择时L1L3回测对比.html |
| M2 | 三层对比报告 | learn/投资复盘/2026-08-11-择时三层回测对比.html |
| M3 | 增强项复测 | 追加到三层报告 |
| 生产 | 三层 overlay 接入(开关默认关) | timing_overlay/ + MarketAnalysis.py |
| 配套 | 探讨文档(决策依据) | learn/择时策略/2026-08-11-牛熊判断与牛市仓位优化探讨.md |