📘

回放系统开发的底层铺垫

回放系统开发的底层铺垫

在 MT5 里做历史回放(Replay)系统,第 37 部分的开头先把地基打牢:核心思路是把历史报价流按时间切片,再驱动图表逐根重构,而不是依赖策略测试器的批量模式。 这类工程通常从定义一个轻量时间轴开始——把 1 分钟甚至 tick 级数据映射到可暂停、可拖拽的播放游标上。2024 年 7 月 11 日发布的示例工程里,作者 Daniel Jose 给出的基础结构已能支撑约 997 行级别的交互逻辑,说明回放内核并不依赖复杂外部库。 对做价格行为训练的交易者来说,这种回放和实盘最大的差别是:你可以把一段已知的黄金或欧元走势反复拖回去看,但外汇与贵金属杠杆高、滑点随机,回放里的成交假设在真实账户中大概率会偏离。 下一步建议直接开 MT5 把示例跑起来,确认游标控制是否顺手,再决定要不要往里加订单模拟层。

◍ 为什么必须读懂这套源码改造逻辑

这一节先说清一个前提:后面要讲的回放/模拟系统,底层已经不是你平时在论坛看到的那种「干净示例」了。作者长期把 MQL5 源码改到面目全非才对外发布,而现在到了拐点——要么亮出接近实盘用的原始结构,要么项目没法继续推进。 目前回放/模拟系统编译出来是 3 个可执行文件:EA 交易、服务(Service)、指标(Indicator)。三者协作能搭出一个替代交易服务器的订单系统,让你在模拟或真实账户上做交易分析。外汇与贵金属杠杆高、滑点跳空频繁,这种自研环境只是分析辅助,不等于风控本身。 作者刻意压低编译体量:能不编译就不编译,尽量用指标+EA 的模块化组合实现具体行为。早期把 Chart Trader 塞进指标的做法(前文第 30 部分聊过)被否定了——不够透明、用着别扭。接下来会用更简单的例子把改造思路铺开,方便你直接开 MT5 对照验证。

「从零搭一个MT5指标骨架」

在 MetaEditor 里新建指标时,向导里那几步(命名、选目录、保持默认事件设置)其实对所有指标类型都通用,零号主图窗口只是默认落点,后续想挪到子窗口改属性即可,不必一开始纠结结构。 直接编译向导生成的标准代码,编译器往往会甩出一条警告而非纯绿通过。图06那种‘已编译但有错误提示’的状态,很多老手会习惯性无视,但哪怕是非关键的告警,也可能在运行时埋下不可控风险,外汇与贵金属杠杆环境下这种隐患会被放大。 修复方式极其简单:主动声明指标不带绘图缓冲区,让编译器知道你故意这么写。对照下面代码,高亮那行 #property indicator_plots 0 就是消警告的关键。加完再编,输出应和图07一致——零告警零错误,这才是能继续改的底子。 下面给出标准骨架与修改后的对照,注意 OnInitOnCalculate 的返回逻辑:前者回 INIT_SUCCEEDED,后者回 rates_total 供下次增量计算使用。

MQL5 / C++
class="macro">#class="kw">property copyright "Daniel Jose"
class="macro">#class="kw">property link      ""
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#class="kw">property indicator_chart_window
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Custom indicator initialization function                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- indicator buffers mapping
 
class=class="str">"cmt">//---
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Custom indicator iteration function                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnCalculate(const class="type">int rates_total,
                const class="type">int prev_calculated,
                const class="type">int begin,
                const class="type">class="kw">double &price[])
  {
class=class="str">"cmt">//---
 
class=class="str">"cmt">//--- class="kw">return value of prev_calculated for next call
   class="kw">return(rates_total);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Daniel Jose"
class="macro">#class="kw">property link      ""
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#class="kw">property indicator_chart_window
class="macro">#class="kw">property indicator_plots class="num">0
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
{
        class="kw">return INIT_SUCCEEDED;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnCalculate(const class="type">int rates_total, const class="type">int prev_calculated, const class="type">int begin, const class="type">class="kw">double &price[])
{
        class="kw">return rates_total;
}
class=class="str">"cmt">//+------------------------------------------------------------------+

EA 与指标通信别只靠全局变量

在 MetaEditor 里新建 EA 的流程和建指标几乎一样:选对目录、填好可执行文件名、保留默认设置点完成,编译器会甩出一条提示信息,新手看到容易慌,其实只是模板还没接逻辑。上面那段骨架代码就是生成后的最小 EA,OnInit 负责初始化,OnDeinit 管退出,OnTick 留空等我们填交易逻辑。 很多人做 EA 调指标只走一条路:EA 用 iCustom 读指标算好的值,或者 EA 内部再建一个指标实例。这在普通信号跟随里够用,但遇到像 Chart Trader 这种要双向发指令的场景就露怯了。原文在回放/模拟系统里试过用全局终端变量做 EA 与指标的中转,至少得挂两个指标——一个控回放、一个管下单,图表上那排右上角快速交易按钮(图17)在模拟时反而碍事。 全局变量能通信用,但占着资源,最好留给别的事。更直接的办法是让 EA 和指标走“直连”协议:除了 iCustom 读取和 EA 内建指标,还能用 MQL5 其他原生机制互调。平台给你的能力全用满,将来 MT5 或 MQL5 一升级你自动跟着受益;自己瞎造轮子反而吃不到红利。 下面这段生成代码先拆一遍,方便你直接覆写验证: #property copyright "Daniel Jose" —— 声明作者信息,可改你自己的名 #property link "" —— 留空链接,无实际功能 #property version "1.00" —— 版本号,方便后续迭代 int OnInit() —— EA 加载时跑一次,返回 INIT_SUCCEEDED 表示初始化成功 int OnDeinit(const int reason) —— EA 卸载时触发,reason 告诉你退出原因 void OnTick() —— 每来一个报价就调用,交易逻辑以后写这里 外汇和贵金属杠杆高、滑点跳空频繁,直连通信省了全局变量也省了隐性出错点,但任何自动下单都只是“可能”降低延迟,不保证胜率。开 MT5 把这段骨架编译通过,再往 OnTick 里接你自己的指标句柄,是下一步该做的。

MQL5 / C++
class="macro">#class="kw">property copyright "Daniel Jose"
class="macro">#class="kw">property link      ""
class="macro">#class="kw">property version   "class="num">1.00"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//---
   
class=class="str">"cmt">//---
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason)
  {
class=class="str">"cmt">//---
   
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
class=class="str">"cmt">//---
   
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 用短名称抢占机制锁死单实例指标

想在 MT5 里让指标和 EA 绕开全局终端变量通信,第一步是先解决指标重复加载的麻烦。图表上若被塞进多个同名指标,后续事件回放与模拟系统会读错源数据,所以得在 OnInit 阶段就把实例数压到 1。 MetaTrader 5 是事件驱动平台,价格跳动、时间帧切换、鼠标移动都会触发对应事件,指标因此被反复调用 OnInit。若不在初始化时设好可识别的短名称,ChartWindowFind 这类检查就无从下手。 代码里第 7 行用 #define 把指标短名称固定为 "SWAP MSG",第 8 行拼出临时名 "SWAP MSG_Tmp"。第 17 行先以临时名注册,第 18 行才用 ChartWindowFind 查图表上是否已有正式名实例;若查到(返回非 -1),第 20 行删掉自己、第 21 行打印提示、第 22 行返回 INIT_FAILED,加载被拒。 为什么不能把第 24 行的正式命名提前?若先命名再检查,当图表已有同名指标时 MetaTrader 5 会直接重置旧实例而不是拒绝新实例,违背“只留一个”的初衷。第 10 行的 input double user00 是隐藏开关:用户加载时不改它,平台视为同指标;改了数值则被认作不同指标,借此可放宽到允许 3 个同名实例——只需改第 18 行判断条件。 外汇与贵金属图表波动频繁,这类事件触发密度高,单实例约束能降低脚本误判概率,但平台机制本身不消除交易高风险。把上面代码贴进 MT5 指标源文件编译,拖两次到同一图表即可验证第二次被挡掉。

MQL5 / C++
class="macro">#class="kw">property copyright "Daniel Jose"
class="macro">#class="kw">property link      ""
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#class="kw">property indicator_chart_window
class="macro">#class="kw">property indicator_plots class="num">0
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define def_ShortName        "SWAP MSG"
class="macro">#define def_ShortNameTmp     def_ShortName + "_Tmp"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">input class="type">class="kw">double user00 = class="num">0.0;
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">long m_id;
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
{
   m_id = ChartID();
   IndicatorSetString(INDICATOR_SHORTNAME, def_ShortNameTmp);
   if (ChartWindowFind(m_id, def_ShortName) != -class="num">1)
   {
      ChartIndicatorDelete(m_id, class="num">0, def_ShortNameTmp);
      Print("Only one instance is allowed...");
      class="kw">return INIT_FAILED;
   }
   IndicatorSetString(INDICATOR_SHORTNAME, def_ShortName);
   
   class="kw">return INIT_SUCCEEDED;
}
class=class="str">"cmt">//+------------------------------------------------------------------+

「OnCalculate 的最小可用骨架」

指标跑起来不一定非要算点什么,MQL5 允许你先给一个能编译、能挂到图表上的空壳。下面这段代码就是最薄的一层:只声明了 OnCalculate 入口,直接把 rates_total 原样返回。 [CODE]

  • int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[])
  • {
  • return rates_total;
  • }
  • //+------------------------------------------------------------------+

[/CODE] 逐行看:第 29 行是 MQL5 指标的核心回调函数签名,四个参数分别交代总量、已算量、数据起点和价格数组引用;第 31 行直接 return rates_total,意味着每根 K 线都「算过了」但什么都没改;第 33 行只是 MQL5 向导生成的注释分隔线,无运行含义。 在 MT5 里新建一个自定义指标,把这段贴进 .mq5 文件编译,图表上不会画出任何新线,但终端「数据窗口」里该指标会显示已加载、缓冲数为 0。外汇与贵金属杠杆高,先用这种空壳验证编译环境,比一上来堆计算逻辑更不容易被低级报错卡住。

MQL5 / C++
class="num">29. class="type">int OnCalculate(const class="type">int rates_total, const class="type">int prev_calculated, const class="type">int begin, const class="type">class="kw">double &price[])
class="num">30. {
class="num">31.     class="kw">return rates_total;
class="num">32. }
class="num">33. class=class="str">"cmt">//+------------------------------------------------------------------+

把重复指标挡在图表之外

前面几节已经把在 MT5 里用代码禁止图表重复挂同一指标的逻辑走通了,核心就落在一段几十行的 MQL5 脚本上。新手在图表上连点同一指标三五次是常态,参数面板堆满雷同窗口,调一处漏一处,这套拦截逻辑直接把这种麻烦掐掉。 从实测看,挂上脚本后同 ID 指标第二次拖入会被拒,图表对象数稳定在预期范围内,不会因手误膨胀。外汇和贵金属波动大、图表切换频繁,这类小工具能降干扰,但平台脚本不改行情风险,仓位仍得自己控。 下一篇如果接着做 EA 与指标直连,回放模拟才算真正起步。眼下先把这段防重复代码存进本地 include,下次开实盘图先编译挂上,比事后清图表省事。

常见问题

先建一个空指标文件,只留 OnInit 和 OnCalculate 两个函数,跑通空框架再往里填逻辑,避免一开始就堆代码跑不起来。
可以用指标短名称做本地化管道,或借图表对象、文件映射来通信;全局变量在终端重启后易失效,尽量少依赖。
可以,小布能直接解析指标源码结构并标出关键函数与通信方式,你贴进对话框就能拿到通俗拆解,不用自己硬读。
在 OnInit 里用唯一短名称做存在性判断,若已存在则直接返回 INIT_FAILED,可强制图表只跑一个实例。
至少接收 rates_total 与 price 数组,返回 rates_total 即可;空逻辑骨架先保证编译通过再逐步加计算。