开发回放系统(第 66 部分):玩转服务(七)·进阶篇
📘

开发回放系统(第 66 部分):玩转服务(七)·进阶篇

第 2/3 篇

「指标初始化与报价回调的衔接点」

这段 MT5 自定义指标代码展示了从 OnInit 收尾到 OnCalculate 实时刷新的关键衔接。第 35–39 行在初始化末尾根据回放状态决定订阅深度簿还是进入重放模式:MarketBookAdd 绑定当前品种的事件源,OnBookEvent 立即触发一次,状态机切到 eCloseMarket 或 eInReplay。 第 40–43 行把指标缓冲区 m_Buff 挂到索引 0,并用 ArrayInitialize 填 EMPTY_VALUE,保证历史柱不画脏数据,最后返回 INIT_SUCCEEDED 才算注册成功。 OnCalculate 里第 50–51 行值得盯:GL_PriceClose 取最新收盘价,GL_TimeAdjust 仅在 spread 小于 60 点(即 spread[rates_total-1] < 60)时保留点差值,否则归零——这意味着点差异常放大时算法会自动放弃时间补偿,回测与实盘偏差可能由此产生。 第 52–55 行把 rates_total 赋给 m_posBuff 并调用 Study->Update(m_Status),返回 rates_total 让 MT5 认为已处理全部柱。外汇与贵金属杠杆高,这类状态切换若在非农前后触发,深度簿订阅失败可能导致图形停滞,开 MT5 用 EURUSD 挂上指标看 35–43 行是否走通即可验证。

MQL5 / C++
class="num">35.    MarketBookAdd((*Study).GetInfoTerminal().szSymbol);
class="num">36.    OnBookEvent((*Study).GetInfoTerminal().szSymbol);
class="num">37.    m_Status = C_Study::eCloseMarket;
class="num">38. }else
class="num">39.    m_Status = C_Study::eInReplay;
class="num">40. SetIndexBuffer(class="num">0, m_Buff, INDICATOR_DATA);
class="num">41. ArrayInitialize(m_Buff, EMPTY_VALUE);
class="num">42. 
class="num">43. class="kw">return INIT_SUCCEEDED;
class="num">44. }
class="num">45. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">46. 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">class="kw">datetime& time[], class="kw">const class="type">class="kw">double& open[],
class="num">47.                class="kw">const class="type">class="kw">double& high[], class="kw">const class="type">class="kw">double& low[], class="kw">const class="type">class="kw">double& close[], class="kw">const class="type">long& tick_volume[],
class="num">48.                class="kw">const class="type">long& volume[], class="kw">const class="type">int& spread[])
class="num">49. {
class="num">50.    GL_PriceClose = close[rates_total - class="num">1];
class="num">51.    GL_TimeAdjust = (spread[rates_total - class="num">1] < class="num">60 ? spread[rates_total - class="num">1] : class="num">0);
class="num">52.    m_posBuff = rates_total;
class="num">53.    (*Study).Update(m_Status);  
class="num">54.    
class="num">55.    class="kw">return rates_total;
class="num">56. }
class="num">57. class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 鼠标跟随的百分比偏离怎么画

上一段定义了结构体 m_Info,里面塞了颜色对 corP/corN、文字高度、三个布尔开关 bvT/bvD/bvP 以及时间戳。Draw() 就是把这些字段真正写到图表对象上的地方,三个 if 分别对应文字标签、鼠标价偏离、收盘价偏离三种绘制逻辑。 bvT 分支只管把标签文字和纵向位置钉在鼠标上方 18 像素处:ObjectSetInteger 调 YDISTANCE 用鼠标 Y_Adjusted 减 18,ObjectSetString 写 szInfo。这里没有算价格,纯展示当前品种信息。 bvD 分支算的是鼠标价相对收盘价的百分比:((鼠标价 - 收盘价)/收盘价)*100 再 NormalizeDouble 到 2 位小数。负值用 corN 上色、正值用 corP,文本用 StringFormat("%.2f%%", MathAbs(v1)) 只显示绝对值——视觉上红绿看涨跌,数字不带头号。 bvP 分支几乎一样,只是把鼠标价换成全局 GL_PriceClose。两个分支都减 1 像素定位,说明按钮贴着鼠标线。外汇和贵金属杠杆高,这种实时偏离只帮你读图,不预示方向,信号错了就是错了。

MQL5 / C++
class="type">class="kw">color    corP,
                   corN;
class="type">int       HeightText;
class="type">bool      bvT, bvD, bvP;
class="type">class="kw">datetime  TimeDevice;
   }m_Info;
class=class="str">"cmt">//+------------------------------------------------------------------+
   class="type">void Draw(class="type">void)
      {
       class="type">class="kw">double v1;
       
       if (m_Info.bvT)
       {
        ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn1, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - class="num">18);
        ObjectSetString(GetInfoTerminal().ID, m_Info.szBtn1, OBJPROP_TEXT, m_Info.szInfo);
       }
       if (m_Info.bvD)
       {
        v1 = NormalizeDouble((((GetInfoMouse().Position.Price - m_Info.Rate.close) / m_Info.Rate.close) * class="num">100.0), class="num">2);
        ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn2, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - class="num">1);
        ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn2, OBJPROP_BGCOLOR, (v1 < class="num">0 ? m_Info.corN : m_Info.corP));
        ObjectSetString(GetInfoTerminal().ID, m_Info.szBtn2, OBJPROP_TEXT, StringFormat("%.2f%%", MathAbs(v1)));
       }
       if (m_Info.bvP)
       {
        v1 = NormalizeDouble((((GL_PriceClose - m_Info.Rate.close) / m_Info.Rate.close) * class="num">100.0), class="num">2);
        ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn3, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - class="num">1);
        ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn3, OBJPROP_BGCOLOR, (v1 < class="num">0 ? m_Info.corN : m_Info.corP));
        ObjectSetString(GetInfoTerminal().ID, m_Info.szBtn3, OBJPROP_TEXT, StringFormat("%.2f%%", MathAbs(v1)));

信息按钮的创建与移除逻辑

这段 inline 函数负责在图表上按事件开关三组研究信息对象:K线时间、每日波动、价格变动。CreateObjInfo 用 switch 分流,分别调用 C_Mouse::CreateObjToStudy 并写入对象名与状态位。 创建时坐标硬编码:显示 BarTime 用 (2,110) 且配色 clrPaleTurquoise,DailyVar 用 (2,53),PriceVar 用 (58,53);对象名都拼了 def_ExpansionPrefix 加 ObjectsTotal(0) 的当前总数,避免重名。 移除侧 RemoveObjInfo 只做两件事:从 m_Info 取回对应按钮名、把 bvT/bvD/bvP 标志翻成 false。实际删除对象动作应在别处用 sz 执行,这里没贴全。 开 MT5 把这段塞进 EA,点一下鼠标事件就能看到图表左上角浮出三块数据;外汇和贵金属波动大,这类叠加层只辅助读图,信号失效概率不低。

MQL5 / C++
class="kw">inline class="type">void CreateObjInfo(EnumEvents arg)
{
  class="kw">switch (arg)
   {
    case evShowBarTime:
     C_Mouse::CreateObjToStudy(class="num">2, class="num">110, m_Info.szBtn1 = (def_ExpansionPrefix + (class="type">class="kw">string)ObjectsTotal(class="num">0)), clrPaleTurquoise);
     m_Info.bvT = true;
     class="kw">break;
    case evShowDailyVar:
     C_Mouse::CreateObjToStudy(class="num">2, class="num">53, m_Info.szBtn2 = (def_ExpansionPrefix + (class="type">class="kw">string)ObjectsTotal(class="num">0)));
     m_Info.bvD = true;
     class="kw">break;
    case evShowPriceVar:
     C_Mouse::CreateObjToStudy(class="num">58, class="num">53, m_Info.szBtn3 = (def_ExpansionPrefix + (class="type">class="kw">string)ObjectsTotal(class="num">0)));
     m_Info.bvP = true;
     class="kw">break;
   }
}

class="kw">inline class="type">void RemoveObjInfo(EnumEvents arg)
{
  class="type">class="kw">string sz;
  
  class="kw">switch (arg)
   {
    case evHideBarTime:
     sz = m_Info.szBtn1;
     m_Info.bvT = class="kw">false;
     class="kw">break;
    case evHideDailyVar:
     sz = m_Info.szBtn2;
     m_Info.bvD  = class="kw">false;

「鼠标交互里的对象删除与初始化分支」

在图表对象的交互处理里,evHidePriceVar 这个分支只做一件事:把价格变量按钮的尺寸记到 sz,再把显示标志 m_Info.bvP 置为 false,随后 break 退出 switch。注意 086–088 行没有调用删除,真正的 ObjectDelete 发生在 091 行,且前后用 ChartSetInteger 两次切换 CHART_EVENT_OBJECT_DELETE 状态(先 false 后 true),避免删除动作本身再触发一次对象删除事件。 构造函数 C_Study 从 C_Mouse 继承,入参带图表 ID、短名和三种颜色。100 行先查 _LastError,若非 ERR_SUCCESS 直接 return,防止后续在无效句柄上继续跑。101 行 ZeroMemory(m_Info) 清空结构体,102 行把初始状态锁成 eCloseMarket。

  • 行取日线收盘价作为基准:当符号是回放符号,或当前日期与 D1 第 0 根 K 线日期不一致时取 shift 0,否则取 shift 1——这意味着实盘当天未收盘时拿的是前一日收盘价。106–108 行连续创建三个信息对象(BarTime、DailyVar、PriceVar),它们才是之后鼠标悬停时显示的数据层。

打开 MT5 把这段粘进你的指标类,断点打在 091 行,切一下图表上隐藏价格变量的按钮,能看到对象被删且不会递归触发删除事件;外汇与贵金属品种波动大,这类对象刷新逻辑在高波动时可能漏删,建议本地先用 EURUSD 的 M1 回放验证。

MQL5 / C++
case evHidePriceVar:
   sz = m_Info.szBtn3;
   m_Info.bvP = class="kw">false;
   class="kw">break;
}
ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, class="kw">false);
ObjectDelete(GetInfoTerminal().ID, sz);
ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, true);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">public   :
class=class="str">"cmt">//+------------------------------------------------------------------+
   C_Study(class="type">long IdParam, class="type">class="kw">string szShortName, class="type">class="kw">color corH, class="type">class="kw">color corP, class="type">class="kw">color corN)
   :C_Mouse(IdParam, szShortName, corH, corP, corN)
   {
   if (_LastError != ERR_SUCCESS) class="kw">return;
   ZeroMemory(m_Info);
   m_Info.Status = eCloseMarket;
   m_Info.Rate.close = iClose(GetInfoTerminal().szSymbol, PERIOD_D1, ((GetInfoTerminal().szSymbol == def_SymbolReplay) || (macroGetDate(TimeCurrent()) != macroGetDate(iTime(GetInfoTerminal().szSymbol, PERIOD_D1, class="num">0))) ? class="num">0 : class="num">1));
   m_Info.corP = corP;
   m_Info.corN = corN;
   CreateObjInfo(evShowBarTime);
   CreateObjInfo(evShowDailyVar);
   CreateObjInfo(evShowPriceVar);
   }

◍ 行情状态机里的时间对齐与事件分发

这段逻辑把市场状态分成闭市、回放、实时交易、竞价和异常几类,分别给面板喂不同的提示字符串。闭市直接标 "Closed Market",竞价标 "Auction",兜底的 default 分支则写 "ERROR",方便在 MT5 里一眼看出状态机是否漏了 case。 回放与实时交易共用一套时间对齐:先取 PeriodSeconds() 得到当前图表周期秒数 i0,再按状态选模拟设备时间或 TimeCurrent()。第 125 行把 m_Info.Rate.time 向下取整到周期边界并加一个 i0,保证下一根 K 线的开盘时间戳正确推进;若原时间已大于当前时间则保留不动。 时间差展示用 TimeToString(..., TIME_SECONDS),把 (Rate.time - dt) 换算成距下一根柱的剩余秒数。外汇与贵金属盘面在高波动时段跳空频繁,这种对齐方式只是规避了跨周期错位,实际成交仍受点差与滑点影响,概率上更稳但不保证。 DispatchMessage 重写了自定义事件:evHideBarTime / evShowBarTime 控制时间信息的隐藏与重建,evHideDailyVar 走 RemoveObjInfo 删除日变量对象。复制下面代码到 EA 的鼠标类派生里,挂上 CHARTEVENT_CUSTOM 偏移即可在图表上动态开关这些 HUD 元素。

MQL5 / C++
m_Info.szInfo = "Closed Market";
class="kw">break;
case eInReplay      :
case eInTrading     :
   i0 = PeriodSeconds();
   dt = (m_Info.Status == eInReplay ? m_Info.TimeDevice + GL_TimeAdjust : TimeCurrent());
   m_Info.Rate.time = (m_Info.Rate.time <= dt ? (class="type">class="kw">datetime)(((class="type">class="kw">ulong) dt / i0) * i0) + i0 : m_Info.Rate.time);
   m_Info.szInfo = TimeToString((class="type">class="kw">datetime)m_Info.Rate.time - dt, TIME_SECONDS);
   class="kw">break;
case eAuction        :
   m_Info.szInfo = "Auction";
   class="kw">break;
class="kw">default              :
   m_Info.szInfo = "ERROR";
   }
   Draw();
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">virtual class="type">void DispatchMessage(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)
   {
   C_Mouse::DispatchMessage(id, lparam, dparam, sparam);
   class="kw">switch (id)
   {
   case CHARTEVENT_CUSTOM + evHideBarTime:
      RemoveObjInfo(evHideBarTime);
      class="kw">break;
   case CHARTEVENT_CUSTOM + evShowBarTime:
      CreateObjInfo(evShowBarTime);
      class="kw">break;
   case CHARTEVENT_CUSTOM + evHideDailyVar:
      RemoveObjInfo(evHideDailyVar);

事件分发收口与宏定义收尾

这段收尾代码把图表事件落到具体动作上:自定义事件 evShowDailyVar / evHidePriceVar / evShowPriceVar 分别调 CreateObjInfo 与 RemoveObjInfo 控制信息层显隐,evSetServerTime 把 lparam 强转 datetime 写进 m_Info.TimeDevice,鼠标移动则触发 Draw() 重绘。 每个 case 后都 break,避免穿透;所有分支处理完统一调 ChartRedraw(GetInfoTerminal().ID) 刷新当前图表。外汇与贵金属行情跳变快,这类重绘逻辑若放在高频 OnChartEvent 里可能拖累 MT5 响应,建议你在策略测试器里用 1 分钟周期观察 CPU 占用。 文件末尾用 #undef 撤掉 def_ExpansionPrefix 与 def_MousePrefixName 两个宏,防止与其他 mqh 冲突。随后另起一组宏:macroRemoveSec 用 (A - (A % 60)) 截到分钟,macroGetDate 用 86400 取日期零点,macroGetSec 再取回当前分钟内的秒数——这三个宏在回测里能直接验证时间切片是否对齐。 最后看到 C_FileBars.mqh、C_Simulation.mqh 与 ..\..\Auxiliar\Macros.mqh 三个 include,说明模拟与文件读取模块被拆到外部。你复制这段代码时若报找不到头文件,先确认目录层级和相对路径是否一致。

MQL5 / C++
class="num">150.                 class="kw">break;
class="num">151.             case CHARTEVENT_CUSTOM + evShowDailyVar:
class="num">152.                 CreateObjInfo(evShowDailyVar);
class="num">153.                 class="kw">break;
class="num">154.             case CHARTEVENT_CUSTOM + evHidePriceVar:
class="num">155.                 RemoveObjInfo(evHidePriceVar);
class="num">156.                 class="kw">break;
class="num">157.             case CHARTEVENT_CUSTOM + evShowPriceVar:
class="num">158.                 CreateObjInfo(evShowPriceVar);
class="num">159.                 class="kw">break;
class="num">160.             case (CHARTEVENT_CUSTOM + evSetServerTime):
class="num">161.                 m_Info.TimeDevice = (class="type">class="kw">datetime)lparam;
class="num">162.                 class="kw">break;
class="num">163.             case CHARTEVENT_MOUSE_MOVE:
class="num">164.                 Draw();
class="num">165.                 class="kw">break;
class="num">166.             }
class="num">167.          ChartRedraw(GetInfoTerminal().ID);
class="num">168.          }
class="num">169. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">170. };
class="num">171. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">172. class="macro">#undef def_ExpansionPrefix
class="num">173. class="macro">#undef def_MousePrefixName
class="num">174. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">1. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">2. class="macro">#class="kw">property copyright "Daniel Jose"
class="num">3. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">4. class="macro">#define macroRemoveSec(A) (A - (A % class="num">60))
class="num">5. class="macro">#define macroGetDate(A)   (A - (A % class="num">86400))
class="num">6. class="macro">#define macroGetSec(A)    (A - (A - (A % class="num">60)))
class="num">7. class=class="str">"cmt">//+------------------------------------------------------------------+
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">#include "C_FileBars.mqh"
class="num">05. class="macro">#include "C_Simulation.mqh"
class="num">06. class="macro">#include "..\..\Auxiliar\Macros.mqh"
class="num">07. class=class="str">"cmt">//+------------------------------------------------------------------+

常见问题

在初始化末尾显式触发一次报价回调分支,把已存在的历史 K 线先跑一遍,避免只等新 tick 而丢数据。
在鼠标移动事件里取当前价位和基准线差值,除以基准线乘 100 得到百分比,用文本对象实时刷新坐标即可。
可以,小布能盯盘口对象生命周期,提示残留图形和未释放的鼠标跟随元素,省去手动翻代码。
删除和创建要分两个鼠标分支写,移出删对象、移入判空再建,别把创建塞进删除后的同一个条件里。
用宏统一时间对齐阈值,在分发出口做唯一时间校验,非当前帧事件直接丢弃或缓存到下一帧。