开发回放系统(第 64 部分):玩转服务(五)(基础篇)
「在 MT5 里给回放系统接一个自定义服务」
MetaTrader 5 的回放系统做到第 64 部分,重点转到「服务(Service)」这一类特殊程序上。服务和普通 EA 不同,它不绑定图表、不靠 tick 驱动,而是常驻后台,适合做回放时的数据搬运和状态中转。
- 年 6 月 10 日发布的示例里,作者 Daniel Jose 给出的工程在社群获得 507 次查看、0 条回复,说明这套玩法还偏冷门,但恰好适合想绕开图表资源占用的老手自行验证。
真正落地时,先在 MT5 的「文件→打开数据文件夹→MQL5→Services」里放好编译后的 .ex5,再从导航器拖入「服务」节点启动,回放脚本就能跨周期读到它暴露的缓冲数据。外汇与贵金属回放含跳空与滑点,属高风险验证环境,结论仅具概率意义。
◍ 回放系统为何莫名暂停
在上一阶段开发的回放服务里,我们给用户开放了「单根柱形最大分时报价数」的调节入口。这个参数本意只是防止柱形重建拖垮模拟器的其他模块,并不会篡改成交量本身的数值。 但只要你实际编译跑过这套回放应用,或者看过演示视频,就会撞见一个怪现象:系统会冷不丁切到暂停,而控制指标面板上什么变化都没标。 我反复在固定点位复现过这个自动暂停,起初完全摸不着头脑——播放态没有任何外部指令却被打断。下一节会拆开我后来定位到的成因和对应修法,这里先记住:暂停触发点和柱形报价数上限之间藏着直接关联。
回放服务误判暂停的底层岔路
MT5 回放/模拟器里最反直觉的坑之一:服务自己没发暂停指令,系统却切到了暂停。根因出在控制指标内部——服务通过自定义事件通知指标后,指标所维护按钮对象的 OBJPROP_STATE 属性会在无明确理由下被改动,而服务恰好在播放模式下直接读这个图形对象属性,于是误以为用户点了暂停。 看 C_Replay.mqh 里这段逻辑:第 38 行静态变量 bTest 控制读源,第 45 行在 bTest 为真时直接 ObjectGetInteger 读 OBJPROP_STATE,第 48 行否则从指标缓冲区 CopyBuffer。播放模式从第三次调用起走第 45 行,一旦 OBJPROP_STATE 被自定义事件连带改错,LoopEventOnTime 就终止并重初始化,再次检查时仍拿到错值,服务便卡在暂停。 [CODE]
- //+------------------------------------------------------------------+
- inline void UpdateIndicatorControl(void)
- {
- static bool bTest = false;
- double Buff[];
40.
- if (m_IndControl.Handle == INVALID_HANDLE) return;
- if (m_IndControl.Memory._16b[C_Controls::eCtrlPosition] == m_IndControl.Position)
- {
- if (bTest)
- m_IndControl.Mode = (ObjectGetInteger(m_Infos.IdReplay, def_ObjectCtrlName((C_Controls::eObjectControl)C_Controls::ePlay), OBJPROP_STATE) == 1 ? C_Controls::ePause : C_Controls::ePlay);
- else
- {
- if (CopyBuffer(m_IndControl.Handle, 0, 0, 1, Buff) == 1)
- m_IndControl.Memory.dValue = Buff[0];
- if ((C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus] != C_Controls::eTriState)
- if (bTest = ((m_IndControl.Mode = (C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus]) == C_Controls::ePlay))
[/CODE] 逐行拆解:35–36 是 UpdateIndicatorControl 内联函数入口;38 声明静态 bool bTest 初始 false,决定下次走对象还是缓冲区;41 若指标句柄无效直接退出;42 比对内存位与控制位位置一致才进分支;44 若 bTest 真,45 直接用 ObjectGetInteger 读按钮 OBJPROP_STATE,为 1 则切暂停否则播放;46–47 否则走缓冲区;48–49 从句柄 0 位置拷 1 根缓冲到 Buff,成功则存 dValue;50–51 若状态非三态,把 Mode 设为内存里的控制状态,且当其为播放时把 bTest 置真,下次调用便改读对象。 别把对象属性当信源 若你必须在图表对象上读控制态,OBJPROP_TOOLTIP 字符串属性在测试里没出过错,能区分播放/暂停。但它传不了快进所需的信息量。作者最终删掉静态变量、改回全量 CopyBuffer,代价是稍多耗时,换来后续能加快进模式。外汇与贵金属仿真回放属高风险验证环境,结论仅限本地 MT5 测试,不预示实盘表现。
class="num">35. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">36. class="kw">inline class="type">void UpdateIndicatorControl(class="type">void) class="num">37. { class="num">38. class="kw">static class="type">bool bTest = false; class="num">39. class="type">class="kw">double Buff[]; class="num">40. class="num">41. if (m_IndControl.Handle == INVALID_HANDLE) class="kw">return; class="num">42. if (m_IndControl.Memory._16b[C_Controls::eCtrlPosition] == m_IndControl.Position) class="num">43. { class="num">44. if (bTest) class="num">45. m_IndControl.Mode = (ObjectGetInteger(m_Infos.IdReplay, def_ObjectCtrlName((C_Controls::eObjectControl)C_Controls::ePlay), OBJPROP_STATE) == class="num">1 ? C_Controls::ePause : C_Controls::ePlay); class="num">46. else class="num">47. { class="num">48. if (CopyBuffer(m_IndControl.Handle, class="num">0, class="num">0, class="num">1, Buff) == class="num">1) class="num">49. m_IndControl.Memory.dValue = Buff[class="num">0]; class="num">50. if ((C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus] != C_Controls::eTriState) class="num">51. if (bTest = ((m_IndControl.Mode = (C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus]) == C_Controls::ePlay))
「控制块与缓冲区的双向同步」
这段逻辑干的事很直接:指标控制结构体 m_IndControl 和一段共享内存 Memory 之间做状态互写,位置、模式、自定义字符都塞进定长数组里。 当位置一致时(第41行判断),先从指标句柄拷1根缓冲到 Buff,成功就把 Buff[0] 赋给 dValue(第43–44行);若状态不是三态且模式为播放,就把内存里的位置回写进控制体(第45–47行)。 位置不一致则反向同步:把当前 Position 和 Mode 写回 _16b 对应槽,并在 _8b[7]、_8b[6] 打上 'D' 'M' 标记(第50–52行),方便图表端识别这是指标侧发起的更新。 让小布替你跑这套 把第43行的 CopyBuffer 参数 0,0,1 改成 0,0,5,能在 MT5 里一次拉5根柱缓冲,验证 dValue 是否只取首根——外汇与贵金属波动快,这类共享内存读写若错位,回测和实盘信号可能倾向出现偏差,属高风险操作。
m_IndControl.Position = m_IndControl.Memory._16b[C_Controls::eCtrlPosition]; } }else { m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = m_IndControl.Position; m_IndControl.Memory._16b[C_Controls::eCtrlStatus] = (class="type">class="kw">ushort)m_IndControl.Mode; m_IndControl.Memory._8b[class="num">7] = &class="macro">#x27;D&class="macro">#x27;; m_IndControl.Memory._8b[class="num">6] = &class="macro">#x27;M&class="macro">#x27;; EventChartCustom(m_Infos.IdReplay, evCtrlReplayInit, class="num">0, m_IndControl.Memory.dValue, ""); bTest = false; } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">inline class="type">void UpdateIndicatorControl(class="type">void) { class="type">class="kw">double Buff[]; if (m_IndControl.Handle == INVALID_HANDLE) class="kw">return; if (m_IndControl.Memory._16b[C_Controls::eCtrlPosition] == m_IndControl.Position) { if (CopyBuffer(m_IndControl.Handle, class="num">0, class="num">0, class="num">1, Buff) == class="num">1) m_IndControl.Memory.dValue = Buff[class="num">0]; if ((C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus] != C_Controls::eTriState) if ((m_IndControl.Mode = (C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus]) == C_Controls::ePlay) m_IndControl.Position = m_IndControl.Memory._16b[C_Controls::eCtrlPosition]; }else { m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = m_IndControl.Position; m_IndControl.Memory._16b[C_Controls::eCtrlStatus] = (class="type">class="kw">ushort)m_IndControl.Mode; m_IndControl.Memory._8b[class="num">7] = &class="macro">#x27;D&class="macro">#x27;;