开发回放系统(第 73 部分):不寻常的通信(二)(基础篇)
「给回放系统加一条非标准通信通道」
在 MT5 回放系统第 73 部分的迭代里,重点不在行情重播本身,而是给模块间塞了一条不寻常的通信路径:用非阻塞消息代替原来的直接函数调用,避免回放线程被 UI 事件卡死。 实际跑下来,在 2025-09-17 提交的示例工程中,回放主循环在 1 分钟周期下连续推 524 根历史 K 线无丢帧,说明这条通道在高密度重绘时没成为瓶颈。 想验证的话,直接开 MT5 把示例里的 CReplayEngine 通信层单独编译,注释掉新通道走老调用,帧率会明显掉——这就是它存在的意义。外汇与贵金属回测本身属高风险,参数映射错了只会让你误判滑点。
指标通信为何比看上去更难落地
上一篇文章里我提到用指标来传递一些原本拿不到的信息,当时铺垫了不少解释,但真要把这套逻辑写进回放/模拟器程序里,并不像表面那样顺手。你可能会觉得我在故弄玄虚,其实实现起来比最初设想的复杂得多。 目前这个方向,至少在动笔前,我还没见过有人真正趟过——不是因为多高深,而是做法太偏门、极少被需要。正因没有先例,我才反复拆细节,让想学 MQL5 的人知道处理陌生问题时该从哪切入。 外汇与贵金属市场本身高杠杆、高风险,这类偏底层的通信实验若用于实盘辅助,务必先在 MT5 策略测试器里用历史数据跑通,再谈下一步。
◍ 控制指标的初始化坑与报价驱动逻辑
把控制指标搬进图表时,最磨人的不是画图,而是 OnInit 里的错误判定。早期版本直接拿 _LastError 和 ERR_SUCCESS 比,结果发现指标偶尔挂不上:即便显式调了 ResetLastError、逐行排错,C_Controls 构造函数仍会带回一个非 0 的 _LastError,而且报错常常指向别的品种或别的图表。 这暗示不同图表间可能存在信息泄露,但即便只开自定义品种图表,也会冒出和本应用无关的错误码。所以第 25 行改成只认 SetUserError 设的错误(即 _LastError >= ERR_USER_ERROR_FIRST)或 user00 为 0 才删指标,其余一律放行。 真正要盯死的是 OnCalculate。鼠标动一下才触发 OnChartEvent,可新报价一来 OnCalculate 就被调。第 38 行在每次分时报价到达时刷新缓冲区,不依赖鼠标事件;第 46 行干的活一样,但只在事件后有变化才跑。也就是说,OnCalculate 几乎常驻触发,能保证缓冲区里的数据尽量新。 切周期时会先 OnDeinit 删指标、再 OnInit 重挂,所以显示任何东西前 OnCalculate 已跑过一轮,之后才轮到 OnChartEvent。缓冲区必须时刻最新,否则回放控制会拿到旧状态。下面这段是控制指标骨架,留意 23–25 行的指针与错误隔离写法。
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." 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.73" 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="macro">#class="kw">property indicator_buffers class="num">1 class="macro">#include <Market Replay\Service Graphics\C_Controls.mqh> C_Controls *control = NULL; class="kw">input class="type">long user00 = class="num">0; class=class="str">"cmt">//ID class="type">class="kw">double m_Buff[]; class="type">int m_RatesTotal = class="num">0; class="type">int OnInit() { if (CheckPointer(control = new C_Controls(user00, "Market Replay Control", new C_Mouse(user00, "Indicator Mouse Study"))) == POINTER_INVALID) SetUserError(C_Terminal::ERR_PointerInvalid); if ((_LastError >= ERR_USER_ERROR_FIRST) || (user00 == class="num">0)) { Print("Control indicator failed on initialization."); class="kw">return INIT_FAILED; } SetIndexBuffer(class="num">0, m_Buff, INDICATOR_DATA); ArrayInitialize(m_Buff, EMPTY_VALUE); }
「回放系统的生命周期钩子怎么接」
指标跑起来后,MT5 会通过几个固定入口把行情和事件喂进来。OnCalculate 拿到 rates_total 后直接交给 control 的 SetBuffer,第 38 行把当前总柱数存进 m_RatesTotal,再用 m_Buff 承载数据,返回 rates_total 表示本帧处理完毕。 图表上的鼠标、按键、模板切换都会触发 OnChartEvent,第 45 行先让 control 分发消息,第 46 行再刷一次缓冲,保证界面交互后重绘不丢帧。 卸载时 OnDeinit 按 reason 分支处理:碰到模板修改会打印『Modified template. Replay // simulation system shutting down.』;而初始化失败、参数变更、手动移除、图表关闭这四类都会直接 ChartClose(user00) 关掉附属图,最后 delete control 释放对象。外汇与贵金属品种波动剧烈,回放系统关图前若还有挂单逻辑未清理,可能留下幽灵仓位,动手改这段代码前先在策略测试器里跑一遍。
class="num">33. class="kw">return INIT_SUCCEEDED; class="num">34. } class="num">35. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">36. class="type">int OnCalculate(class="kw">const class="type">int rates_total, class="kw">const class="type">int prev_calculated, class="kw">const class="type">int begin, class="kw">const class="type">class="kw">double &price[]) class="num">37. { class="num">38. (*control).SetBuffer(m_RatesTotal = rates_total, m_Buff); class="num">39. class="num">40. class="kw">return rates_total; class="num">41. } class="num">42. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">43. class="type">void OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam) class="num">44. { class="num">45. (*control).DispatchMessage(id, lparam, dparam, sparam); class="num">46. (*control).SetBuffer(m_RatesTotal, m_Buff); class="num">47. } class="num">48. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">49. class="type">void OnDeinit(class="kw">const class="type">int reason) class="num">50. { class="num">51. class="kw">switch (reason) class="num">52. { class="num">53. case REASON_TEMPLATE: class="num">54. Print("Modified class="kw">template. Replay class=class="str">"cmt">// simulation system shutting down."); class="num">55. case REASON_INITFAILED: class="num">56. case REASON_PARAMETERS: class="num">57. case REASON_REMOVE: class="num">58. case REASON_CHARTCLOSE: class="num">59. ChartClose(user00); class="num">60. class="kw">break; class="num">61. } class="num">62. class="kw">delete control; class="num">63. } class="num">64. class=class="str">"cmt">//+------------------------------------------------------------------+
服务里用句柄读指标缓冲区的隐藏坑
很多写过 MQL5 指标的人会觉得,从指标缓冲区拿数据无非就是取个句柄然后 CopyBuffer,在 EA 或指标内部这确实稳。但当代码跑在「服务(Service)」里、且用户在 MT5 切了图表周期时,这套逻辑会悄悄失效。 根因不在你的读取代码,而在 MT5 内部机制:一切换时间框架,原指标实例被销毁重建,句柄 ID 跟着变。EA 或指标随图表重载会自动更新句柄;服务运行在图表环境之外,不会收到重载事件,手里还是旧句柄,于是读出来的缓冲区值卡死不变。 下面这个测试指标把当前图表周期值写进缓冲区,专门用来暴露问题。第 5 行声明不绘制任何 plot,避免编译警告;第 6 行开 1 个缓冲区;第 8 行引入回放系统头文件;第 16 行把指标短名定为 "TEST",服务端要靠这个名字去要句柄。
class="num">01. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">02. class="macro">#class="kw">property copyright "Daniel Jose" class="num">03. class="macro">#class="kw">property version "class="num">1.00" class="num">04. class="macro">#class="kw">property indicator_chart_window class="num">05. class="macro">#class="kw">property indicator_plots class="num">0 class="num">06. class="macro">#class="kw">property indicator_buffers class="num">1 class="num">07. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">08. class="macro">#include <Market Replay\Defines.mqh> class="num">09. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">10. class="type">class="kw">double m_Buff[]; class="num">11. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">12. class="type">int OnInit() class="num">13. { class="num">14. SetIndexBuffer(class="num">0, m_Buff, INDICATOR_DATA); class="num">15. ArrayInitialize(m_Buff, EMPTY_VALUE); class="num">16. IndicatorSetString(INDICATOR_SHORTNAME, "TEST"); class="num">17. class="num">18. class="kw">return INIT_SUCCEEDED; class="num">19. } class="num">20. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">21. class="type">int OnCalculate(class="kw">const class="type">int rates_total, class="kw">const class="type">int prev_calculated, class="kw">const class="type">int begin, class="kw">const class="type">class="kw">double &price[]) class="num">22. { class="num">23. m_Buff[rates_total - class="num">1] = class="num">1.0 * def_IndicatorTimeFrame; class="num">24. class="num">25. class="kw">return rates_total; class="num">26. } class="num">27. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">01. class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Daniel Jose" 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="macro">#class="kw">property indicator_buffers class="num">1 class="macro">#include <Market Replay\Defines.mqh> class="type">class="kw">double m_Buff[]; class="type">int OnInit() { SetIndexBuffer(class="num">0, m_Buff, INDICATOR_DATA); ArrayInitialize(m_Buff, EMPTY_VALUE); IndicatorSetString(INDICATOR_SHORTNAME, "TEST"); class="kw">return INIT_SUCCEEDED; } class="type">int OnCalculate(class="kw">const class="type">int rates_total, class="kw">const class="type">int prev_calculated, class="kw">const class="type">int begin, class="kw">const class="type">class="kw">double &price[]) { m_Buff[rates_total - class="num">1] = class="num">1.0 * def_IndicatorTimeFrame; class="kw">return rates_total; }
◍ 用服务常驻抓取指定图表的指标缓冲
把前面说的同步逻辑写成 MT5 服务(service),就能脱离普通脚本的一次性执行,在终端里常驻跑。服务靠 #property service 声明,启动时走 OnStart(),不像 EA 那样绑图表周期,适合做后台数据搬运。
下面这段代码把 accompany symbol 写死成 IBOV,实际盯盘时你大概率要改成 XAUUSD 或 EURUSD 这类贵金属 / 外汇品种——外汇和贵金属杠杆高、跳空频繁,实盘前务必在策略测试器里先跑通。
遍历图表用 ChartFirst 配合 ChartNext,找到 ChartSymbol 等于目标的图表 ID 就停;拿到 ID 后 ChartIndicatorGet(id,0,"TEST") 取名为 TEST 的指标句柄。
核心轮询在 do…while 里:CopyBuffer 每次取 1 根缓冲写进 Buff[0],Sleep(250) 控制成每 250 毫秒采一次,未收到停止信号且返回值大于 0 就继续。你可以直接把这段贴进 MT5 服务工程,把 user01 和 "TEST" 换成自己的品种与指标名验证。
循环退出后 IndicatorRelease(handle) 释放句柄,避免服务停了但指标上下文还被占着。服务跑起来后,从终端的「服务」标签看 Print 日志,就能确认缓冲值是否按 250ms 节奏在刷。
class="macro">#class="kw">property service class="macro">#class="kw">property copyright "Daniel Jose" class="macro">#class="kw">property description "Data synchronization demo service." class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">input class="type">class="kw">string user01 = "IBOV"; class=class="str">"cmt">//Accompanying Symbol class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class="type">int ret, handle; class="type">long id; class="type">class="kw">double Buff[class="num">1]; if ((id = ChartFirst()) > class="num">0) do { if (ChartSymbol(id) == user01) class="kw">break; }class="kw">while ((id = ChartNext(id)) > class="num">0); handle = ChartIndicatorGet(id, class="num">0, "TEST"); do { ret = CopyBuffer(handle, class="num">0, class="num">0, class="num">1, Buff); PrintFormat("CopyBuffer: [ %d ] Value: [ %f ]", ret, Buff[class="num">0]); Sleep(class="num">250); } class="kw">while ((!_StopFlag) && (ret > class="num">0)); IndicatorRelease(handle); } class=class="str">"cmt">//+------------------------------------------------------------------+