Files

748 lines
39 KiB
Markdown
Raw Permalink Normal View 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](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 第一条已落库。*