主要内容: - Phase 8 PROMOTE: finalist #1 (trial #324) registry 条目,自动生成 - Optuna objective warmup bug 修复 (shared/optimizer/objective.py) - studies/ 目录按用途重组为 optuna/ + finalists/ + features/ 三层 - reports/ 加入 Optuna 中文 dashboard (5 主图 + 18 slice + 15 contour) - 新增 PROJECT_GUIDE.md 项目说明文档 - 新增 build_registry_entry.py / build_optuna_dashboard.py / build_feature_datasets.py - .gitignore: 允许提交 studies/*.db (Optuna DB) 和 reports/*.html (MT5 + dashboard)
39 KiB
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):
| 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/<name>/ |
glue:数据 → 信号 → 止损 → engine → 指标 | 被另一策略 import(copy,不 import) |
| Registry | registry/ |
已批准、锁定的结果——真相源 | 被随意编辑 |
1.3 三个核心设计决策
决策 1:Engine 一无所知
engine.run() 的输入是预算好的:bars + 信号数组 + SL/TP 价格数组。
Engine 只决定价格是否触到止损,从不决定止损放在哪。
这条接缝把"策略"和"模拟器"分开:换策略 → 改 caller 和数组;engine 不动。 这是整个项目可冻结、可复用的根基(02-architecture.md §2)。
engine.run 的签名(shared/core/engine.py):
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)。
为什么这么严: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):
| 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 | KB 入口:抽象两层模型、文档导航 |
| — | CLAUDE.md | 给 AI 助手的分阶段搭装 playbook |
| 01 | 01-stack-and-install.md | 技术栈每个库 + 每个系统的安装命令 |
| 02 | 02-architecture.md | 单向依赖分层 + 数据流 |
| 03 | 03-engine-design.md | bar-by-bar engine 设计、intra-bar 4-sub-tick、保真度 |
| 04 | 04-isolation-rules.md | 8 条隔离铁律(冻结/fork/instrument/registry) |
| 05 | 05-config-and-inputs.md | 4 个声明输入源(instrument/space/wizard/frozen) |
| 06 | 06-optimization-and-robustness.md | Optuna objective、diverse top-N、反过拟合层 |
| 07 | 07-mt5-bridge.md | 编译 EA、生成 .set/.ini、跑 tester、解析报告 |
| 08 | 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。 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.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 |
| ML 特征数据集 | studies/features/trade_features_*.parquet + trial_features_*.parquet(含 .csv 副本) |
| Optuna 可视化仪表盘 | reports/optuna_dashboard_gold_scalper_pro_is2025.html |
| MT5 IS 报告 | reports/IS-ReportTester-52845377.html |
| MT5 OOS 报告 | reports/OOS-ReportTester-52845377.html |
| 注册表第一条 | 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 一次性准备(已完成可跳过)
# 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 的 8 步走:
Step 1 — Hypothesis
一句话写下来,绑到一个已有的 engine + preset。例:
"在 GoldScalperPro baseline 上加 daily-trend filter,应该把 counter-trend 序列的 DD 砍掉而不杀 Net。"
Step 2 — Scaffold
按 02-architecture.md §4 命名约定建文件夹:
strategies/gold_scalper_pro/iterations/<base>-<approach>-<YYYY-MM-DD>/
├── 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)。
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。
# 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 的 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)。
Step 8 — PROMOTE
复制 finalist 到 registry/(04-isolation-rules.md Rule 7)。 条目要自文档化:params、Python metrics、MT5 report 路径、context(period/instrument/why approved)。 Append-only:不编辑已批准条目,新发现是新条目。Discarded iteration 进 archive/,不删—— 负面结果也是数据。
3.3 常用脚本速查
| 脚本 | 用途 | 典型用法 |
|---|---|---|
| scripts/optimize.py | 跑 Optuna 搜索(smoke / full) | python scripts/optimize.py --smoke 或 --trials 500 |
| scripts/reeval_finalist_forward.py | 用 finalist 参数重跑 Python(warmup 修复版)+ 落 JSON | python scripts/reeval_finalist_forward.py |
| scripts/compare_finalist.py | 对比 Python vs MT5 | python scripts/compare_finalist.py |
| scripts/diag_atr_check.py | ATR 逐笔诊断 | 调 sizing 偏差时用 |
| scripts/diag_mt5_trades.py | 解析 MT5 HTML 逐笔 trade | 对账时用 |
| scripts/diag_size_after_warmup.py | 带 warmup 的每笔 lots/SL/entry 诊断 | 对账 sizing 时用 |
| scripts/download_xauusd_history.py | 下载 M5 | 数据更新 |
| scripts/download_xauusd_m1.py | 下载 M1 | 数据更新 |
| scripts/query_xauusd_spec.py | 查 symbol 规格 | 新 symbol 时用 |
3.4 快速 A/B(不开 Optuna)
# 在 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)
- 在
strategies/<new_strategy>/下建文件夹,至少包含:__init__.pyinstruments.py— 新 symbol 的InstrumentConfig(从 MT5 spec 查,不要猜)signals.py— 把 EA 的EvaluateEntry/OpenTrade翻译成build_signals(params, bars, instrument) → SignalPack<name>_engine.py— 实现EngineProtocol。如果 EA 移动 SL(BE/trailing/basket trailing),必须支持m1_bars=kwargsearch_space.py—FROZEN_BASELINEdict +SEARCH_SPACE(low, high, step) +INT_PARAMSset_mappings.py— Python param 名 ↔ EA input 名映射(生成 .set 时用)
- 复制 scripts/optimize.py 改 import 到新策略
- 先做 trade-by-trade 对账(03-engine-design.md §8):选一个已知 preset,跑短窗口,Python vs MT5 逐笔对账直到通过 §8 target gate。未对账过的 engine 不许上 Optuna
- 进 08-workflow-cycle.md 8 步循环
4.2 加新 symbol(同策略)
按 04-isolation-rules.md Rule 4:instrument 是数据,不是代码分支。
- 在
strategies/<strategy>/instruments.py加InstrumentConfig对象,字段从 MT5 symbol spec 查 - 准备三个 cost-stress 变体:
real/worst_case/best_case(用get_profile()派生) - 用 scripts/query_xauusd_spec.py 当模板改成新 symbol
- 下载新 symbol的历史 M5 + M1
- 零 engine 改动——同策略换 symbol 只是换 config
4.3 加新指标
在 shared/indicators/base.py 加纯 numpy 函数。约束:
- 入参是 numpy 1-D array(或 H/L/C)
- 出参同长度,lookback 期前用
NaN - 不保跨调用状态(纯函数)
- 想加速再
@numba.njit,否则先正确后快
4.4 加新 gate(入场过滤)
在 shared/gates/base.py 加。Gate 是布尔 mask,AND 进 signal:
allow = directional_signal & ~block_condition
Gate 不能开/平仓,只能过滤。Common gates:regime filter、时段、exhaustion。
4.5 加新 robustness 层
在 shared/robustness/layers.py 加只读分析函数。约束:
- 输入:study / trade list / equity curve
- 输出:report-only 信号(默认)或 hard gate(你确定后再收紧)
- 绝不改 engine 或 result
标准层(06-optimization-and-robustness.md §4): 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:
cp shared/core/scalper_engine.py shared/core/scalper_engine_<idea>.py- 加实验 hook,default OFF
- Regression-verify:fork-with-change-disabled 跑出来要和原版 1:1(同 Net/DD/trade count)。不对说明 copy 不干净,先修
- A/B:fork-with-change-on vs frozen,同数据
- 显著且稳健 → 考虑 promote 成新 baseline(Rule 3);否则删 fork
4.7 加 iteration(新研究想法)
按 08-workflow-cycle.md §4 命名:
strategies/gold_scalper_pro/iterations/<base>-<approach>-<YYYY-MM-DD>/
例: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():
if not mt5.initialize(): # 不传 path → 复用已运行的终端
...
if not mt5.login(login, password=password, server=server): # 单独 login
...
见 scripts/download_xauusd_history.py:36-53。
坑 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。
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。
坑 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)。
坑 11:.set 文件必须是 UTF-16-LE
症状:生成的 .set UTF-8 编码,MT5 tester 静默忽略。
修复:生成 .set 时写 UTF-16-LE(07-mt5-bridge.md §2a)。
坑 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)。
坑 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)。
坑 16:smoke 先,full 后
症状:直接 500 trials,跑到一半发现 objective 有 bug,浪费算力。
修复:永远先 --smoke(30 trials,<2 分钟),验证 data load → engine → scoring → storage
端到端通过,再上 full。(06-optimization-and-robustness.md §6)
坑 17:partial Optuna study 不算 finalist
症状:用被中断的 study 的"top-1"做 MT5 验证,结果不靠谱。
根因:TPE 还没收敛,preliminary 排名不是真正的 top。
修复:finalist 必须来自完成的搜索(06-optimization-and-robustness.md §5)。
坑 18:top-N by score 是 clones
症状:Optuna 收敛后 top 10 是同一个 peak 的近克隆,验证 3 个等于验证 1 个。
修复:用 diverse top-N 选择(shared/optimizer/selector.py)—— greedy max-distance + rank_weight,挑出"各自好但参数区域不同"的 2–3 个 finalist。
6. 已知限制(carry forward)
来自 registry 条目 §7:
- Optuna warmup bug(已修复 2026-06-26):原
optimize.py把 bars 切到 IS 窗口直接喂 objective,导致 EMA/RSI/ATR 在 IS 起点才开始预热,首笔信号晚 14h。修复后 warmup 模式 精确复现 MT5 首笔交易时间。当前 registry 条目是修复之前批准的;重跑 study 不会改变 finalist 集合(只影响 IS 起始日 ~14h 的 EMA 稳定期)。 - Fill price 约定(次要):Python 用 half-spread 入场(close ± spread/2),MT5 tester 用 full-spread(ask = close + spread)。单 tick 差,不复合。
- 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):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 只实现了 stability_region。
按 06-optimization-and-robustness.md §4 应补:
neighborhood sensitivity、walk-forward(已有 scripts/walk_forward.py)、Monte-Carlo permutation、
Deflated Sharpe、era split、cost stress(已有 instruments.py
的 XAUUSD_PROFILES 三 variant)。
8.4 加 .set 生成器 + 自动 MT5 verify
当前 MT5 是手动跑(用户在 Strategy Tester 里导出 HTML)。按 07-mt5-bridge.md §3b
可实现:shared/mt5_pipeline/set_gen.py 从 param dict 生成 UTF-16-LE .set + tester.ini,
launch terminal64.exe /config: 自动跑 + ShutdownTerminal=1 自动退出 + 自动解析报告。
shared/mt5_pipeline/ 已经有 set_gen.py/ini_gen.py/runner.py/compare.py
骨架,需要填充实现。
8.5 加 numba 加速
当前 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. 速查命令卡
# === 一次性准备 ===
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 第一条已落库。