Files
mymt5opp/PROJECT_GUIDE.md
gavindiaz 63a829cc46 phase 7-8 完成 + warmup 修复 + 产物结构化重组
主要内容:
- 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)
2026-06-27 00:28:07 +08:00

39 KiB
Raw Permalink Blame History

PROJECT_GUIDE — Backtesting + Optuna + MT5 Stack 项目说明

这份文档是项目实例说明:它描述当前这个目录里实际跑通的那一套 (GoldScalperPro EA × XAUUSD × IC Markets Demo)——架构为什么这么搭、开发过程踩过 的坑、日常怎么用、后续怎么扩展。它和 README.md/0108-*.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 1Python 镜像引擎):把 EA 的填单/出场逻辑 bar-by-bar 重写一遍,跑在 Parquet 历史数据上。全 2 年 XAUUSD M5×M1 数据一次回测几秒钟。Optuna 拿它做 几百上千次试验,从中挑出 23 个 diverse finalist。
  • Tier 2MT5 Strategy Tester:只对 finalist 跑真实 tester。MT5 的数字才是 live 决策依据;Python 数字只负责排序和 A/B。
   Idea  ─►  Python mirror engine  ─►  Optuna search (thousands of trials, fast)
                                              │
                                              ▼
                                    23 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 → 指标 被另一策略 importcopy,不 import
Registry registry/ 已批准、锁定的结果——真相源 被随意编辑

1.3 三个核心设计决策

决策 1Engine 一无所知

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 为 Trueedge-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 barstrailing/BE EA 必传(见 §1.3 决策 3)
) -> Result

决策 2Engine 一旦验证就冻结

验证通过的 engine 就是满意基线,永不编辑它来试新想法——fork 它 复制一份加一个 default-OFF 的实验 hook,先证明 fork-with-change-off == 原版 1:1 再 A/B04-isolation-rules.md Rule 2)。

为什么这么严engine 的验证是 trade-by-trade 对账 MT5,很贵。一行"小改"可能 悄悄让百万个 bar 的填单位移,让所有已信任的数字都失效——而且你很久之后才发现

决策 3trailing/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 08 全闭环

Phase 内容 状态
0 设备 profileWindows + 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+ PROMOTEregistry 第一条记录) 完成
Optuna warmup bug 修复 完成(2026-06-26

2.2 Finalist #1 验证结果摘要

来源:registry/gold_scalper_pro_xauusd_2025-01-01_2026-06-26.md。 Optuna trial #324score=858.47。IS=2025 全年,OOS=2026 H1MT5 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.parquetgitignored
历史 M1 数据 data/XAUUSD_M1_2024-06-26_2026-06-26.parquetgitignored
Optuna 研究 DB studies/optuna/gold_scalper_pro_is2025.db500 trialsresumable
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
凭证 .envgitignored,含 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        # M1trailing/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          # 自包含 snapshotcopy 上次的,改)
└── wizard-answers.yaml  # 运行时 Q&A 写到这里

复制而不是 import——iteration 各自拥有 snapshot,几个月前的 iteration 仍能跑(04-isolation-rules.md Rule 5)。

Step 3 — Stats(先量后调)

先用真实历史测量信号特性:触发频率、原始胜率、平均有利/不利偏移。 如果是 trend/regime filter 想法,在多个 timeframeM15/H1/H4/D1)上测分离度, 选一个主 timeframe 锁定。这一步不做参数搜索

很多 iteration 死在这里——好结果,省下后面的算力。

Step 4 — Minimal scope(反过拟合闸门)

最小可工作版本core engine + 新 filterexit 最小化(trend 策略:trailing only 不要 BEgrid:第一层 only,不要 martingale)。

一个问题:有没有任何 edge handful 个 A/B 跑,不是搜索。没有 →

Step 5 — Expand + Optuna

只在 step 4 显示 edge 后展开。一次加一层,每层 A/B 对前一版: 加 BE → 加 grid 二层 → 加 ATR stop → 放宽 range。最后跑 Optuna。

# Smoke 先(30 trialsIS H1<2 分钟)
python scripts/optimize.py --smoke

# Full study500 trialsIS = 2025~1020 分钟,前台进度条)
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 自动切成 IS2025 全年)+ OOS2026 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 路径、contextperiod/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 参数重跑 Pythonwarmup 修复版)+ 落 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

  1. strategies/<new_strategy>/ 下建文件夹,至少包含:
    • __init__.py
    • instruments.py — 新 symbol 的 InstrumentConfig(从 MT5 spec 查,不要猜)
    • signals.py — 把 EA 的 EvaluateEntry/OpenTrade 翻译成 build_signals(params, bars, instrument) → SignalPack
    • <name>_engine.py — 实现 Engine Protocol。如果 EA 移动 SLBE/trailing/basket trailing),必须支持 m1_bars= kwarg
    • search_space.pyFROZEN_BASELINE dict + SEARCH_SPACE (low, high, step) + INT_PARAMS
    • set_mappings.py — Python param 名 ↔ EA input 名映射(生成 .set 时用)
  2. 复制 scripts/optimize.py 改 import 到新策略
  3. 先做 trade-by-trade 对账03-engine-design.md §8):选一个已知 preset,跑短窗口,Python vs MT5 逐笔对账直到通过 §8 target gate。未对账过的 engine 不许上 Optuna
  4. 08-workflow-cycle.md 8 步循环

4.2 加新 symbol(同策略)

04-isolation-rules.md Rule 4instrument 是数据,不是代码分支

  1. strategies/<strategy>/instruments.pyInstrumentConfig 对象,字段从 MT5 symbol spec 查
  2. 准备三个 cost-stress 变体:real / worst_case / best_case(用 get_profile() 派生)
  3. scripts/query_xauusd_spec.py 当模板改成新 symbol
  4. 下载新 symbol的历史 M5 + M1
  5. 零 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 是布尔 maskAND 进 signal

allow = directional_signal & ~block_condition

Gate 不能开/平仓,只能过滤。Common gatesregime 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 logicfork engine

绝不编辑 frozen engine。按 04-isolation-rules.md Rule 2

  1. cp shared/core/scalper_engine.py shared/core/scalper_engine_<idea>.py
  2. 加实验 hookdefault OFF
  3. Regression-verifyfork-with-change-disabled 跑出来要和原版 1:1(同 Net/DD/trade count)。不对说明 copy 不干净,先修
  4. A/Bfork-with-change-on vs frozen,同数据
  5. 显著且稳健 → 考虑 promote 成新 baselineRule 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-01grid-second-tier-atr-2026-07-15。 每个 iteration 各自拥有 optimize.py snapshot——copy 上次的改,不 import。


5. 踩坑列表(开发中实际遇到)

5.1 MT5 连接类

坑 1mt5.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

坑 2terminal 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 数据类

坑 4MT5 数据目录路径拼装

症状:要读 MT5 写的文件(如 .set、HTML 报告),不知道放哪。

修复:通过 mt5.terminal_info().data_path 取数据目录,再拼 MQL5\FilesMQL5\Profiles\Tester

坑 5:M1 数据即使信号 TF 更高也要拉并传给 engine

症状trailing/BE EA 在 bar-level 模拟下 net 比 MT5 高 4050%,看起来像"fidelity 噪声"。

根因这不是噪声是 bug(见 §1.3 决策 3)。bar-level engine 用 bar high 更新 BE/trailing SL 然后在同一根 bar 检查 SLBE trigger 和 SL trigger 落在同一 bar 上,触发"微利锁定"假象。

修复:下载 M1 + 传 m1_bars=engine.run,切换到 tick-level 模拟。gap 从 48.5% 降到 5.6%。

坑 6Python 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 类

坑 7Optuna objective 缺 indicator warmup

症状finalist #1 Python 首笔交易时间 = 2025-01-02T15:50,比 MT52025-01-02T01:55)晚 14 小时。

根因scripts/optimize.py:130 bars_is = slice_window(m5, IS_START, IS_END) 把 bars 切到 IS 窗口直接喂给 objectiveEMA/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

坑 8SL distance 用 fill_price 还是 close 算

症状trailing/BE EA 的 sizing 在 trade #1 之后偏离 MT5,复利后 equity 指数发散。

根因signals.pysl_prices[i] = close[i] ± sl_dist[i],其中 sl_dist[i] = InpAtrSLMult × ATR[i]。 所以 |close[i] - sl_prices[i]| 精确还原 sl_dist[i]——和 MT5 sizing 用的距离一致。 如果用 fill_pricesl_distance,会让距离随 open-gap 漂移,有时大有时小, lots 不一致,equity 指数发散。

修复scalper_engine.py:211sl_distance = abs(closes[i] - sl) 不用 fill_price。代码里有详细注释说明这个决策。

坑 9bar-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 类

坑 10MT5 测试报告是 UTF-16-LE 编码

症状:直接 open(report.html, encoding='utf-8').read() 解析乱码或抛 UnicodeDecodeError。

修复:用正确编码读,lxml/html5lib 解析。MT5 HTML 报告默认 UTF-16-LE07-mt5-bridge.md §7)。

坑 11.set 文件必须是 UTF-16-LE

症状:生成的 .set UTF-8 编码,MT5 tester 静默忽略。

修复:生成 .set 时写 UTF-16-LE07-mt5-bridge.md §2a)。

坑 12forward mode 自动切 IS/OOS

症状:想分别跑 IS 和 OOS 两份报告,但 MT5 一次只能跑一个窗口。

修复:用 forward mode——FromDate=2025.01.01, ToDate=2026.06.26, ForwardDate=2026.01.01。 MT5 自动切成 IS2025 全年)+ OOS(2026 H1)两段,导出一份报告但内部分两段指标。 当前 IS/OOS HTML 是分别导出的(用两次单段跑也行)。

坑 13lot/money mode 的 *_Lot 必须为 0 才启用 money mode

症状EA 的 "money mode" 用 LotAmount 算 lot,但如果 *_Lot 不为 0EA 会用 fixed lot 并忽略 LotAmount——run 看起来"work"但每笔 sizing 都错。

修复:用 money mode 时确认 *_Lot = 005-config-and-inputs.md §4)。

坑 14tick_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.16tv = 1.0

5.5 Workflow 类

坑 15:不要重跑正在跑的 Optuna

症状:以为超时重启一个 study,结果两个进程争同一个 study.dbCPU 全占、互相 thrash。

修复:重启前检查 study.db 是否还在写(optuna.load_study 看 trial count 增量)。 一个 study 多 workern_jobs=N)优于 N 个独立脚本04-isolation-rules.md Rule 8)。

坑 16smoke 先,full 后

症状:直接 500 trials,跑到一半发现 objective 有 bug,浪费算力。

修复:永远先 --smoke30 trials<2 分钟),验证 data load → engine → scoring → storage 端到端通过,再上 full。(06-optimization-and-robustness.md §6)

坑 17partial Optuna study 不算 finalist

症状:用被中断的 study 的"top-1"做 MT5 验证,结果不靠谱。

根因TPE 还没收敛,preliminary 排名不是真正的 top。

修复finalist 必须来自完成的搜索(06-optimization-and-robustness.md §5)。

坑 18top-N by score 是 clones

症状Optuna 收敛后 top 10 是同一个 peak 的近克隆,验证 3 个等于验证 1 个。

修复:用 diverse top-N 选择shared/optimizer/selector.py)—— greedy max-distance + rank_weight,挑出"各自好但参数区域不同"的 23 个 finalist。


6. 已知限制(carry forward

来自 registry 条目 §7

  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-spreadask = close + spread)。单 tick 差,不复合。
  3. ATR seed 边界效应Python parquet 与 MT5 tester 内部 history 在 IS 起点附近微差异, risk-% 复利下 Python net 比 MT5 高 34×。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
拓扑 Aall-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~1020 分钟。 建议:值得做。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.pyXAUUSD_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 大约 1020 分钟。如果做 5000 trials 或更大搜索空间, 在 hot path(_simulate_m1_exits 内层循环)加 @numba.njit 能提速 510×。 先正确后快——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         # M1trailing/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 不改 frozenpartial study 不算 finalist,未对账 engine 不上 Optuna)。
  • 工程上把"输入"和"代码"分开(4 个声明源),把"已知"和"未知"分开(registry append-only + known limitations)。

最有价值的一条经验测过才知道。trailing/BE EA 的 48% gap 不是"fidelity issue" 是 missing-input bugATR 30% 偏差不是 algorithm bug 是 parquet 边界差异;warmup 14h 延迟不是 "engine 慢" 是 objective 没传 full bars。每一条都通过测量定位、根因分析、针对性修复,而不是 靠"调参"或"重试"。

按这个流程走,研究速度会快很多,结果也更可信——因为每一个数字都能解释它为什么是那个值。


文档版本:2026-06-26。对应代码状态:Phase 8 完成,warmup bug 已修复,registry 第一条已落库。