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

748 lines
39 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](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/<name>/` | glue:数据 → 信号 → 止损 → engine → 指标 | 被另一策略 importcopy,不 import |
| Registry | `registry/` | 已批准、锁定的结果——真相源 | 被随意编辑 |
### 1.3 三个核心设计决策
#### 决策 1Engine 一无所知
`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 为 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/B[04-isolation-rules.md Rule 2](04-isolation-rules.md#L21-L44))。
**为什么这么严**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#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 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](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.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 trialsresumable |
| 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 # M1trailing/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/<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](04-isolation-rules.md#L74-L84))。
#### 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。
```powershell
# 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](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](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 路径、contextperiod/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 参数重跑 Pythonwarmup 修复版)+ 落 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/<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.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/<strategy>/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 是布尔 maskAND 进 signal
```python
allow = directional_signal & ~block_condition
```
Gate **不能开/平仓**,只能过滤。Common gatesregime 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 logicfork 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_<idea>.py`
2. 加实验 hook**default OFF**
3. **Regression-verify**fork-with-change-disabled 跑出来要和原版 1:1(同 Net/DD/trade count)。不对说明 copy 不干净,先修
4. A/Bfork-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/<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 连接类
#### 坑 1mt5.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)。
#### 坑 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 数据类
#### 坑 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 高 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](registry/gold_scalper_pro_xauusd_2025-01-01_2026-06-26.md)。
### 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](shared/optimizer/objective.py#L158-L183)。
#### 坑 8SL 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**。代码里有详细注释说明这个决策。
#### 坑 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-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))。
#### 坑 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 = 0`[05-config-and-inputs.md §4](05-config-and-inputs.md#L114-L142))。
#### 坑 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.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))。
#### 坑 16smoke 先,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))
#### 坑 17partial Optuna study 不算 finalist
**症状**:用被中断的 study 的"top-1"做 MT5 验证,结果不靠谱。
**根因**TPE 还没收敛,preliminary 排名不是真正的 top。
**修复**finalist 必须来自**完成**的搜索([06-optimization-and-robustness.md §5](06-optimization-and-robustness.md#L140-L154))。
#### 坑 18top-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,挑出"各自好但参数区域不同"的 23 个 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-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](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`~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](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 大约 1020 分钟。如果做 5000 trials 或更大搜索空间,
在 hot path`_simulate_m1_exits` 内层循环)加 `@numba.njit` 能提速 510×。
**先正确后快**——numba 是 stack 里的备选项,不是过早优化。
### 8.6 加新策略
按 §4.1 流程。下一个候选:把 GoldScalperPro 的 `STOP_ATR` 模式换成 `STOP_POINTS` 当作新策略——
或者更激进,把另一类 EAgrid 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 # 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 第一条已落库。*