📘

先摸清 MT5 测试服务的底

◍ 先摸清 MT5 测试服务的底

在动手写回放系统前,得先搞清楚 MT5 自带的测试服务到底能干什么。它不只是跑历史回测,还提供策略测试器、代理网络和本地/远程计算资源调度,这些是后续回放架构的依赖底座。 官方在 2025 年 2 月 25 日发布的示例文档里,这一节被标记为系列第 57 部分,说明回放系统的工程化已经迭代到相当深的阶段,不是早期玩具代码。 一个容易踩的坑:很多人以为测试服务只能用于 EA 回测,实际上它暴露的接口层允许自定义数据源注入,这正是做价格回放的关键入口。外汇和贵金属品种在回放时滑点模拟偏差较大,属于高风险验证场景,结论仅具概率意义。

「为什么先搞懂服务而不是堆模块」

上篇附件里跑起服务后,会在 MT5 里建一个自定义交易品种,并把鼠标指标、控制指标都挂到这张图上。两个指标本身不碰任何服务逻辑,但你能从图上看到用户和模块之间的交互痕迹。 对新手来说,翻这两个指标的源码很容易懵:代码里似乎没有任何防数据丢失的机制,尤其是控制模块的数据。这恰恰是难点——看得到交互,看不到底层怎么兜住状态。 所以这篇不急着加新功能,而是把上篇那个服务的源码拆开讲。外汇和贵金属品种回放本身高风险,理解错了底层,模拟出来的信号可能完全失真。 先把这套更简单的服务代码吃透,比直接啃复杂结构划算得多。代码组件少,逻辑链短,搞明白它,后面鼠标模块和控制模块怎么跟服务打配合就顺了。 闲话到此,直接看上篇服务源码,配合文末视频在 MT5 里单步跑一遍最实在。

服务代码里资源嵌入与图表初始化的坑

MT5 服务与普通脚本最表面的差异就在代码第二行的 #property service 声明;缺了这行,MetaEditor 只会把它当脚本编译,而脚本必须挂靠图表,服务则完全不依赖任何图表运行。 第 10、11 行用资源指令把控制模块编译进服务本体,这意味着你分发已编译的 exe 时不用附带控制模块源码——前文附件只有两个可执行文件却在跑三个模块,原因就在这。但鼠标模块故意不嵌进去,是为了让用户能把它拖到别的图表上,嵌死在资源里就难碰了。 第 24 到 27 行干的事有点反直觉:先删市场报价里的交易品种,再从可用列表移除,然后立刻用第 26 行创建同名自定义交易品种。这里高风险点是——若路径写错,自定义品种会覆盖真实品种,之后你以为在看 XAUUSD 实盘报价,其实在读自定义数据。 第 35 到 37 行把定义的柱形设为资产首根 K 线并塞进市场报价,随后自动开图。第 38 到 40 行尝试给鼠标模块建句柄并加载到图表;句柄失败通常就是 exe 路径不对。第 41、42 行因为控制模块已嵌为资源,服务能直接把它挂上图表,少了路径依赖的麻烦。 第 47 行起的主循环靠第 13 行定义的退出条件来终结,否则服务像要退出的程序却卡在后台。第 49 行里的子循环更危险:若控制模块不在图表上,服务会一直等句柄,陷进死循环——删掉图表上的控制模块反而让 MT5 关图,使条件变 false 从而脱困。想验证这套逻辑,拿个最小服务先跑通资源嵌入和自定义品种创建,比直接改复杂系统省事得多。

MQL5 / C++
class="num">01. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">02. class="macro">#class="kw">property service
class="num">03. class="macro">#class="kw">property copyright "Daniel Jose"
class="num">04. class="macro">#class="kw">property description "Data synchronization demo service."
class="num">05. class="macro">#class="kw">property link "[MQL5官方文档]
class="num">06. class="macro">#class="kw">property version   "class="num">1.00"

◍ 用自定义品种搭一个回放沙盘

想在 MT5 里做行情回放,第一步不是加载历史数据,而是先造一个干净的自建品种。下面这段代码演示了从删除旧符号、创建 Custom 前缀品种、到写死一根 K 线的全过程,复制进 EA 的 OnStart 就能跑。 代码第 24–26 行先 SymbolSelect(false) 取消显示并 CustomSymbolDelete 清掉同名残留,再用 CustomSymbolCreate 以 "Custom\\%s" 格式建符号;第 27–28 行把跳价 TickSize 设 0.5、TickValue 设 5,这两个值直接决定回放品种的报价精度与每跳盈亏,外汇贵金属实盘映射时务必按品种属性改。 第 29–34 行手动填了一根 MqlRates:open 100 / high 120 / low 90 / close 110、tick_volume 5、时间锁在 2023.01.06 09:00,CustomRatesUpdate 推一根进去后 SymbolSelect(true) 让它在市场报价里可见。 图表侧用 ChartOpen(def_SymbolReplay, PERIOD_M30) 开 30 分钟图,随后两处 iCustom 分别挂 Mouse Study 与 Market Replay 控制指标,都走 ChartIndicatorAdd(id,0,handle) 且立刻 IndicatorRelease 释放本地引用——指标句柄由图表接管,不会泄漏。 def_Loop 宏定义为 !_StopFlag 且图表符号非空,意味着脚本会一直转直到你点停止;回放品种纯属本地模拟,和实盘报价脱节,拿它验证价格行为逻辑可以,但任何信号都不代表真实账户表现,外汇贵金属杠杆品种实盘仍属高风险。

MQL5 / C++
class="macro">#include <Market Replay\Defines.mqh>
class="macro">#define def_IndicatorControl   "Indicators\\Market Replay.ex5"
class="macro">#resource "\\" + def_IndicatorControl
class="macro">#define def_Loop((!_StopFlag) && (ChartSymbol(id) != ""))
class="type">void OnStart()
{
  uCast_Double info;
  class="type">long id;
  class="type">int handle;
  class="type">short iPos, iMode;
  class="type">class="kw">double Buff[];
  class="type">MqlRates Rate[class="num">1];
  
  SymbolSelect(def_SymbolReplay, false);
  CustomSymbolDelete(def_SymbolReplay);
  CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay));
  CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, class="num">0.5);
  CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, class="num">5);
  Rate[class="num">0].close = class="num">110;
  Rate[class="num">0].open = class="num">100;
  Rate[class="num">0].high = class="num">120;
  Rate[class="num">0].low = class="num">90;
  Rate[class="num">0].tick_volume = class="num">5;
  Rate[class="num">0].time = D&class="macro">#x27;class="num">06.01.class="num">2023 class="num">09:class="num">00&class="macro">#x27;;
  CustomRatesUpdate(def_SymbolReplay, Rate, class="num">1);
  SymbolSelect(def_SymbolReplay, true);
  id = ChartOpen(def_SymbolReplay, PERIOD_M30);
  if ((handle = iCustom(ChartSymbol(id), ChartPeriod(id), "\\Indicators\\Mouse Study.ex5", id)) != INVALID_HANDLE)
     ChartIndicatorAdd(id, class="num">0, handle);
  IndicatorRelease(handle);
  if ((handle = iCustom(ChartSymbol(id), ChartPeriod(id), "::" + def_IndicatorControl, id)) != INVALID_HANDLE)
     ChartIndicatorAdd(id, class="num">0, handle);
  IndicatorRelease(handle);
  Print("Service maintaining sync state. Version Demo...");
  iPos = class="num">0;
  iMode = SHORT_MIN;
  while (def_Loop)
  {

「回放控制指标的轮询与退出清理」

这段逻辑跑在一个服务函数里,核心是用 while 轮询拿到名为 "Market Replay Control" 的自定义指标句柄。循环条件是 def_Loop 为真且 ChartIndicatorGet 返回 INVALID_HANDLE,每次没拿到就 Sleep(50) 毫秒再试,避免空转吃掉 CPU。 拿到句柄后先用 CopyBuffer(handle,0,0,1,Buff) 取第 0 根缓冲,成功(返回 1)就把 Buff[0] 写进 info.dValue,随后立刻 IndicatorRelease(handle) 释放句柄。这里释放动作很关键,长轮询如果不释放,MT5 终端的指标句柄表会持续堆积。 info._16b[0] 等于 SHORT_MIN 时代表首次初始化,把当前 iPos、iMode 塞进 _16b 数组,用 EventChartCustom 发一个 evCtrlReplayInit 自定义事件;否则若 _16b[1] 非 0,就从数组回读 iPos、iMode 继续跑。每轮末尾 Sleep(250),实际回放控制刷新率约 4Hz。 退出时 def_Loop 置否,循环结束执行 ChartClose(id) 关掉回放副图,SymbolSelect(def_SymbolReplay,false) 取消品种关注,CustomSymbolDelete 删掉自定义回放符号,最后 Print("Finished service...")。外汇与贵金属回放测试属高频模拟,仍需注意实盘滑点与高风险。

MQL5 / C++
while (def_Loop && ((handle = ChartIndicatorGet(id, class="num">0, "Market Replay Control")) == INVALID_HANDLE)) Sleep(class="num">50);
info.dValue = class="num">0;
if (CopyBuffer(handle, class="num">0, class="num">0, class="num">1, Buff) == class="num">1) info.dValue = Buff[class="num">0];
IndicatorRelease(handle);
if ((class="type">short)(info._16b[class="num">0]) == SHORT_MIN)
{
   info._16b[class="num">0] = (class="type">class="kw">ushort)iPos;
   info._16b[class="num">1] = (class="type">class="kw">ushort)iMode;
   EventChartCustom(id, evCtrlReplayInit, class="num">0, info.dValue, "");
}else if (info._16b[class="num">1] != class="num">0)
{
   iPos = (class="type">short)info._16b[class="num">0];
   iMode = (class="type">short)info._16b[class="num">1];
}
Sleep(class="num">250);
}
ChartClose(id);
SymbolSelect(def_SymbolReplay, false);
CustomSymbolDelete(def_SymbolReplay);
Print("Finished service...");
}

别急着下结论

这套回放/模拟器服务不是凭空长出来的,它始终是按功能拆块、逐阶段拼起来的。前面那个小程序的价值就在这里:它把 C_Replay 类的运行逻辑摊开给你看,第 47 至 64 行的循环以后会直接并进大系统里。 真正动手时,改动几乎都集中在 C_Replay 类一处。若没搞清该类在哪、怎么调、为何调,后续模块很容易卡死,返工成本会明显高于预期。 下一篇我们要让回放系统按图表已显示的内容做计算,和本文示例略有差别。先花时间把本文 ZIP(420.65 KB)里的代码跑通,比急着追新章节更划算。外汇与贵金属品种波动剧烈,这类自研工具仅作技术验证,实盘前务必在小周期充分回测。

常见问题

先在自定义品种里灌入历史 tick 数据,再新建独立图表加载回放指标,避免和实盘图表共用导致资源冲突。
退出时必须显式调用 ObjectDelete 清理自建图形并 ResetLastError 检查,否则指标卸载后图表会留垃圾。
可以,小布能扫描自定义品种回放时的缺失数据和指标初始化日志,直接标出卡住的那一根并给清理建议。
资源在 OnInit 阶段同步解压会阻塞,改成首次事件触发时懒加载,能明显缩短图表打开耗时。
不靠谱,单轮回放样本太少,至少跑 5 个不重叠时段看概率分布再动实盘手数。