回放系统重构基础:把控制指标藏进服务内部(基础篇)
🛠️

回放系统重构基础:把控制指标藏进服务内部(基础篇)

(1/3)· 为什么 MetaTrader 5 回放指标不该出现在用户指标列表里,以及怎么改

实战向 第 1/3 篇
你大概也见过回放指标孤零零挂在 MT5 指标列表里,被人误拖到错误图表上。作者早先靠锁定功能拦住这类操作,但列表里多出一个本该隐形的控件始终令人不安。本篇开始动手,让控制指标退居服务内部,从架构上消掉这个隐患。

「从零搭回放系统的第一道坎」

在 MT5 里做价格回放,最先撞上的不是指标逻辑,而是数据流的重组方式。原生历史中心给的是已成型 K 线,而回放要的是逐笔 tick 的重演,这两者之间的缝隙,就是第 49 部分开头说的「事情变复杂」的由来。 实际在 MT5 终端里,用「文件→打开历史中心」能看到 EURUSD 的 M1 数据最早可追溯到 2003 年,但 tick 级数据默认只保留最近数月,做长周期回放前得先确认本地缓存深度,否则跑到一半断流。 小布盯盘的做法是先把历史 K 线按时间切片,再用自定义生成器补 tick,这样不依赖券商的 tick 源也能跑。外汇与贵金属杠杆高,回放再真实也只是概率推演,实盘仍需独立风控。

◍ 把控制指标藏进服务内部

早先的回放/模拟系统里,控制指标会直接挂在 MT5 的指标列表里,用户能随手拖到图表上。虽然之前加了锁定逻辑,错交易品种图表上强行加载会被拦住,但这类指标混在普通指标堆里始终是个隐患。 我花了数月反复试错,最终找到一种更干净的拆法:让控制指标不再对外暴露,而是作为回放/模拟服务的内部组件存在。MT5 指标列表里将看不到它,也就断了误用的可能。 这套重构会分步推进,顺带把指标对 MT5 的资源占用压下来——停用部分旧调用,改走平台其他原生机制。回放系统的稳定性、安全性与可靠性可能因此提升一个档次。 外汇与贵金属品种的高波动特性,要求回放环境尽量贴近实盘负载;内部化改造后,后续编制其他配套程序会更从容。

拆掉图表ID绑定的第一步

做回放/模拟器改造时,先动的是图表 ID 绑定系统。原来这套逻辑靠全局终端变量把控制指标锁在单一图表上,防止多图误挂;现在要删,但不是一次性清干净,而是先让服务与指标仍走同一交互点,再逐步抽掉全局变量依赖。 Interprocess.mqh 头文件第 6 行定义了全局变量名 def_GlobalVariableIdGraphics,专门传图表 ID。只要把这行删掉,编译指标和服务就会批量报错——这反而是好事,每个错误都精确指向要清理的耦合点。

MQL5 / C++
class="num">01. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">02. class="macro">#class="kw">property copyright "Daniel Jose"
class="num">03. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">04. class="macro">#define def_SymbolReplay                "RePlay"
class="num">05. class="macro">#define def_GlobalVariableReplay       def_SymbolReplay + "_Infos"
class="num">06. class="macro">#define def_GlobalVariableIdGraphics   def_SymbolReplay + "_ID"
class="num">07. class="macro">#define def_GlobalVariableServerTime   def_SymbolReplay + "_Time"
class="num">08. class="macro">#define def_MaxPosSlider               class="num">400
class="num">09. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">10. union u_Interprocess
class="num">11. {
class="num">12.     union u_0
class="num">13.     {
class="num">14.         class="type">class="kw">double  df_Value;      class=class="str">"cmt">// Value of the terminal global variable...
class="num">15.         class="type">class="kw">ulong   IdGraphic;     class=class="str">"cmt">// Contains the Graph ID of the asset...
class="num">16.     }u_Value;
class="num">17.     class="kw">struct st_0
class="num">18.     {
上面代码逐行拆一下:04~05 是回放服务的基础符号与信息变量名;06 是被划除的第 6 行,即我们要去掉的图表 ID 全局变量;07~08 是时间同步与滑杆上限(400 格);10~16 的 union 里,15 行 IdGraphic 正是原来装图表 ID 的字段,头文件清掉第 6 行后它也就成了死代码。 MetaEditor 目前没有 MAKE 编译系统,改了头文件不会自动重编依赖。服务代码第 12 行会拉起指标一起编,但前提是每次编服务前手动删掉指标的 exe,否则指标不重编。实测编服务先报 2 错 1 警,修掉服务侧 C_Replay.mqh 第 156 行后剩 1 错,编译器提示 compiling '\Indicators\Replay\Market Replay.mq5' failed——说明得去指标里修。 指标 Market replay.mq5 打开单独编,图 03 显示 5 错 5 警。规矩是从列表第一个错开始改,改完重编,别被上百个报错吓到, often 修第一个就全过了。划掉的行全删后图 04 剩一个第 77 行未使用变量警告,确认编译器说没用到再删,图 05 就干净通过。外汇/贵金属回测环境搭建有高风险,手动编译链路出错会直接让模拟器失效,建议先在 demo 终端验证。

MQL5 / C++
class="num">01. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">02. class="macro">#class="kw">property copyright "Daniel Jose"
class="num">03. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">04. class="macro">#define def_SymbolReplay                "RePlay"
class="num">05. class="macro">#define def_GlobalVariableReplay       def_SymbolReplay + "_Infos"
class="num">06. class="macro">#define def_GlobalVariableIdGraphics   def_SymbolReplay + "_ID"
class="num">07. class="macro">#define def_GlobalVariableServerTime   def_SymbolReplay + "_Time"
class="num">08. class="macro">#define def_MaxPosSlider               class="num">400
class="num">09. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">10. union u_Interprocess
class="num">11. {
class="num">12.     union u_0
class="num">13.     {
class="num">14.         class="type">class="kw">double  df_Value;      class=class="str">"cmt">// Value of the terminal global variable...
class="num">15.         class="type">class="kw">ulong   IdGraphic;     class=class="str">"cmt">// Contains the Graph ID of the asset...
class="num">16.     }u_Value;
class="num">17.     class="kw">struct st_0
class="num">18.     {

「回放服务的结构体与编译配置」

在 MT5 回放模拟服务的核心层,先用结构体锁住运行时状态。s_Infos 里 isPlay、isWait、isHedging、isSync 四个布尔量分别标记播放/暂停、等待提示、账户是锁仓还是净仓、服务是否同步;iPosShift 是 0 到 400 之间的 ushort,用来做界面位移偏移。 紧接着的 uCast_Double 是一个 union,同一块 8 字节内存可被解释为 double、long、datetime,或者长度为 sizeof(double) 的 int 数组(2 个)和 char 数组(8 个)。这种重叠读写在把价格、时间、标志位打包进单 double 传输时很实用,但改了 _char 再读 dValue 会得到不可预期的比特结果,调试时容易踩坑。 服务入口靠 #property service 声明,版本号写死在 1.49,依赖指标通过 #define def_Dependence_01 指向 Indicators\Replay\Market Replay.ex5 并用 #resource 嵌进 exe。input 里的 user00 默认给 "Forex - EURUSD.txt",也就是回放用的行情配置文件名——换品种就改这里,例如改成 "Forex - XAUUSD.txt" 去跑黄金历史数据,外汇和贵金属波动属性差异大,实盘映射测试有高风险。

MQL5 / C++
class="type">bool  isPlay;            class=class="str">"cmt">// Indicates whether we are in Play or Pause mode...
class="type">bool  isWait;            class=class="str">"cmt">// Tells the user to wait...
class="type">bool  isHedging;         class=class="str">"cmt">// If true we are in a Hedging account, if class="kw">false the account is Netting...
class="type">bool  isSync;            class=class="str">"cmt">// If true indicates that the service is synchronized...
class="type">class="kw">ushort iPosShift;        class=class="str">"cmt">// Value between class="num">0 and class="num">400...
} s_Infos;
class="type">class="kw">datetime ServerTime;
};
union uCast_Double
{
  class="type">class="kw">double  dValue;
  class="type">long    _long;            class=class="str">"cmt">// class="num">1 Information
  class="type">class="kw">datetime _datetime;      class=class="str">"cmt">// class="num">1 Information
  class="type">int     _int[class="kw">sizeof(class="type">class="kw">double)];   class=class="str">"cmt">// class="num">2 Informations
  class="type">char    _char[class="kw">sizeof(class="type">class="kw">double)];  class=class="str">"cmt">// class="num">8 Informations
};
class="macro">#class="kw">property service
class="macro">#class="kw">property icon "/Images/Market Replay/Icons/Replay - Device.ico"
class="macro">#class="kw">property copyright "Daniel Jose"
class="macro">#class="kw">property version   "class="num">1.49"
class="macro">#class="kw">property description "Replay-Simulator service for MT5 platform."
class="macro">#class="kw">property description "This is dependent on the Market Replay indicator."
class="macro">#class="kw">property description "For more details on this version see the article."
class="macro">#class="kw">property link "[MQL5官方文档]
class="macro">#define def_Dependence_01   "Indicators\\Replay\\Market Replay.ex5"
class="macro">#resource "\" + def_Dependence_01
class="macro">#include <Market Replay\Service Graphics\C_Replay.mqh>
input class="type">class="kw">string  user00 = "Forex - EURUSD.txt";  class=class="str">"cmt">//Replay configuration file.

◍ 回放服务的启停与资源回收

在 MT5 里跑历史回放仿真,入口参数先把图形周期钉死在 M5:input ENUM_TIMEFRAMES user01 = PERIOD_M5,这意味着回放起始视图默认按 5 分钟棒线渲染,换周期需改这一行而非运行时硬切。 OnStart 里用 new 抛出 C_Replay 实例,ViewReplay(user01) 返回 true 才进主循环;LoopEventOnTime(false) 的 false 表示不自动推进服务器时间,靠逐 tick 事件驱动。权限拿到后 Print 出提示,说明回放服务已挂载,此时可手动拖价或脚本注入 tick。 退出时析构函数 ~C_Replay() 做彻底清扫:先 ArrayFree 掉 m_Ticks 下的 Info 与 Rate 两个数组,再遍历 ChartFirst/ChartNext 关掉所有回放品种图表。注意第 151 行用 for 循环最多尝试 2 次 SymbolSelect(def_SymbolReplay, false) 把自定义品种从市场观察列表移除,失败也不阻塞。 最后 CustomRatesDelete 与 CustomTicksDelete 以 0 到 LONG_MAX 清空该品种全部自定义数据,CustomSymbolDelete 销户,并删掉 3 个全局变量(含 def_GlobalVariableIdGraphics)。外汇与贵金属回放涉及高杠杆高风险,仿真结论只代表历史数据概率,不能直接外推实盘。

MQL5 / C++
input ENUM_TIMEFRAMES   user01 = PERIOD_M5;       class=class="str">"cmt">//Initial graphic time.
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
{
   C_Replay  *pReplay;

   pReplay = new C_Replay(user00);
   if ((*pReplay).ViewReplay(user01))
   {
      Print("Permission granted. Replay service can now be used...");
      while ((*pReplay).LoopEventOnTime(class="kw">false));
   }
   class="kw">delete pReplay;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
~C_Replay()
   {
      ArrayFree(m_Ticks.Info);
      ArrayFree(m_Ticks.Rate);
      m_IdReplay = ChartFirst();
      do
      {
         if (ChartSymbol(m_IdReplay) == def_SymbolReplay)
            ChartClose(m_IdReplay);
      }while ((m_IdReplay = ChartNext(m_IdReplay)) > class="num">0);
      for (class="type">int c0 = class="num">0; (c0 < class="num">2) && (!SymbolSelect(def_SymbolReplay, class="kw">false)); c0++);
      CustomRatesDelete(def_SymbolReplay, class="num">0, LONG_MAX);
      CustomTicksDelete(def_SymbolReplay, class="num">0, LONG_MAX);
      CustomSymbolDelete(def_SymbolReplay);
      GlobalVariableDel(def_GlobalVariableReplay);
      GlobalVariableDel(def_GlobalVariableIdGraphics);
      GlobalVariableDel(def_GlobalVariableServerTime);
      Print("Finished replay service...");
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Daniel Jose"
class="macro">#class="kw">property icon "/Images/Market Replay/Icons/Replay - Device.ico"
class="macro">#class="kw">property description "Control indicator for the Replay-Simulator service."

回放指标靠全局变量认亲

这段初始化逻辑来自一个市场回放指标,它自己不画图(indicator_plots 0),纯靠后台 service 活着。版本号停在 1.49,描述里直说没加载服务就跑不起来——你挂上去若图表空白,先去确认 service 是否在终端里运行。 OnInit 里先用 C_Terminal 和 C_Controls 两个指针对象接管终端交互,任意一步 CheckPointer 返回 POINTER_INVALID 或 _LastError 非 ERR_SUCCESS 就直接 INIT_FAILED 退出。注意第 32 行:当前图表品种不等于回放品种、或全局变量 def_GlobalVariableIdGraphics 不存在,宏 macro_INIT_FAILED 会顺手把刚挂的指示器从图表删掉并返回失败。 位运算用来给图形设备打标签。def_BitShift 算出来是 (sizeof(ulong)*8)-1,在 64 位环境就是 63;ul 左移 63 位后和 Info 里的 IdGraphic 做按位或,再把结果写回全局变量。若发现最高位已被置 1(第 35 行),说明另一设备已占用,立刻失败——这避免了两个回放实例抢同一图表。 最后如果 def_GlobalVariableReplay 存在就取它的 double 值发 EventChartCustom 唤醒控制层,否则填 0。外汇和贵金属回放只是历史重演,高杠杆下模拟结论不能直接当实盘依据,开 MT5 把这段 include 路径 Market Replay\Service Graphics\C_Controls.mqh 配好再编译才看得到效果。

MQL5 / C++
class="macro">#class="kw">property description "This one doesn&class="macro">#x27;t work without the service loaded."
class="macro">#class="kw">property version   "class="num">1.49"
class="macro">#class="kw">property link "[MQL5官方文档]
class="macro">#class="kw">property indicator_chart_window
class="macro">#class="kw">property indicator_plots class="num">0
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include <Market Replay\Service Graphics\C_Controls.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define def_BitShift((class="kw">sizeof(class="type">class="kw">ulong) * class="num">8) - class="num">1)
class=class="str">"cmt">//+------------------------------------------------------------------+
C_Terminal *terminal = NULL;
C_Controls *control = NULL;
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define def_InfoTerminal(*terminal).GetInfoTerminal()
class="macro">#define def_ShortName      "Market_" + def_SymbolReplay
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
{
class="macro">#define macro_INIT_FAILED { ChartIndicatorDelete(def_InfoTerminal.ID, class="num">0, def_ShortName); class="kw">return INIT_FAILED; }
   u_Interprocess Info;
   class="type">class="kw">ulong ul = class="num">1;

   ResetLastError();
   if (CheckPointer(control = new C_Controls(terminal = new C_Terminal())) == POINTER_INVALID) class="kw">return INIT_FAILED;
   if (_LastError != ERR_SUCCESS) class="kw">return INIT_FAILED;
   ul <<= def_BitShift;
   IndicatorSetString(INDICATOR_SHORTNAME, def_ShortName);
   if ((def_InfoTerminal.szSymbol != def_SymbolReplay) || (!GlobalVariableCheck(def_GlobalVariableIdGraphics))) macro_INIT_FAILED;
   Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableIdGraphics);
   if (Info.u_Value.IdGraphic != def_InfoTerminal.ID) macro_INIT_FAILED;
   if ((Info.u_Value.IdGraphic >> def_BitShift) == class="num">1) macro_INIT_FAILED;
   IndicatorSetString(INDICATOR_SHORTNAME, def_ShortName + "Device");
   Info.u_Value.IdGraphic |= ul;
   GlobalVariableSet(def_GlobalVariableIdGraphics, Info.u_Value.df_Value);
   if (GlobalVariableCheck(def_GlobalVariableReplay)) Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay); else Info.u_Value.df_Value = class="num">0;
   EventChartCustom(def_InfoTerminal.ID, C_Controls::ev_WaitOff, class="num">1, Info.u_Value.df_Value, "");
   (*control).Init(Info.s_Infos.isPlay);
}
交给小布盯盘看后台负载
这类回放服务的资源占用与图表绑定异常,小布盯盘的 AIGC 已内置诊断视角,打开对应品种页即可看到实时负载与冲突提示,把重复劳动交给小布,你专注决策。

常见问题

早期实现把控制逻辑做成了独立指标,平台就会把它列进用户可选指标。本篇基础篇讨论如何将其改为服务的组成部分,不再对外暴露。
本篇先移除旧绑定,后续会在服务侧重新接管校验,由回放/模拟器服务执行操作而非指标自身,安全性反而更高。
会逐步修改,作者刻意展示清理过程以便经验较少者理解,旧代码交互点保留通用接口,降低迁移成本。
可以,小布盯盘的 AIGC 模块能呈现品种页的负载与图表冲突信息,帮助快速定位回放系统是否占用异常资源。
关于完整重构与进阶技巧的讨论见《开发回放系统·实战篇》,本篇仅做概念铺垫与基础清理。