开发回放系统(第 64 部分):玩转服务(五)(基础篇)
📘

开发回放系统(第 64 部分):玩转服务(五)(基础篇)

第 1/3 篇

「在 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 测试,不预示实盘表现。

MQL5 / C++
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 是否只取首根——外汇与贵金属波动快,这类共享内存读写若错位,回测和实盘信号可能倾向出现偏差,属高风险操作。

MQL5 / C++
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;;

常见问题

大概率是服务误判了暂停条件,不是行情问题。先查控制块里的状态标记,别急着重启终端。
要在控制块和缓冲区之间做双向同步,写之前先读标记位。不同步就会丢帧或重复回放。
小布可以读取你的回放品种页状态,标出控制块异常和缓冲堆积点,你直接看提示就行。
检查服务初始化时的缓冲分配,默认块太小会频繁申请内存。调大初始缓冲能明显提速。
这是暂停期间缓冲没冻结导致的追帧。在控制块里加一个暂停即锁缓冲的逻辑就能解决。