MetaTrader 5 中进行测试的原理(基础篇)
MT5回测引擎到底在跑什么
MT5 的策略测试器不是简单把历史K线重放一遍。它按 tick 级别重建市场:先读历史报价,再结合点差、库存费、成交模式(即时或交易所)去模拟每一笔下单与平仓。2013 年官方文档给出的测试器基础架构,至今仍是多数 EA 回测可信度的底层约束。 一个常被忽略的事实是:默认「每笔 tick」模式下,测试器会用相邻分钟棒内的插值生成 tick,并非真实成交 tick。这意味着剥头皮类策略在回测里的滑点表现,可能比实盘更乐观。 想验证这点,开 MT5 随便挂一个 EA 跑 2023 年 XAUUSD 的 H1,对比「每笔 tick」和「真实 tick(需下载历史)」两种模式下的净利润曲线,多数账户会看到真实 tick 下回撤更大。外汇与贵金属杠杆高,回测偏差放大后实盘亏损概率显著上升,动手前先认清这点。
「先证两件事,再让 EA 替你盯盘」
让 EA 接管盘面,图的是它不困不慌、24×7 按规则下单,但上线前得先确认两件事:一是 EA 真的只按你定的交易系统规则出手,二是这套规则在历史行情里确实有过正期望。 MetaTrader 5 自带的 Strategy Tester 就是干这个的——把 EA 丢进去跑历史数据,看它是否严守规则、净值曲线是否向上。外汇和贵金属杠杆高、滑点跳空频繁,历史盈利不代表未来,只能当作可证伪的参考。 开 MT5 按 Ctrl+R 调出测试器,选一个现成 EA 跑 2020—2023 年 EURUSD 的 M1 数据,重点看成交笔数和策略日志是否对得上你的规则描述。
◍ MT5回测里三种价格变动生成模式的差异
MT5 策略测试器提供三种价格变动生成模式:Every tick、1 Minute OHLC、Open prices only。Every tick 最细,每个控制点都触发 EA 的 OnTick(),但耗时最长;后两种是它的简化版,速度换精度。 历史报价以封装的分钟柱从服务器下发,每根柱只明确给四个控制点:开、高、低、收。比如 2011.1.10 00:00 那根 EURUSD 柱,开盘价 1.28940、收盘 1.28958、高 1.28958、低 1.28940,但高低压根不知道出现在第几秒。Every tick 会在四价之间插值补出额外变动点,模拟真实 tick 流。
- Minute OHLC 只认四价,不补中间点。这会带来确定性欺骗——像代码库里的 Grr-al EA,靠「开盘后第一跳低于开盘价就买、下一跳卖」的死逻辑,在 OHLC 模式画出漂亮向上资金曲线;切到 Every tick 一跑,实盘真相暴露:平衡走平但趋势向下。所以粗模好看必须回 Every tick 复核。
Open prices only 更狠:仅在柱开盘价触发一次 OnTick()。EURUSD H1 上 2010.9.1–12.31 测标准版 Moving Average,三种模式资金曲线完全重合,因为它只在柱开做决策、不挂单不拖止损。但该模式禁随机延迟,且不能访问低于测试周期的数据——H1 测就摸不到 M30,M20 测摸不到 M30 但能碰 H1。外汇贵金属波动剧烈,回测模式选错可能把实盘风险完全掩盖。 想自己看差异,可用 WriteTicksFromTester.mq5 把三种模式的 tick 存盘,再用 TicksFromTester.mq5 指标叠图。EA 与指标靠 FILE_COMMON 标志共用沙箱文件,免手动搬文件。
class=class="str">"cmt">//--- 打开文件 file=FileOpen(filename,FILE_WRITE|FILE_CSV|FILE_COMMON,";"); class=class="str">"cmt">//--- 检查文件句柄 if(file==INVALID_HANDLE) { PrintFormat("Error in opening of file %s for writing. Error code=%d",filename,GetLastError()); class="kw">return; } else { PrintFormat("The file will be created in %s folder",TerminalInfoString(TERMINAL_COMMONDATA_PATH)); }
公共目录里读 CSV 的句柄检查
在 MT5 里用 FileOpen 读共享 CSV,必须带 FILE_COMMON 标志,文件才会从 TerminalInfoString(TERMINAL_COMMONDATA_PATH) 返回的公共数据目录加载,而不是当前品种私有的 files 夹。 下面这段代码演示了只读 + CSV + 公共目录的三标志组合,分隔符用分号。句柄若等于 INVALID_HANDLE,说明文件没找到或权限不足,这时候直接用 GetLastError 把错误码打出来,比瞎猜快得多。 很多人忽略 else 分支里的路径回显,其实它在排查“文件明明在却读不到”时很关键:能确认 EA 实际去哪个公共路径找的文件。外汇与贵金属品种切换时终端公共路径一致,但文件写入端若来自别的程序,权限冲突概率不低,需实测确认。
class="type">int file=FileOpen(fname,FILE_READ|FILE_CSV|FILE_COMMON,";"); class=class="str">"cmt">//--- 检查文件句柄 if(file==INVALID_HANDLE) { PrintFormat("Error in open of file %s for reading. Error code=%d",fname,GetLastError()); class="kw">return; } else { PrintFormat("File will be opened from %s",TerminalInfoString(TERMINAL_COMMONDATA_PATH)); }
「回测里的点差从哪来」
MT5 策略测试器不对点差做数学建模,而是直接吃历史数据里的点差值。如果某根 K 线的历史点差记录 ≤0,测试代理会回退到当时请求品种的实际点差,这一点在跑老数据时要心里有数。 测试环境里点差永远被当成浮动类型处理,SymbolInfoInteger(symbol, SYMBOL_SPREAD_FLOAT) 固定返回 true,跟你账户里是不是固定点差品种无关,写 EA 时别拿这个返回值去判断真实环境。 历史数据除了 OHLC,还带每次价格跳动量和成交量,底层用 MqlRates 结构存取。下面这段是结构定义,逐行看字段就知道回测能拿到哪些东西: time 是柱形开始时间;open/high/low/close 分别是开高低收;tick_volume 记录每个价格变动的交易量;spread 就是该根 K 线的点差;real_volume 是市场真实成交量(多数外汇品种为 0)。 外汇和贵金属杠杆高、点差跳变频繁,回测若用到的 spread 字段为空或异常,实盘滑点可能比测试残酷得多,上机前先打印几根历史 K 线的 spread 确认数据质量。
class="kw">struct class="type">MqlRates { class="type">class="kw">datetime time; class=class="str">"cmt">// 柱形开始时间 class="type">class="kw">double open; class=class="str">"cmt">// 开盘价 class="type">class="kw">double high; class=class="str">"cmt">// 最高价 class="type">class="kw">double low; class=class="str">"cmt">// 最低价 class="type">class="kw">double close; class=class="str">"cmt">// 收盘价 class="type">long tick_volume; class=class="str">"cmt">// 每个价格变动的交易量 class="type">int spread; class=class="str">"cmt">// 点差 class="type">long real_volume; class=class="str">"cmt">// 市场交易量 };
◍ 回测里的全局变量是另一套
MT5 策略测试器会模拟客户端全局变量,但按 F3 调出的那张全局变量表,跟回测跑出来的不是同一份。 测试代理在独立空间里维护这些全局变量,不会写入你实盘客户端里能看到的当前全局变量区。 所以凡是依赖 GlobalVariableGet / GlobalVariableSet 做跨 EA 通信或状态记忆的逻辑,回测结果和实盘表现可能脱节——验证时得单独在实盘客户端按 F3 核对一遍。