回放系统重构基础:把控制指标藏进服务内部(基础篇)
(1/3)· 为什么 MetaTrader 5 回放指标不该出现在用户指标列表里,以及怎么改
「从零搭回放系统的第一道坎」
在 MT5 里做价格回放,最先撞上的不是指标逻辑,而是数据流的重组方式。原生历史中心给的是已成型 K 线,而回放要的是逐笔 tick 的重演,这两者之间的缝隙,就是第 49 部分开头说的「事情变复杂」的由来。 实际在 MT5 终端里,用「文件→打开历史中心」能看到 EURUSD 的 M1 数据最早可追溯到 2003 年,但 tick 级数据默认只保留最近数月,做长周期回放前得先确认本地缓存深度,否则跑到一半断流。 小布盯盘的做法是先把历史 K 线按时间切片,再用自定义生成器补 tick,这样不依赖券商的 tick 源也能跑。外汇与贵金属杠杆高,回放再真实也只是概率推演,实盘仍需独立风控。
◍ 把控制指标藏进服务内部
早先的回放/模拟系统里,控制指标会直接挂在 MT5 的指标列表里,用户能随手拖到图表上。虽然之前加了锁定逻辑,错交易品种图表上强行加载会被拦住,但这类指标混在普通指标堆里始终是个隐患。 我花了数月反复试错,最终找到一种更干净的拆法:让控制指标不再对外暴露,而是作为回放/模拟服务的内部组件存在。MT5 指标列表里将看不到它,也就断了误用的可能。 这套重构会分步推进,顺带把指标对 MT5 的资源占用压下来——停用部分旧调用,改走平台其他原生机制。回放系统的稳定性、安全性与可靠性可能因此提升一个档次。 外汇与贵金属品种的高波动特性,要求回放环境尽量贴近实盘负载;内部化改造后,后续编制其他配套程序会更从容。
拆掉图表ID绑定的第一步
做回放/模拟器改造时,先动的是图表 ID 绑定系统。原来这套逻辑靠全局终端变量把控制指标锁在单一图表上,防止多图误挂;现在要删,但不是一次性清干净,而是先让服务与指标仍走同一交互点,再逐步抽掉全局变量依赖。
Interprocess.mqh 头文件第 6 行定义了全局变量名 def_GlobalVariableIdGraphics,专门传图表 ID。只要把这行删掉,编译指标和服务就会批量报错——这反而是好事,每个错误都精确指向要清理的耦合点。
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. {
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 终端验证。
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" 去跑黄金历史数据,外汇和贵金属波动属性差异大,实盘映射测试有高风险。
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)。外汇与贵金属回放涉及高杠杆高风险,仿真结论只代表历史数据概率,不能直接外推实盘。
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 配好再编译才看得到效果。
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); }