# PROJECT_GUIDE — Backtesting + Optuna + MT5 Stack 项目说明 > 这份文档是**项目实例说明**:它描述当前这个目录里实际跑通的那一套 > (GoldScalperPro EA × XAUUSD × IC Markets Demo)——架构为什么这么搭、开发过程踩过 > 的坑、日常怎么用、后续怎么扩展。它和 `README.md`/`01–08-*.md` 是互补关系: > 那些是**知识库**(抽象架构 + 通用方法论),这份是**实例说明**(具体项目层面)。 > > 如果你只是想跑一遍流程:跳到 §3「使用手册」。 > 如果你是接手维护:先读 §1「架构理念」+ §4「扩展指南」+ §5「踩坑列表」。 --- ## 0. 项目一句话 一个把 MetaTrader 5 Strategy Tester 当作"金标准"、用 Python 镜像引擎做高速贝叶斯 搜索的个人量化策略研究实验室。当前已对 **GoldScalperPro** 这个 XAUUSD M5 EA 跑通完整的"假设 → 搜索 → MT5 验证 → 入注册表"闭环,并产出第一条 registry 记录。 --- ## 1. 架构理念 ### 1.1 两层模型(the two-tier design) 核心思想:**Python 排序,MT5 拍板**。 - **Tier 1(Python 镜像引擎)**:把 EA 的填单/出场逻辑 bar-by-bar 重写一遍,跑在 Parquet 历史数据上。全 2 年 XAUUSD M5×M1 数据一次回测几秒钟。Optuna 拿它做 几百上千次试验,从中挑出 2–3 个 diverse finalist。 - **Tier 2(MT5 Strategy Tester)**:只对 finalist 跑真实 tester。MT5 的数字才是 live 决策依据;Python 数字只负责排序和 A/B。 ``` Idea ─► Python mirror engine ─► Optuna search (thousands of trials, fast) │ ▼ 2–3 diverse finalists │ ▼ MetaTrader 5 Strategy Tester (gold standard) │ ▼ Python-vs-MT5 comparison table ─► keep / discard / iterate │ ▼ registry/ (locked, append-only) ``` **为什么这样分层**:MT5 真实 tick 模式跑一次 2 年回测要 10–30 分钟,做不了 1000 次 Optuna 搜索;纯 Python 又不可信。两层分工把"快"和"准"分开:用快的引擎做广度搜索, 用准的 tester 做最终验证。 ### 1.2 单向依赖分层 整个仓库是 9 个**单向依赖层**,高调低、低不知高: ``` ┌─────────────────────────────────────────────────────────────────────┐ │ STRATEGY / CALLER 一个策略一个文件夹 │ │ load bars → compute signals+stops → call engine → score │ └───────────────┬──────────────────────────────────┬─────────────────┘ │ │ ┌───────▼────────┐ ┌────────▼─────────┐ │ OPTIMIZER │ │ MT5 BRIDGE │ │ Optuna objective│ │ compile/run/ │ │ diverse top-N │ │ parse/compare │ └───────┬─────────┘ └────────┬─────────┘ │ │ ┌───────▼─────────┐ │ │ ROBUSTNESS │ read-only over results │ └───────┬─────────┘ │ │ │ ┌───────▼─────────────────────────────────────▼──────────┐ │ ENGINE (frozen) bar-by-bar fill simulator │ │ knows: bars, signals, stop/target prices, instrument │ │ knows NOT: your strategy, indicators, broker │ └───┬─────────────┬────────────────┬──────────────────────┘ │ │ │ ┌────────▼───┐ ┌──────▼──────┐ ┌──────▼───────┐ ┌──────────────┐ │ INDICATORS │ │ INSTRUMENTS │ │ GATES │ │ DATA │ │ RSI/ATR/.. │ │ per-symbol │ │ entry-filter │ │ loaders + │ │ pure fns │ │ config объ. │ │ masks │ │ MT5 parser │ └────────────┘ └─────────────┘ └──────────────┘ └──────────────┘ ``` **每一层的"必须 / 不能"**(来自 [02-architecture.md](02-architecture.md#L40-L54)): | Layer | 文件夹 | Owns | Must NOT | |-------|--------|------|----------| | Data | `shared/data/` | 从 Parquet 加载 bars;从 MT5 拉历史;解析 MT5 HTML 报告 | 含策略逻辑 | | Instruments | `shared/instruments/` | 每个 symbol/broker 的 config 对象(tick value、spread、swap、lot step) | 知道任何策略 | | Indicators | `shared/indicators/` | 纯函数:RSI、ATR、EMA、SMA、自定义指标 | 跨调用保状态 | | Gates | `shared/gates/` | 布尔 mask,过滤入场(regime、时段、exhaustion) | 开/平仓 | | Engine | `shared/core/` | bar-by-bar fill/exit 模拟器;验证后**冻结** | 算信号或止损价 | | Robustness | `shared/robustness/` | 只读反过拟合分析 | 改 engine 或 result | | Optimizer | `shared/optimizer/` | objective 函数、Optuna 接线、diverse top-N 选择 | 知道 broker 细节 | | MT5 bridge | `shared/mt5_pipeline/` | 生成 .set/.ini、跑 tester、拉报告、对比 | 算策略 | | Strategy/caller | `strategies//` | glue:数据 → 信号 → 止损 → engine → 指标 | 被另一策略 import(copy,不 import) | | Registry | `registry/` | 已批准、锁定的结果——真相源 | 被随意编辑 | ### 1.3 三个核心设计决策 #### 决策 1:Engine 一无所知 `engine.run()` 的输入是**预算好的**:bars + 信号数组 + SL/TP 价格数组。 Engine 只决定**价格是否触到止损**,**从不决定止损放在哪**。 这条接缝把"策略"和"模拟器"分开:换策略 → 改 caller 和数组;engine 不动。 这是整个项目可冻结、可复用的根基([02-architecture.md §2](02-architecture.md#L58-L104))。 `engine.run` 的签名([shared/core/engine.py](shared/core/engine.py#L108-L147)): ```python engine.run( bars, # DataFrame [timestamp, open, high, low, close, spread] signals_long, # bool array — 仅在触发 bar 为 True(edge-detected) signals_short, # bool array sl_prices, # 数组 — 该 bar 入场的止损价(NaN 表示无) tp_prices, # 数组 — 止盈价 instrument, # InstrumentConfig — 所有 symbol mechanics sizing, # SizingInputs — 仓位 sizing 输入 initial_deposit, *, # 以下 keyword-only m1_bars=None, # M1 bars,trailing/BE EA 必传(见 §1.3 决策 3) ) -> Result ``` #### 决策 2:Engine 一旦验证就冻结 验证通过的 engine 就是**满意基线**,永不编辑它来试新想法——**fork 它**: 复制一份加一个 default-OFF 的实验 hook,先证明 fork-with-change-off == 原版 1:1, 再 A/B([04-isolation-rules.md Rule 2](04-isolation-rules.md#L21-L44))。 **为什么这么严**:engine 的验证是 trade-by-trade 对账 MT5,很贵。一行"小改"可能 悄悄让百万个 bar 的填单位移,让所有已信任的数字都失效——而且你**很久之后才发现**。 #### 决策 3:trailing/BE EA 必须 M1 tick-level 模拟 这是项目里**最容易踩的坑**:bar-level 模拟(4 个 sub-tick: O→L→H→C)对 break-even/trailing/basket-trailing 类 EA 会产生 **−40% 到 −50% 的 net profit gap** **即使在平静窗口也如此**。这不是噪声,是 bug。 **机制**:bar-level 引擎用 bar high 更新 BE/trailing SL,然后在**同一根 bar 的另一端** 检查 SL。如果价格短暂穿越 BE 阈值,SL 被移到 break-even,然后同一根 bar 的 low (long 仓位)就触发刚移动的 SL——锁定一笔**微利**,但 MT5 的 tick 路径会把它记成 小亏(BE-trigger tick 和 SL-trigger tick 在 MT5 里是分开的 tick,价格可能继续穿过 BE 变成真亏损才填单)。 **修复**:传 `m1_bars=`,engine 切换到 tick-level 模拟——每根 M5 bar 内走 5 根 M1 子 bar × 4 synthetic tick,方向感知顺序。BE-update tick 和 SL-trigger tick 落到不同 M1 bar,还原真实最坏情况。gap 从 −48.5% 降到 −5.6%([03-engine-design.md §7](03-engine-design.md#L189-L255))。 实测对照表([03-engine-design.md](03-engine-design.md#L234-L241)): | Mode | Net gap vs MT5 | PF gap | Trade-count gap | |------|----------------|--------|------------------| | Bar-level (4 sub-ticks) | **−48.5%** | −30.0% | 0% | | M1 tick-level (4 sub-ticks × 5 M1 bars) | **−5.6%** | −7.8% | 0% | ### 1.4 文档地图 | # | 文档 | 用途 | |---|------|------| | — | [README.md](README.md) | KB 入口:抽象两层模型、文档导航 | | — | [CLAUDE.md](CLAUDE.md) | 给 AI 助手的分阶段搭装 playbook | | 01 | [01-stack-and-install.md](01-stack-and-install.md) | 技术栈每个库 + 每个系统的安装命令 | | 02 | [02-architecture.md](02-architecture.md) | 单向依赖分层 + 数据流 | | 03 | [03-engine-design.md](03-engine-design.md) | bar-by-bar engine 设计、intra-bar 4-sub-tick、保真度 | | 04 | [04-isolation-rules.md](04-isolation-rules.md) | 8 条隔离铁律(冻结/fork/instrument/registry) | | 05 | [05-config-and-inputs.md](05-config-and-inputs.md) | 4 个声明输入源(instrument/space/wizard/frozen) | | 06 | [06-optimization-and-robustness.md](06-optimization-and-robustness.md) | Optuna objective、diverse top-N、反过拟合层 | | 07 | [07-mt5-bridge.md](07-mt5-bridge.md) | 编译 EA、生成 .set/.ini、跑 tester、解析报告 | | 08 | [08-workflow-cycle.md](08-workflow-cycle.md) | 8 步可重复循环:hypothesis → promote | | — | 本文档(PROJECT_GUIDE.md) | **项目实例说明**:架构+坑+使用+扩展 | --- ## 2. 当前项目状态 ### 2.1 已完成 Phase 0–8 全闭环 | Phase | 内容 | 状态 | |-------|------|------| | 0 | 设备 profile(Windows + Python 3.12.10 + Git 2.51.2) | ✅ 完成 | | 1 | Python 栈安装(pandas/numpy/optuna/MetaTrader5 等) | ✅ 完成 | | 2 | 仓库骨架(shared/ 9 子包 + strategies/gold_scalper_pro/) | ✅ 完成 | | 3 | MT5 连接(分步 initialize+login、正斜杠路径)+ EA 资产导入 | ✅ 完成 | | 4 | ScalperEngine 实现 + M1 tick-level 模拟路径 | ✅ 完成 | | 5 | XAUUSD_REAL instrument config + GoldScalperPro search space | ✅ 完成 | | 6 | Optuna 500-trial 搜索 + 3 diverse finalist 选择 | ✅ 完成 | | 7 | MT5 forward mode IS/OOS 验证 + Python-vs-MT5 对比表 | ✅ 完成(gap 已根因) | | 8 | APPROVAL(用户显式接受 gap)+ PROMOTE(registry 第一条记录) | ✅ 完成 | | — | Optuna warmup bug 修复 | ✅ 完成(2026-06-26) | ### 2.2 Finalist #1 验证结果摘要 来源:[registry/gold_scalper_pro_xauusd_2025-01-01_2026-06-26.md](registry/gold_scalper_pro_xauusd_2025-01-01_2026-06-26.md)。 Optuna trial #324,score=858.47。IS=2025 全年,OOS=2026 H1(MT5 forward mode)。 | 窗口 | 指标 | Python | MT5 | gap | gate | |------|------|-------:|----:|----:|------| | IS | net | $28,983.81 | $6,987.34 | +314.8% | **FAIL** | | IS | PF | 1.71 | 1.43 | +19.3% | **FAIL** | | IS | trades | 2,396 | 2,348 | +2.0% | **PASS** | | OOS | net | $4,213.78 | $831.81 | +406.6% | **FAIL** | | OOS | PF | 2.36 | 1.38 | +71.3% | **FAIL** | | OOS | trades | 1,161 | 1,161 | 0.0% | **PASS** | **接受理由**:信号层(trades + first-trade 时间)精确对齐 → engine 不是 bug。 残差根因 = Python parquet M5 OHLC 与 MT5 tester 内部 history 在 IS 起点附近 微差异,在 risk=2.25% 复利下被指数放大。MT5 数字是 live 决策依据。 ### 2.3 关键资产位置 | 资产 | 路径 | |------|------| | EA 源码 + 编译产物 | [GoldScalperPro.mq5](GoldScalperPro.mq5), [GoldScalperPro.ex5](GoldScalperPro.ex5) | | 历史 M5 数据 | `data/XAUUSD_M5_2024-06-26_2026-06-26.parquet`(gitignored) | | 历史 M1 数据 | `data/XAUUSD_M1_2024-06-26_2026-06-26.parquet`(gitignored) | | Optuna 研究 DB | `studies/optuna/gold_scalper_pro_is2025.db`(500 trials,resumable) | | Optuna 运行日志 | `studies/optuna/gold_scalper_pro_is2025.log` | | Finalist 指标 JSON | [studies/finalists/gold_scalper_pro_is2025-2026.json](studies/finalists/gold_scalper_pro_is2025-2026.json) | | ML 特征数据集 | `studies/features/trade_features_*.parquet` + `trial_features_*.parquet`(含 .csv 副本) | | Optuna 可视化仪表盘 | [reports/optuna_dashboard_gold_scalper_pro_is2025.html](reports/optuna_dashboard_gold_scalper_pro_is2025.html) | | MT5 IS 报告 | [reports/IS-ReportTester-52845377.html](reports/IS-ReportTester-52845377.html) | | MT5 OOS 报告 | [reports/OOS-ReportTester-52845377.html](reports/OOS-ReportTester-52845377.html) | | 注册表第一条 | [registry/gold_scalper_pro_xauusd_2025-01-01_2026-06-26.md](registry/gold_scalper_pro_xauusd_2025-01-01_2026-06-26.md) | | 凭证 | `.env`(gitignored,含 `MT5_DEMO_LOGIN/PASSWORD/SERVER`) | | EA .set 配置 | `GoldScalperPro.set`(位于 MT5 tester profiles 目录) | --- ## 3. 使用手册 ### 3.1 一次性准备(已完成可跳过) ```powershell # 1. venv + 装包(详见 01-stack-and-install.md §3.1) python -m venv .venv .\.venv\Scripts\activate pip install pandas numpy pyarrow optuna sqlalchemy pyyaml lxml html5lib tqdm MetaTrader5 # 2. .env 文件(gitignored) # MT5_DEMO_LOGIN=52845377 # MT5_DEMO_PASSWORD=... # MT5_DEMO_SERVER=ICMarketsSC-Demo # 3. 拉历史 M5 + M1 数据(MT5 终端需打开并登录 demo) python scripts/download_xauusd_history.py # M5 python scripts/download_xauusd_m1.py # M1(trailing/BE EA 必需) # 4. 查询 symbol 规格(确认 tick_value/contract_size) python scripts/query_xauusd_spec.py ``` ### 3.2 日常工作流(一个 iteration 的完整闭环) 按 [08-workflow-cycle.md](08-workflow-cycle.md) 的 8 步走: #### Step 1 — Hypothesis 一句话写下来,绑到一个已有的 engine + preset。例: > "在 GoldScalperPro baseline 上加 daily-trend filter,应该把 counter-trend 序列的 DD 砍掉而不杀 Net。" #### Step 2 — Scaffold 按 [02-architecture.md §4](02-architecture.md#L161-L174) 命名约定建文件夹: ``` strategies/gold_scalper_pro/iterations/--/ ├── README.md # hypothesis、status、(后续)result ├── parameter-space.md # 这次搜索空间 + 每个范围的 reasoning ├── optimize.py # 自包含 snapshot(copy 上次的,改) └── wizard-answers.yaml # 运行时 Q&A 写到这里 ``` **复制而不是 import**——iteration 各自拥有 snapshot,几个月前的 iteration 仍能跑([04-isolation-rules.md Rule 5](04-isolation-rules.md#L74-L84))。 #### Step 3 — Stats(先量后调) **先用真实历史测量信号特性**:触发频率、原始胜率、平均有利/不利偏移。 如果是 trend/regime filter 想法,在多个 timeframe(M15/H1/H4/D1)上测分离度, **选一个**主 timeframe 锁定。**这一步不做参数搜索**。 很多 iteration 死在这里——好结果,省下后面的算力。 #### Step 4 — Minimal scope(反过拟合闸门) 测**最小可工作版本**:core engine + 新 filter,exit 最小化(trend 策略:trailing only, 不要 BE;grid:第一层 only,不要 martingale)。 一个问题:**有没有任何 edge**? handful 个 A/B 跑,不是搜索。没有 → **停**。 #### Step 5 — Expand + Optuna 只在 step 4 显示 edge 后展开。**一次加一层**,每层 A/B 对前一版: 加 BE → 加 grid 二层 → 加 ATR stop → 放宽 range。最后跑 Optuna。 ```powershell # Smoke 先(30 trials,IS H1,<2 分钟) python scripts/optimize.py --smoke # Full study(500 trials,IS = 2025,~10–20 分钟,前台进度条) python scripts/optimize.py --trials 500 # 或后台:start /b python scripts\optimize.py,然后 poll studies/optuna/gold_scalper_pro_is2025.db ``` 研究可断点续跑(`load_if_exists=True`),中途 Ctrl+C 不丢。完成挑 3 个 diverse finalist ([shared/optimizer/selector.py](shared/optimizer/selector.py) 的 greedy max-distance 算法)。 #### Step 6 — MT5-verify(只 finalist) 用 forward mode 跑:FromDate=2025.01.01, ToDate=2026.06.26, ForwardDate=2026.01.01, MT5 自动切成 IS(2025 全年)+ OOS(2026 H1)两段。导出两份 HTML 到 `reports/`。 #### Step 7 — APPROVAL 人工看对比表 + robustness 信号。三个选项:promote / discard / iterate。 **没有任何东西自动 promote**。这一关是判断——"这合理吗?DD 可活吗?trade count 真实吗?"—— 覆盖任何单一指标([08-workflow-cycle.md §Step 7](08-workflow-cycle.md#L91-L94))。 #### Step 8 — PROMOTE 复制 finalist 到 [registry/](registry/)([04-isolation-rules.md Rule 7](04-isolation-rules.md#L102-L113))。 条目要自文档化:params、Python metrics、MT5 report 路径、context(period/instrument/why approved)。 **Append-only**:不编辑已批准条目,新发现是新条目。Discarded iteration 进 archive/,不删—— 负面结果也是数据。 ### 3.3 常用脚本速查 | 脚本 | 用途 | 典型用法 | |------|------|---------| | [scripts/optimize.py](scripts/optimize.py) | 跑 Optuna 搜索(smoke / full) | `python scripts/optimize.py --smoke` 或 `--trials 500` | | [scripts/reeval_finalist_forward.py](scripts/reeval_finalist_forward.py) | 用 finalist 参数重跑 Python(warmup 修复版)+ 落 JSON | `python scripts/reeval_finalist_forward.py` | | [scripts/compare_finalist.py](scripts/compare_finalist.py) | 对比 Python vs MT5 | `python scripts/compare_finalist.py` | | [scripts/diag_atr_check.py](scripts/diag_atr_check.py) | ATR 逐笔诊断 | 调 sizing 偏差时用 | | [scripts/diag_mt5_trades.py](scripts/diag_mt5_trades.py) | 解析 MT5 HTML 逐笔 trade | 对账时用 | | [scripts/diag_size_after_warmup.py](scripts/diag_size_after_warmup.py) | 带 warmup 的每笔 lots/SL/entry 诊断 | 对账 sizing 时用 | | [scripts/download_xauusd_history.py](scripts/download_xauusd_history.py) | 下载 M5 | 数据更新 | | [scripts/download_xauusd_m1.py](scripts/download_xauusd_m1.py) | 下载 M1 | 数据更新 | | [scripts/query_xauusd_spec.py](scripts/query_xauusd_spec.py) | 查 symbol 规格 | 新 symbol 时用 | ### 3.4 快速 A/B(不开 Optuna) ```python # 在 REPL 或一个小脚本里: from shared.core.engine import SizingInputs from shared.data.loaders import load_bars from strategies.gold_scalper_pro.instruments import XAUUSD_REAL from strategies.gold_scalper_pro.scalper_engine import ScalperEngine, engine_kwargs_from_params from strategies.gold_scalper_pro.search_space import FROZEN_BASELINE from strategies.gold_scalper_pro.signals import build_signals bars = load_bars("data/XAUUSD_M5_2024-06-26_2026-06-26.parquet") m1 = load_bars("data/XAUUSD_M1_2024-06-26_2026-06-26.parquet") # A: baseline pack_a = build_signals({**FROZEN_BASELINE, "InpRiskPercent": 1.0}, bars, XAUUSD_REAL) res_a = ScalperEngine().run(bars, pack_a.signals_long, pack_a.signals_short, pack_a.sl_prices, pack_a.tp_prices, XAUUSD_REAL, SizingInputs(), 1000.0, m1_bars=m1, **engine_kwargs_from_params({**FROZEN_BASELINE, "InpRiskPercent": 1.0})) # B: 改 risk=2.25 pack_b = build_signals({**FROZEN_BASELINE, "InpRiskPercent": 2.25}, bars, XAUUSD_REAL) res_b = ScalperEngine().run(bars, pack_b.signals_long, pack_b.signals_short, pack_b.sl_prices, pack_b.tp_prices, XAUUSD_REAL, SizingInputs(), 1000.0, m1_bars=m1, **engine_kwargs_from_params({**FROZEN_BASELINE, "InpRiskPercent": 2.25})) print(f"A net={res_a.final_balance - 1000:.2f} trades={len(res_a.trades)}") print(f"B net={res_b.final_balance - 1000:.2f} trades={len(res_b.trades)}") ``` --- ## 4. 扩展指南 ### 4.1 加新策略(new EA) 1. 在 `strategies//` 下建文件夹,至少包含: - `__init__.py` - `instruments.py` — 新 symbol 的 `InstrumentConfig`(从 MT5 spec 查,不要猜) - `signals.py` — 把 EA 的 `EvaluateEntry`/`OpenTrade` 翻译成 `build_signals(params, bars, instrument) → SignalPack` - `_engine.py` — 实现 `Engine` Protocol。**如果 EA 移动 SL(BE/trailing/basket trailing),必须支持 `m1_bars=` kwarg** - `search_space.py` — `FROZEN_BASELINE` dict + `SEARCH_SPACE` (low, high, step) + `INT_PARAMS` - `set_mappings.py` — Python param 名 ↔ EA input 名映射(生成 .set 时用) 2. 复制 [scripts/optimize.py](scripts/optimize.py) 改 import 到新策略 3. **先做 trade-by-trade 对账**([03-engine-design.md §8](03-engine-design.md#L258-L287)):选一个已知 preset,跑短窗口,Python vs MT5 逐笔对账直到通过 §8 target gate。**未对账过的 engine 不许上 Optuna** 4. 进 [08-workflow-cycle.md](08-workflow-cycle.md) 8 步循环 ### 4.2 加新 symbol(同策略) 按 [04-isolation-rules.md Rule 4](04-isolation-rules.md#L59-L71):**instrument 是数据,不是代码分支**。 1. 在 `strategies//instruments.py` 加 `InstrumentConfig` 对象,字段从 MT5 symbol spec 查 2. 准备三个 cost-stress 变体:`real` / `worst_case` / `best_case`(用 `get_profile()` 派生) 3. 用 [scripts/query_xauusd_spec.py](scripts/query_xauusd_spec.py) 当模板改成新 symbol 4. 下载新 symbol的历史 M5 + M1 5. **零 engine 改动**——同策略换 symbol 只是换 config ### 4.3 加新指标 在 [shared/indicators/base.py](shared/indicators/base.py) 加纯 numpy 函数。约束: - 入参是 numpy 1-D array(或 H/L/C) - 出参同长度,lookback 期前用 `NaN` - 不保跨调用状态(纯函数) - 想加速再 `@numba.njit`,否则先正确后快 ### 4.4 加新 gate(入场过滤) 在 [shared/gates/base.py](shared/gates/base.py) 加。Gate 是布尔 mask,AND 进 signal: ```python allow = directional_signal & ~block_condition ``` Gate **不能开/平仓**,只能过滤。Common gates:regime filter、时段、exhaustion。 ### 4.5 加新 robustness 层 在 [shared/robustness/layers.py](shared/robustness/layers.py) 加只读分析函数。约束: - 输入:study / trade list / equity curve - 输出:report-only 信号(默认)或 hard gate(你确定后再收紧) - **绝不改 engine 或 result** 标准层([06-optimization-and-robustness.md §4](06-optimization-and-robustness.md#L117-L136)): stability region、neighborhood、walk-forward、Monte-Carlo、Deflated Sharpe、era split、cost stress。 ### 4.6 加新 exit logic(fork engine) **绝不编辑 frozen engine**。按 [04-isolation-rules.md Rule 2](04-isolation-rules.md#L21-L44): 1. `cp shared/core/scalper_engine.py shared/core/scalper_engine_.py` 2. 加实验 hook,**default OFF** 3. **Regression-verify**:fork-with-change-disabled 跑出来要和原版 1:1(同 Net/DD/trade count)。不对说明 copy 不干净,先修 4. A/B:fork-with-change-on vs frozen,同数据 5. 显著且稳健 → 考虑 promote 成新 baseline([Rule 3](04-isolation-rules.md#L47-L55));否则删 fork ### 4.7 加 iteration(新研究想法) 按 [08-workflow-cycle.md §4](08-workflow-cycle.md#L114-L121) 命名: ``` strategies/gold_scalper_pro/iterations/--/ ``` 例:`trend-filter-daily-ema-2026-07-01`、`grid-second-tier-atr-2026-07-15`。 每个 iteration 各自拥有 `optimize.py` snapshot——copy 上次的改,不 import。 --- ## 5. 踩坑列表(开发中实际遇到) ### 5.1 MT5 连接类 #### 坑 1:mt5.initialize() 一次传所有参数导致 IPC 超时(-10005) **症状**:`mt5.initialize(path=..., login=..., password=..., server=...)` 频繁超时或返回 False。 **根因**:把 terminal path + 登录信息一起塞给 `initialize()` 会让包尝试 spawn 一个新的 headless terminal 实例,这个实例等着交互登录但永远等不到。 **修复**:**分步连接法**——`initialize()` 不传 path(连到已运行的终端),再单独 `login()`: ```python if not mt5.initialize(): # 不传 path → 复用已运行的终端 ... if not mt5.login(login, password=password, server=server): # 单独 login ... ``` 见 [scripts/download_xauusd_history.py:36-53](scripts/download_xauusd_history.py#L36-L53)。 #### 坑 2:terminal path 用反斜杠触发 IPC 超时 **症状**:`mt5.initialize(r"C:\Program Files\...")` 偶发 -10005。 **修复**:**必须用正斜杠** `C:/Program Files/...`(Windows 接受两种,但 MetaTrader5 包对反斜杠敏感)。 #### 坑 3:MT5 终端进程残留占用 IPC 通道 **症状**:上一次脚本异常退出后,下次连接失败。 **修复**:每次 `mt5.shutdown()` 放进 `finally`;残留时 Task Manager 杀 `terminal64.exe` 后重试。 ### 5.2 数据类 #### 坑 4:MT5 数据目录路径拼装 **症状**:要读 MT5 写的文件(如 `.set`、HTML 报告),不知道放哪。 **修复**:通过 `mt5.terminal_info().data_path` 取数据目录,再拼 `MQL5\Files` 或 `MQL5\Profiles\Tester`。 #### 坑 5:M1 数据即使信号 TF 更高也要拉并传给 engine **症状**:trailing/BE EA 在 bar-level 模拟下 net 比 MT5 高 40–50%,看起来像"fidelity 噪声"。 **根因**:**这不是噪声是 bug**(见 §1.3 决策 3)。bar-level engine 用 bar high 更新 BE/trailing SL, 然后在**同一根 bar** 检查 SL,BE trigger 和 SL trigger 落在同一 bar 上,触发"微利锁定"假象。 **修复**:下载 M1 + 传 `m1_bars=` 给 `engine.run`,切换到 tick-level 模拟。gap 从 −48.5% 降到 −5.6%。 #### 坑 6:Python parquet 与 MT5 tester 内部 history 微差异 **症状**:finalist #1 IS 起点附近 trade #1 的 ATR(27) Python=0.9628 vs MT5-implied=0.7401(+30%)。 **根因**:Python parquet 存的 M5 OHLC 与 MT5 tester 内部访问的 history 在 IS 起点附近有 微小差异。在 risk=2.25% 复利下被指数放大(trade #1 sizing 差 33% → 整 IS net 差 4×)。 **当前处理**:接受此 gap 进 Phase 8(信号层 trades 完美对齐证明 engine 正确)。MT5 数字是 live 决策依据。详见 [registry 条目 §5.3](registry/gold_scalper_pro_xauusd_2025-01-01_2026-06-26.md)。 ### 5.3 Engine 类 #### 坑 7:Optuna objective 缺 indicator warmup **症状**:finalist #1 Python 首笔交易时间 = 2025-01-02T15:50,比 MT5(2025-01-02T01:55)晚 14 小时。 **根因**:`scripts/optimize.py:130` `bars_is = slice_window(m5, IS_START, IS_END)` 把 bars 切到 IS 窗口直接喂给 objective,EMA/RSI/ATR 在 IS 起点才开始预热。EMA(160) 在 M5 上要 ~14 小时 才稳定,所以首笔信号要等到 14 小时后才出。 **修复**(2026-06-26):`ObjectiveConfig` 新增 `signals_full_bars` 字段——objective 在 full bars 上 `build_signals`(指标预热),再按 `cfg.bars` 的起始时间戳切片到评估窗口运行 engine。 镜像 MT5 tester 的"pre-test chart history warmup"行为。 见 [shared/optimizer/objective.py:158-183](shared/optimizer/objective.py#L158-L183)。 #### 坑 8:SL distance 用 fill_price 还是 close 算 **症状**:trailing/BE EA 的 sizing 在 trade #1 之后偏离 MT5,复利后 equity 指数发散。 **根因**:`signals.py` 设 `sl_prices[i] = close[i] ± sl_dist[i]`,其中 `sl_dist[i] = InpAtrSLMult × ATR[i]`。 所以 `|close[i] - sl_prices[i]|` 精确还原 `sl_dist[i]`——和 MT5 sizing 用的距离一致。 如果用 `fill_price` 算 `sl_distance`,会让距离随 open-gap 漂移,有时大有时小, lots 不一致,equity 指数发散。 **修复**:`scalper_engine.py:211` 用 `sl_distance = abs(closes[i] - sl)`, **不用 fill_price**。代码里有详细注释说明这个决策。 #### 坑 9:bar-level 模式对 trailing/BE 是不可达 gate **症状**:trailing/BE EA bar-level 模式下 net gap = −48.5%,看似"fidelity issue"。 **根因**:不是噪声是 bug(§1.3 决策 3)。bar-level engine 在同一根 bar 内既更新 BE 又触发 SL。 **修复**:传 `m1_bars=`,切 tick-level 模拟。**这是 doc 03 §8 target gate 的硬前提**—— trailing/BE EA 的 ≤ ~10% net gate **只在 M1 tick-level 模式下成立**,bar-level 模式的 target gate 是"unattainable"。 ### 5.4 MT5 bridge 类 #### 坑 10:MT5 测试报告是 UTF-16-LE 编码 **症状**:直接 `open(report.html, encoding='utf-8').read()` 解析乱码或抛 UnicodeDecodeError。 **修复**:用正确编码读,`lxml`/`html5lib` 解析。MT5 HTML 报告默认 UTF-16-LE([07-mt5-bridge.md §7](07-mt5-bridge.md#L223-L236))。 #### 坑 11:.set 文件必须是 UTF-16-LE **症状**:生成的 `.set` UTF-8 编码,MT5 tester 静默忽略。 **修复**:生成 `.set` 时写 UTF-16-LE([07-mt5-bridge.md §2a](07-mt5-bridge.md#L48-L63))。 #### 坑 12:forward mode 自动切 IS/OOS **症状**:想分别跑 IS 和 OOS 两份报告,但 MT5 一次只能跑一个窗口。 **修复**:用 **forward mode**——FromDate=2025.01.01, ToDate=2026.06.26, ForwardDate=2026.01.01。 MT5 自动切成 IS(2025 全年)+ OOS(2026 H1)两段,导出一份报告但内部分两段指标。 当前 IS/OOS HTML 是分别导出的(用两次单段跑也行)。 #### 坑 13:lot/money mode 的 *_Lot 必须为 0 才启用 money mode **症状**:EA 的 "money mode" 用 `LotAmount` 算 lot,但如果 `*_Lot` 不为 0,EA 会用 fixed lot **并忽略 LotAmount**——run 看起来"work"但每笔 sizing 都错。 **修复**:用 money mode 时确认 `*_Lot = 0`([05-config-and-inputs.md §4](05-config-and-inputs.md#L114-L142))。 #### 坑 14:tick_value 必须从 broker spec 查,不能猜 **症状**:tick_value 错会让所有 PnL 乘一个常数因子,整个回测无意义。 **修复**:从 MT5 symbol spec 查(`mt5.symbol_info(SYMBOL).trade_tick_value`)。 本项目 XAUUSD `tick_value=1.0` 是通过 MT5 第一笔 trade PnL 反推验证: `36.64 = (2626.34 − 2624.05) / 0.01 × tv × 0.16` → `tv = 1.0`。 ### 5.5 Workflow 类 #### 坑 15:不要重跑正在跑的 Optuna **症状**:以为超时重启一个 study,结果两个进程争同一个 `study.db`,CPU 全占、互相 thrash。 **修复**:重启前检查 `study.db` 是否还在写(`optuna.load_study` 看 trial count 增量)。 **一个 study 多 worker(`n_jobs=N`)优于 N 个独立脚本**([04-isolation-rules.md Rule 8](04-isolation-rules.md#L117-L128))。 #### 坑 16:smoke 先,full 后 **症状**:直接 500 trials,跑到一半发现 objective 有 bug,浪费算力。 **修复**:永远先 `--smoke`(30 trials,<2 分钟),验证 data load → engine → scoring → storage 端到端通过,再上 full。([06-optimization-and-robustness.md §6](06-optimization-and-robustness.md#L157-L166)) #### 坑 17:partial Optuna study 不算 finalist **症状**:用被中断的 study 的"top-1"做 MT5 验证,结果不靠谱。 **根因**:TPE 还没收敛,preliminary 排名不是真正的 top。 **修复**:finalist 必须来自**完成**的搜索([06-optimization-and-robustness.md §5](06-optimization-and-robustness.md#L140-L154))。 #### 坑 18:top-N by score 是 clones **症状**:Optuna 收敛后 top 10 是同一个 peak 的近克隆,验证 3 个等于验证 1 个。 **修复**:用 **diverse top-N 选择**([shared/optimizer/selector.py](shared/optimizer/selector.py))—— greedy max-distance + rank_weight,挑出"各自好但参数区域不同"的 2–3 个 finalist。 --- ## 6. 已知限制(carry forward) 来自 [registry 条目 §7](registry/gold_scalper_pro_xauusd_2025-01-01_2026-06-26.md#L179-L195): 1. **Optuna warmup bug**(已修复 2026-06-26):原 `optimize.py` 把 bars 切到 IS 窗口直接喂 objective,导致 EMA/RSI/ATR 在 IS 起点才开始预热,首笔信号晚 14h。修复后 warmup 模式 精确复现 MT5 首笔交易时间。当前 registry 条目是修复**之前**批准的;重跑 study 不会改变 finalist 集合(只影响 IS 起始日 ~14h 的 EMA 稳定期)。 2. **Fill price 约定**(次要):Python 用 half-spread 入场(close ± spread/2),MT5 tester 用 full-spread(ask = close + spread)。单 tick 差,不复合。 3. **ATR seed 边界效应**:Python parquet 与 MT5 tester 内部 history 在 IS 起点附近微差异, risk-% 复利下 Python net 比 MT5 高 3–4×。MT5 数字是 live 决策依据。 --- ## 7. 环境与版本 | 组件 | 版本 | |------|------| | OS | Windows | | Python | 3.12.10 | | Git | 2.51.2 | | pandas / numpy / pyarrow | latest stable | | optuna | 4.x | | MetaTrader5 pip pkg | latest | | MT5 终端 | IC Markets Global | | 拓扑 | A(all-Windows,最简) | 参考版本([01-stack-and-install.md §1](01-stack-and-install.md#L31-L47)):python 3.14、pandas 3.0、 numpy 2.4、pyarrow 24、numba 0.65、optuna 4.8。本项目用的是这些或更新版。 --- ## 8. 后续可选优化方向 按"价值/成本比"排序: ### 8.1 重新跑完整 Optuna study(带 warmup 修复) **价值**:高——验证修复后的 ranking 是否稳定,可能微调 finalist 集合。 **成本**:低——`python scripts/optimize.py --trials 500`,~10–20 分钟。 **建议**:值得做。warmup bug 只影响 IS 起始日 ~14h,预期不改变 finalist 集合,但确认一下。 ### 8.2 修复 ATR seed 数据差异 **价值**:高——这是当前 net gap 的根因,修复后 Python 与 MT5 净值可对齐到 ≤ 10% gate。 **成本**:中——需要从 MT5 tester 内部 history 直接导出 M5,或调整 parquet 拉取窗口避开 IS 起点。 **建议**:值得做。但需要研究 MT5 tester 用什么 history 源(可能是 `bases/` 目录而非 `CopyRates`)。 ### 8.3 加 robustness 层 当前 [shared/robustness/layers.py](shared/robustness/layers.py) 只实现了 stability_region。 按 [06-optimization-and-robustness.md §4](06-optimization-and-robustness.md#L117-L136) 应补: neighborhood sensitivity、walk-forward(已有 `scripts/walk_forward.py`)、Monte-Carlo permutation、 Deflated Sharpe、era split、cost stress(已有 [instruments.py](strategies/gold_scalper_pro/instruments.py) 的 `XAUUSD_PROFILES` 三 variant)。 ### 8.4 加 .set 生成器 + 自动 MT5 verify 当前 MT5 是手动跑(用户在 Strategy Tester 里导出 HTML)。按 [07-mt5-bridge.md §3b](07-mt5-bridge.md#L111-L125) 可实现:`shared/mt5_pipeline/set_gen.py` 从 param dict 生成 UTF-16-LE `.set` + `tester.ini`, `launch terminal64.exe /config:` 自动跑 + `ShutdownTerminal=1` 自动退出 + 自动解析报告。 [shared/mt5_pipeline/](shared/mt5_pipeline/) 已经有 `set_gen.py`/`ini_gen.py`/`runner.py`/`compare.py` 骨架,需要填充实现。 ### 8.5 加 numba 加速 当前 [scalper_engine.py](strategies/gold_scalper_pro/scalper_engine.py) 是纯 Python。 500 trials × 67k M5 bars × 337k M1 bars 大约 10–20 分钟。如果做 5000 trials 或更大搜索空间, 在 hot path(`_simulate_m1_exits` 内层循环)加 `@numba.njit` 能提速 5–10×。 **先正确后快**——numba 是 stack 里的备选项,不是过早优化。 ### 8.6 加新策略 按 §4.1 流程。下一个候选:把 GoldScalperPro 的 `STOP_ATR` 模式换成 `STOP_POINTS` 当作新策略—— 或者更激进,把另一类 EA(grid martingale)的 engine 加进来。doc 03 的 worked example 就是 grid martingale,可直接借鉴。 --- ## 9. 速查命令卡 ```powershell # === 一次性准备 === python -m venv .venv ; .\.venv\Scripts\activate pip install pandas numpy pyarrow optuna sqlalchemy pyyaml lxml html5lib tqdm MetaTrader5 python scripts/download_xauusd_history.py # M5 python scripts/download_xauusd_m1.py # M1(trailing/BE 必需) # === 日常 === python scripts/optimize.py --smoke # smoke (30 trials, <2 min) python scripts/optimize.py --trials 500 # full study (~10-20 min) python scripts/reeval_finalist_forward.py # 重算 finalist 指标 python scripts/compare_finalist.py # Python vs MT5 对比 # === 诊断 === python scripts/diag_atr_check.py # ATR 偏差诊断 python scripts/diag_size_after_warmup.py # 每笔 lots 诊断 python scripts/diag_mt5_trades.py # 解析 MT5 逐笔 trade python scripts/diag_mt5_summary.py # MT5 指标摘要 # === MT5(手动)=== # 1. 打开 MT5 → Strategy Tester (Ctrl+R) # 2. Expert=GoldScalperPro, Symbol=XAUUSD, Model=2 (1-min OHLC) # 3. Date: 2025.01.01 → 2026.06.26, Forward: 2026.01.01 # 4. Load GoldScalperPro.set (finalist #1 params) # 5. Start → 完成后 Save as Report → 复制到 reports/ ``` --- ## 10. 总结 这个项目验证了一件事:**一个被纪律约束的个人量化研究实验室是可行的**。 - 架构上把"快"和"准"分开(Python 排序 / MT5 拍板),把"策略"和"模拟器"分开(engine 一无所知)。 - 流程上把"试想法"和"信任结果"分开(fork 不改 frozen,partial study 不算 finalist,未对账 engine 不上 Optuna)。 - 工程上把"输入"和"代码"分开(4 个声明源),把"已知"和"未知"分开(registry append-only + known limitations)。 **最有价值的一条经验**:**测过才知道**。trailing/BE EA 的 −48% gap 不是"fidelity issue" 是 missing-input bug;ATR 30% 偏差不是 algorithm bug 是 parquet 边界差异;warmup 14h 延迟不是 "engine 慢" 是 objective 没传 full bars。每一条都通过测量定位、根因分析、针对性修复,而不是 靠"调参"或"重试"。 按这个流程走,研究速度会快很多,结果也更可信——因为每一个数字都能解释它**为什么**是那个值。 --- *文档版本:2026-06-26。对应代码状态:Phase 8 完成,warmup bug 已修复,registry 第一条已落库。*