Files
my_metatrader5_mcp/策略开发避坑指南.md
gavindiazandClaude Code 5ca012fe3c refactor: mtlib core + strategies structure for multi-strategy scaling
- mtlib/: shared data loader, backtest engine, metrics, cli wrapper,
  walkforward, GBK-safe console, smoke test (python -m mtlib smoke)
- strategies/xauusd_scalp/: single-source signal (generate_signals +
  generate_signal_at share _check), params dataclass, all runners
- Unify 3 known drifts: session filter (discrete set), spread 0.16,
  backtest signature defaults 1.2/0.6/1.0/0.2
- Fix walkforward verdict (both halves PF>=1) + drop fake stability ratio
- Root shims keep old commands working; data/ holds JSON files
- Baseline check: new vs old engine identical trades, PnL, final equity

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-14 21:10:49 +08:00

12 KiB
Raw Permalink Blame History

MetaTrader 5 策略开发避坑指南

本指南由 XAUUSD M1 超短线策略开发全过程(2026-09)总结而来。 适用于:任何商品、任何周期的指标策略,在 mtdata-cli + 回测 + 实盘这套工具链下的开发。 每开发一个新策略,先走一遍文末的《上线检查清单》。


1. 工具链环境速览

项 值/事实
数据/交易接口 mtdata-cli(MetaTrader 5 MCP 的命令行封装),必须 --json 输出
本机 Windows 11,Git Bash 环境,控制台 GBK 编码
数据文件 xauusd_m1_20k.json 等(data_fetch_candles 导出的 JSON,含全部指标列)
回测/实盘 策略逻辑必须共用同一套信号代码,禁止各自写一份

2. mtdata-cli 契约与易踩坑点(第一条命)

以下每条都是实测踩过的坑,任何脚本编写前必看。写错直接导致无法下单或静默错误。

2.1 交易命令的参数名必须全写

  • 方向:--order-type BUY|SELL(必须大写)。 写 buy 或 --side 会报: Missing required argument(s): order_type/__side. Unrecognized argument(s): --sl, --tp.
  • 止损/止盈:--stop-loss、--take-profit(不是 --sl/--tp)。trade_modify 同样用 --stop-loss。
  • 平仓:trade_close --ticket <ticket>。
  • 命名参数拼接时注意:List 参数 ["trade_place", SYMBOL, "--order-type", ..., "--dry-run", "false"] 不要用字符串里带空格的方式,容易在 subprocess 里被拆错。

2.2 --dry-run 默认是 true(预览校验,不发单)

  • 这是安全默认,但也是新手最大的坑:以为下了单,实际上只是个预览。
  • 真实发单必须显式 --dry-run false,且会被环境校验阻断,常见 blocker:
    • raw_quote_timestamp_ahead_of_clock → 见第 4 节时钟同步
    • invalid_protection_levels → 账户保护级别/杠杆配置问题
  • 脚本里建议做成环境变量开关,默认预览:
    DRY_RUN = os.environ.get("XAUUSD_DRY_RUN", "1") == "1"   # 默认预览
    DRY_FLAG = "--dry-run false" if not DRY_RUN else "--dry-run true"
    

2.3 trade_get_open 的返回字段是 items,不是 positions

  • 读取持仓列表:raw.get("items") or []。
  • 持仓条目字段:ticket、side、entry_price、sl、tp、time、volume、magic、symbol。
  • side 取值不统一(buy/sell、BUY/SELL、long/short、1/-1 都出现过),必须归一化:
    s = str(raw_side).lower()
    if s in ("buy","long","1","+1"): return 1
    if s in ("sell","short","-1"):  return -1
    

2.4 data_fetch_candles --limit N 返回的是固定 N 根滚动窗口

  • 每次调用永远返回恰好 N 根,默认跳过未完成K线(forming_candle_status: "skipped")。
  • 绝不能用 len(candles) 判断"是否来新K线"——长度永远不变。
  • 必须用最后一行的时间戳判断:cur_bar_time != last_bar_time。
  • 需要包含成形中K线时传 include_incomplete=true(回测默认用收盘K线,别混用)。

2.5 幂等与标记

  • --idempotency-key <key>:防重复下单(用"时间戳+方向"做 key)。
  • --magic <int>:给该策略的持仓打标记,用于重启后"认领"自己的单子。
  • 下单成功与否以返回里的 success 为准;ticket 字段名不统一(ticket/order_id/deal/position),写个递归查找函数兜底。

2.6 Windows 控制台是 GBK 编码

  • 脚本里不要打印非 GBK 字符(如 ⚠ ⚠️),会直接 UnicodeEncodeError 崩掉整个循环。
  • 用 ASCII 替代:!! 代替 ⚠。打印完记得 sys.stdout.flush()(重定向到日志时默认缓冲)。

3. 回测与实盘必须"同一套信号逻辑"(第二条命)

3.1 信号代码重复拷贝是最大的隐患

本项目最初有 backtest / walkforward / optimize / live 四个文件,各自实现了一份 generate_signals。陷阱:

  • xauusd_optimize.py 的时段过滤是 (7<=h<=16) or (12<=h<=21),与另外三个文件的离散集合 {7,8,9,10,16,17,18,20} 不一致。
  • optimize 做出来的参数在 backtest 里完全可能是另一回事。

规范(已重构落地,2026-09):

  • 信号逻辑只存在 strategies/<策略>/strategy.py 一处:generate_signals(向量版)与 generate_signal_at(单K线版)共用私有 _check(i)。
  • 回测/样本外/优化/时段分析/实盘全部 import 同一函数;实盘传入 mtlib.cli.df_from_candles() 构造的滚动窗口 DataFrame,调 generate_signal_at(cdf, len(cdf)-1),不再存在第二份规则顺序。
  • 商品事实(点差/合约/时段)收在 mtlib/instruments.py,策略参数收在 strategies/<策略>/params.py。
  • 一致性由冒烟测试把关:python -m mtlib smoke 逐根K线断言 generate_signal_at == generate_signals,并做新旧引擎基准数值校验。

3.2 回测里的三个常见偏差(在本项目中各踩了一个)

  1. 移动止损的 lookahead 偏差:回测在 bar 收盘时更新 SL,但同根 bar 内实际先用哪段价格、SL 是否"提前生效"在回测里处理得较乐观。对剥头皮这种薄边际策略,回测结果系统性偏高。
  2. 成交假设偏差:回测假设信号在收盘价成交;实盘是下一根/即时价成交,还有滑点。
  3. 参数默认值漂移:backtest() 函数签名里 tp_atr_mult=2.0, sl_atr_mult=1.0, trailing_step_atr=0.5,而 main() 实际传的是 1.2/0.6/0.2。函数默认值像"官方参数",其实从未被使用——阅读代码的人会被骗。签名默认值必须 == main 实际传参,或用常量统一收口。

4. 时钟同步:真单下发前的第一道关卡(第三条命)

4.1 现象

本机系统时钟比 MT5 服务器 tick 时间慢了约 56 秒,导致 trade_place --dry-run false 校验被 raw_quote_timestamp_ahead_of_clock 阻断——第一条真实订单会直接卡死。

4.2 检查与修复

  1. 查看时差:w32tm /stripchart 时间服务器 或脚本打印 now() 与报价时间对比。
  2. 一键同步:w32tm /resync(Git Bash 下要用 cmd //c w32tm /resync,避免正斜杠被解析)。
  3. 若 0x80070005 拒绝访问 / 0x80070057 参数错误:用管理员 PowerShell 写注册表再重启服务:
    • Type=NTP、NtpServer=time.windows.com pool.ntp.org
    • MaxPosPhaseCorrection / MaxNegPhaseCorrection = 0xFFFFFFFF
    • 重启 w32time 服务后 w32tm /resync /force。
  4. 标准:修复后时差应在 ±1s 内,再测 trade_place 的 blockers: []。

4.3 预防

  • 实盘脚本启动时打印本机 UTC 时间与最后报价时间误差,>5s 告警。
  • 任何"预览通过但真实单下不去"的环境校验,先查时钟。

5. 交易成本建模:点差必须实测,不能拍脑袋(第四条命)

5.1 血的教训

  • 早期回测假设点差 0.06 USD;实测黄金真实点差约 0.16(16 points),是假设的 2.7 倍。
  • 对持单仅 2~3 根 M1 的剥头皮策略,0.16 的口径就能把训练段从"+3.7% 勉强正"打成 "−1.4% 亏损"(PF 0.96 < 1)。

5.2 规范动作

  1. 每个商品、每个时段都要实测点差(bid/ask 差),写进回测参数,不要沿用默认。
  2. 压力测试点差:至少跑 预期点差 / 最高点差(如 0.16 / 0.22)两档。策略若在点差×1.5 时转负,说明边际太薄,实盘风险高。
  3. 点差对进出场的方向影响也要建模:买入按 close + spread/2 入场,卖出按 close - spread/2,出场扣 full spread。

6. 样本外验证:机器结论不可信,要人工判读(已在 XAUUSD 上验证)

6.1 本项目结果(walk-forward @0.16)

指标 训练段(前半 10k) 测试段(后半 10k)
交易次数 50 52
胜率 34% 50%
盈亏比 PF 0.96 2.22
收益 −1.4% +42.0%
最大回撤 9.7% 10.0%
  • 脚本自动打印"策略具有一定泛化能力"(因为测试段 PF>1)。
  • 但人工判读:训练段 PF<1 说明策略边际本来就很薄,前半段行情不支持这个策略;全部负成本靠后半段的单边趋势行情撑住。这是典型的"行情依赖型"表现,不足以说"稳定盈利策略"。
  • 稳定性比率 test_ret / max(train_ret, 0.01) 这个指标本身会骗人——训练段收益≈0 时比值直接爆炸成几千,"看起来稳"其实是除零近似。

6.2 验证规范

  1. Walk-forward 至少前后 50/50 切分;有条件做 k-fold(多窗口)看逐窗口一致性。
  2. 判读顺序:两段都 PF>1 ± 各自收益 >0 才是基本通过;只看"总收益为正"不通过。
  3. 50 笔/段的样本量远远不够下结论——回测只是筛选器,最终以 demo 真实成交 2~4 周对照为准(实际滑点、实际点差、实际成交时机)。
  4. 优化器按 total_ret 排序 Top 组合同样是过拟合陷阱;应同时看 ret/DD、样本外表现,并对最优参数做邻域扰动测试(参数微调动摇不算真最优)。

7. 实盘脚本工程化要点(本项目 5 处致命 BUG 逐一对应)

以下每一项在 XAUUSD 实盘脚本早期都真实出现过并导致无法正常交易:

# 坑 正确做法
1 CLI 参数名写错(--sl/--tp/小写方向)→ 预览直接报错 见 §2.1
2 读持仓用 positions 字段 → 永远读到空 见 §2.3
3 忘记 --dry-run false → 永远预览,从不下单 见 §2.2
4 没有时间止损 → 单子可以无限挂 只用回测的 max_bars,实盘转成秒数:MAX_BARS*60,超时 trade_close
5 用 len(candles) 判断新K线 → 滚动窗口恒长导致"只用第一根K线"的信号复算/漏算 见 §2.4 时间戳去重

7.1 状态与自愈(推荐模式)

  • 状态落盘 JSON(ticket/方向/入场价/入场ATR/SL/TP/入场时间),重启可恢复。
  • 每轮循环对账:本地 ticket 不在 items 中时,用 --magic 认领自己的单子补回 ticket;认领不到才清状态。
  • 平仓失败不要清状态,返回 bool,下一轮重试(防"以为平了其实没平")。

7.2 止损口径一致

  • 移动止损用入场时的 ATR(不是当前 ATR),trailing_start/step 与回测保持一致,且每根新收盘K线只更新一次。

7.3 主循环骨架

while True:
  拉数据 → 时间戳判断新K线
  对账持仓 → 认领/清状态
  [有仓] 时间止损检查 > 移动止损(新K线一次)
  [无仓] 新K线时才评估信号 → 下单(带 power key + magic + dry-run)
  打印 + flush + sleep

8. 数据分析环节注意

  • 去掉 NaN 行再算指标(dropna()),否则 EMA/ATR 前几十行全是 NaN,指标算出来是错的。
  • 时段过滤用 datetime 的 .hour/.minute,注意时区取 UTC,别混入本地时间。
  • 统计 ADX>25(趋势市)占比、RSI 超买超卖占比,先了解市场形态再定策略类型。
  • 回测图表(价格+信号+权益曲线+盈亏分布)必出,用眼睛看比看数字更能发现问题。

9. 上线检查清单(每新开发一个策略都过一遍)

  • 回测、样本外、优化器、实盘四处的信号逻辑来自同一个函数/常量(时段、指标参数、阈值)
  • 函数签名默认参数 == 实际调用参数
  • 点差已用该商品实测值(非拍脑袋),并跑过 ×1.5 点差压力测试
  • --order-type 大写、--stop-loss/--take-profit 全名、--dry-run false 显式、--magic+--idempotency-key 带上
  • 持仓读取用 items,side 已归一化,ticket 字段有兜底查找
  • 新K线判断用时间戳,锁仓时间止损已实现
  • 状态可落盘/重启恢复/魔术号自愈
  • 时钟与 MT5 服务器偏差 <5s(真单前必查)
  • 打印内容无 GBK 非法字符,每循环 flush
  • walk-forward 两段都 PF≥1 且收益>0(否则人工复核),样本量≥某阈值
  • 先在 demo 真实跑 2~4 周,用实际成交记录对照回测预期