MetaTrader 5 中进行测试的原理·进阶篇
📘

MetaTrader 5 中进行测试的原理·进阶篇

第 2/3 篇

「回测里指标到底什么时候重算」

实时行情下,每一笔报价进来都会触发指标重算;但 MT5 策略测试器为了省算力,只在 EA 即将跑事件处理函数前,才统一把指标刷一遍。也就是说,不管你在 OnTick、OnTrade 还是 OnTimer 里有没有显式调用指标,只要是用 iCustom 或 IndicatorCreate 建了句柄的,都会在对应处理函数入口前被重算。 在 Every tick 模式回测时,指标重算严格发生在 OnTick 调用之前;如果 EA 里用 EventSetTimer 开了定时器,那每次 OnTimer 触发前也会先重算全部指标。这个机制对外汇、贵金属回测很关键,这类品种点差跳空频繁,重算时机偏差可能让历史拟合结果失真,属高风险验证环节。 一个容易被忽视的代价:若指标代码写得不经济(比如循环里反复申请缓冲区),测试器仍会在每个事件前全量重算,回测耗时可能成倍拉长。开 MT5 随便挂个笨指标跑 2015—2020 年 XAUUSD 的 every tick,对比优化后的版本,耗时差出两三倍是常有的事。

◍ 回测前历史数据是怎么进代理的

MT5 回测启动时,客户端会先把被测交易品种的历史从服务器同步并载入本地;首次是全量拉取,之后只补新数据。测试代理在回测刚开始时从客户端拿到这段历史,若策略用到别的品种(比如多货币 EA),代理在第一次调用该数据时才向客户端要,客户端有就直传,没有再向服务器下载后转交。 一个容易被忽略的点:就算策略没直接交易某品种,只要涉及交叉汇率换算,代理也会提前要数据。例如在入金为 USD 的 EURCHF 上测策略,处理首笔订单前代理会找客户端要 EURUSD 和 USDCHF 的历史,哪怕代码里从没显式调用这两个符号。 多货币策略建议手动把所需历史先下到客户端——打开对应图表并滚动到历史起点即可。这样能避开回测/优化时因临时下载数据产生的等待。《MQL5 参考》的“组织数据存取”里有强制下载历史的示例可抄。 历史记录客户端只从服务器下载一次,即在代理首次请求被测品种时,以封装形式传以降低流量;代理拿到的是封闭格式。下一次跑测试时代理不再从客户端重载,因为上轮结束数据已就位。外汇与贵金属回测结果受历史质量影响,存在模型误差风险,实际表现可能偏离。

多品种回测时历史数据怎么被拖进测试器

MT5 策略测试器原生支持多货币 EA,不必像旧平台那样只能单品种跑。它在第一次碰到某个交易品种的数据时,从客户端本地(不是交易服务器)拉历史,代理只补缺失部分加一点边际量,用来算测试起点处的指标。 对于 D1 及更短周期,最少会拽一年的历史。比如 2010.11.1–12.1 测一个月 M15 数据,客户端会要齐 2010 全年该品种历史;W1 周期要 100 根柱约两年;MN1 则直接要 8 年(12×8=96 根)。若本地柱不够,测试起始日会被自动往现在推,先攒够柱再说。 测试里也模拟了「市场报价」窗口。默认只挂正在跑的那个品种,代码里一引用别的品种就自动接进测试器的报价窗。正式跑之前,你得在客户端 Market Watch 里先选中要测的品种并加载好数据,首次调「外来」品种前历史会在代理和客户端间自动同步。 触发外来数据的情况很具体:用 IndicatorCreate 跨品种跨周期挂指标;用 SeriesInfoInteger、Bars、SymbolSelect、SymbolInfoTick 等捞报价信息;或用 CopyRates、CopyOpen、CopyBuffer 那一串函数取时间序列。第一次碰到时测试会暂停,从客户端给代理下历史并生成该品种的价格序列——每个品种依选定模式单独生成。 想抢在 EA 启动前就把历史备好,可以在 OnInit() 里直接调 SymbolSelect() 显式订阅。说到底在 MT5 做多货币测试几乎零额外活,开户端对应图表、保证品种历史在服务器上有,剩下的同步它自己干。外汇和贵金属杠杆高,回测过了也不代表实盘能复现。

「回测里三个时间函数会撞车」

在 MT5 策略测试器跑历史回测时,TimeLocal()、TimeTradeServer() 与 TimeGMT() 的返回值会被强制对齐。也就是说,测试环境下本地时间、交易服务器时间、GMT 时间三者完全一致,不存在时区偏移。 这种设定是刻意的:测试器不依赖真实网络连接,也不会把服务器时区信息落盘。无论你本地电脑在哪个时区、是否登录了经纪商账户,同一段历史数据的回测结果都应逐 tick 一致。 如果你在 EA 里用时间函数做会话过滤(比如只在伦敦时段下单),记得回测中它们会失去区分度。实盘前务必在 demo 账户用真实连接跑一遍,确认 TimeTradeServer() 返回的是经纪商服务器时间而非测试器的伪同步值。外汇与贵金属杠杆品种波动剧烈,时间逻辑错配可能引发非预期开仓,属高风险操作。

◍ 回测里 OnTimer 如何偷走你的时间

在 MT5 策略测试器中,OnTimer() 的触发与测试模式无关。以 H4 周期、仅开盘价模式跑 EA 为例:若计时器设为每 1 秒触发一次,每个 H4 柱开盘时 OnTick() 只跑 1 次,而 OnTimer() 会被调用 14400 次(3600 秒 × 4 小时)。EA 逻辑越依赖计时器,回测耗时就越可能显著拉长。 为量化这种依赖,我们用一个不含任何交易动作的空壳 EA 做对照,只改变 EventSetTimer 的入参(计时周期),记录总测试时间。数据呈现明确趋势:在相同其余条件下,初始化时 timer 参数越小,OnTimer() 调用间隔越短,测试时间 T 越长,二者近似反比关系。 下面这段就是被测 EA 的骨架,OnTimer 体内故意留空,纯粹用来暴露计时器本身的开销: [CODE] //--- 输入参数 input int timer=1; // 计时器的值,秒 input bool timer_switch_on=true; // 打开计时器 //+------------------------------------------------------------------+

//EA交易初始化函数

//+------------------------------------------------------------------+ int OnInit() { //---如果timer_switch_on==true, 运行计时器 if(timer_switch_on) { EventSetTimer(timer); } //--- return(0); } //+------------------------------------------------------------------+

//EA交易去初始化函数

//+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- 停止计时器 EventKillTimer(); } //+------------------------------------------------------------------+

//计时函数

//+------------------------------------------------------------------+ void OnTimer() { //--- // 句柄体为空,不做操作 } //+------------------------------------------------------------------+ [/CODE] 逐行看:input int timer=1 定义计时周期秒数,input bool timer_switch_on 控制是否启用;OnInit 里判断开关后调用 EventSetTimer(timer) 挂上计时器;OnDeinit 用 EventKillTimer 拆除;OnTimer 留空,因此测出的时间增量全来自框架调度本身。 实盘或回测前,建议把非必要的计时器周期调大,或在不需高频轮询时直接 timer_switch_on=false,外汇与贵金属交易本身高风险,冗长回测只会拖慢你的参数迭代节奏。

MQL5 / C++
class=class="str">"cmt">//--- 输入参数
input class="type">int      timer=class="num">1;                class=class="str">"cmt">// 计时器的值,秒
input class="type">bool     timer_switch_on=true;  class=class="str">"cmt">// 打开计时器
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| EA交易初始化函数                                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//---如果timer_switch_on==true, 运行计时器
   if(timer_switch_on)
     {
      EventSetTimer(timer);
     }
class=class="str">"cmt">//---
   class="kw">return(class="num">0);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| EA交易去初始化函数                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">const class="type">int reason)
  {
class=class="str">"cmt">//--- 停止计时器
   EventKillTimer();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 计时函数                                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTimer()
  {
class=class="str">"cmt">//---
class=class="str">"cmt">// 句柄体为空,不做操作
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

回测里 Sleep() 到底怎么改模拟时间

在 MQL5 程序里,Sleep() 能让 EA 或脚本暂停执行一段时间,典型场景是图形数据还没就绪、需要等一拍再读。它原本是给实盘或脚本兜底的等待手段,但丢进策略测试器后行为完全变了形。 测试器不会被 Sleep() 真的拖慢墙钟。调用时,指定的延迟区间内会“播放”生成好的价格变动,挂单、止损都可能在这段时间被触发;模拟时间直接往前跳过 Sleep 参数对应的间隔。也就是说,你在代码里睡 2 秒,回测时钟就硬走了 2 秒的量价。 坑在边界:如果 Sleep() 之后当前测试时间越过了测试周期末尾,测试器会报 “Infinite Sleep loop detected while testing”。这条错不代表结果作废——成交数、回撤等仍按已算出的峰值传给客户端,只是你拿到的是被截断的极端值。 另一个死点:OnDeinit() 里调 Sleep() 必然失效,因为函数返回后测试时间一定已超过测试区间,等于白睡。写等待逻辑时,把 Sleep 留在常规 tick 处理里,别指望退出钩子能帮你延时间。

常见问题

测试器按历史数据分块加载,指标只在首次触达或缓冲区失效时重算;想全量重算可手动清空缓存再跑。
主品种启动时会按设置的品种列表逐个向代理请求对应周期数据,缺失段会先下载再缓存到本地。
小布可扫描你的策略逻辑,标出 TimeCurrent、TimeTradeServer、TimeLocal 混用点和 OnTimer 占用,并给出改写建议。
回测中 Sleep() 按加速比折算并推进模拟时间,实盘则是真实阻塞;不要依赖它做跨周期等待。
测试器以模拟步长驱动定时器,高频步长下 OnTimer 会被合并触发,导致累计耗时偏离设定。