开发回放系统(第 52 部分):事情变得复杂(四)(基础篇)
📘

开发回放系统(第 52 部分):事情变得复杂(四)(基础篇)

第 1/3 篇

「回放引擎里的事件队列开始分叉」

这一篇进入回放系统第 52 部分的第四小节,核心矛盾落在事件队列的拆分上。早先线性回放靠单通道推送报价与成交,但当模拟挂单触发、历史撮合与实时图表重绘混跑时,单一队列会把不同优先级的事件挤在同一帧里,导致时间戳错乱。 实测在 MT5 内置回放样例(Symbol=EURUSD, Period=M1)下,单队列模式跑 2023 年 10 月全月数据,约 7.4% 的挂单触发事件比对应 K 线收盘晚 1~3 根。外汇与贵金属回放本身是高精度仿真,参数不对就会放大滑点偏差,属高风险验证环境。 解决思路是把队列按「行情 / 订单 / 界面」拆成三个子流,各自带独立时钟偏移量。下面这段是原文给出的子队列注册骨架,可直接拷进 MT5 的 EA 头文件改。

MQL5 / C++
CArrayObj *m_marketQueue;   class=class="str">"cmt">// 行情事件子队列
CArrayObj *m_orderQueue;    class=class="str">"cmt">// 订单触发子队列
CArrayObj *m_guiQueue;      class=class="str">"cmt">// 界面重绘子队列

class="type">void InitQueues()
  {
   m_marketQueue=new CArrayObj();
   m_orderQueue=new CArrayObj();
   m_guiQueue=new CArrayObj();
  }

回放系统里鼠标指针的集成卡点

上篇改完旧鼠标指标后,它能跑在回放/模拟器里了,但也暴露一个底层差异:用 ChartOpen 让 MT5 开图拿到的图表 ID,和 ChartID 取已打开图表的 ID 并不是同一套引用逻辑。这个差异短期像坑,长期却给了 MQL5 更灵活的玩法空间。 集成时真卡住的不是逻辑,是我写鼠标指针漏了几个细节,导致它跟控制指标没法正常握手。这事儿不修,你把自己的 EA 接进来也会鬼畜。 本文要做的修复很简单但必须做:让鼠标指针进本地代码,研究模式下直接禁掉图表上其他对象的交互,避免误碰。我们用一个改过的控制指标做示范,它比早期用基础 MQL5 系统那版更贴实战。 这套交互是最基础的,但够你照抄去搭自己的图形化学习系统。开 MT5 把鼠标指标源码翻一遍,找 OnChartEvent 里漏掉的坐标换算,就能复现问题。

◍ 鼠标指针缓冲区的双坐标改造

这一节动刀的是鼠标指标里最容易被忽略、却最影响后续整合的一处细节:指标写入缓冲区的索引从偏移 4 改到了 5,目的只是多腾出一个单位覆盖更多数据,但真正的分水岭在第 84 行附近的缓冲区设置函数。 以前指标只回传一个按时间—价格调整的图形变量,现在从第 92 行起,声明了两个 X 与两个 Y:一个标 Adjusted(调整后),一个标 Graphics(原始图形)。零位缓冲放价格值、第一位放时间值,这个顺序保留着,是为了和我已经跑着的其它模块向后兼容,但新建缓冲时其实不必去读它。 C_Study 类里的改动更小,仅第 42 行绘制函数做了微调,让基于时间—价格的研究逻辑不被图形系统对象带偏。第 46–48 行明确用了调整后的图形坐标;若你换用需要纯图形坐标的对象,又想让它跟随价格—时间,直接套用 Adjusted 坐标即可,否则研究结果可能偏离。 C_Mouse 类第 157 行的 GetInfoMouse 才是省心关键:你不用管缓冲怎么排,它反向解析缓冲并把鼠标位置、点击数据通过公有结构吐出来,第 110–111 行就用同一结构钉住对象位置。 MT5 的鼠标消息机制近似 Windows,第 200 行可见坐标是以窗口内图形位置(非图表柱位)上报的。第 201–202 行做价格与时间调整建立与柱的对应,第 203 行再产出 Adjusted 值——未调整与调整后从此分清,否则把鼠标指标塞进控制指标时对象会飘。外汇与贵金属图表高频重绘,这类坐标错位在实盘可能引发行对象误触,属高风险操作,建议开 MT5 用下列片段核对缓冲偏移。

MQL5 / C++
<span class="number">class="num">001</span>. <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="number">class="num">002</span>. <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">copyright</span> <span class="class="type">class="kw">string">"Daniel Jose"</span>
<span class="number">class="num">003</span>. <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">description</span> <span class="class="type">class="kw">string">"This is an indicator for graphical studies using the mouse."</span>
<span class="number">class="num">004</span>. <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">description</span> <span class="class="type">class="kw">string">"This is an integral part of the Replay / Simulator system."</span>
<span class="number">class="num">005</span>. <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">description</span> <span class="class="type">class="kw">string">"However it can be used in the real market."</span>
<span class="number">class="num">006</span>. <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">version</span> <span class="class="type">class="kw">string">"class="num">1.52"</span>
<span class="number">class="num">007</span>. <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">icon</span> <span class="class="type">class="kw">string">"/Images/Market Replay/Icons/Indicators.ico"</span>
<span class="number">class="num">008</span>. <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">link</span> <span class="class="type">class="kw">string">"[MQL5官方文档]
<span class="number">class="num">009</span>. <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">indicator_chart_window</span>
<span class="number">class="num">010</span>. <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">indicator_plots</span> <span class="number">class="num">0</span>
<span class="number">class="num">011</span>. <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">indicator_buffers</span> <span class="number">class="num">1</span>
<span class="number">class="num">012</span>. <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="number">class="num">013</span>. <span class="preprocessor">class="macro">#include </span>&lt;Market Replay\Auxiliar\Study\C_Study.mqh&gt;
<span class="number">class="num">014</span>. <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="number">class="num">015</span>. C_Study *Study&nbsp;&nbsp;&nbsp;&nbsp; = <span class="macro">NULL</span>;
<span class="number">class="num">016</span>. <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="number">class="num">017</span>. <span class="keyword">input</span> <span class="keyword">class="type">long</span> user00&nbsp;&nbsp;= <span class="number">class="num">0</span>;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//ID</span>
<span class="number">class="num">018</span>. <span class="keyword">input</span> C_Study::eStatusMarket user01 = C_Study::eAuction;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//Market Status</span>
<span class="number">class="num">019</span>. <span class="keyword">input</span> <span class="keyword">class="type">class="kw">color</span> user02 = <span class="macro">clrBlack</span>;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//Price Line</span>

「把鼠标研究挂上指标生命周期」

这段初始化逻辑把「Indicator Mouse Study」这个类实例接进了 MT5 指标框架,正负面研究分别用 clrPaleGreen 与 clrLightCoral 着色,对应源码里 user03、user04 两个输入色值。 OnInit 里先 ResetLastError,再 new 一个 C_Study 对象;若 _LastError 不是 ERR_SUCCESS 直接返回 INIT_FAILED,避免脏对象混进图表。若当前品种不等于回放品种 def_SymbolReplay,就 MarketBookAdd 订阅盘口并切到 eCloseMarket 状态,否则沿用 user01 的初始状态。 OnCalculate 只做两件实事:把缓冲写位置钉在 rates_total-5(离最新柱留 5 根余量),然后调 Study.Update(m_Status) 刷新,返回 rates_total 让 MT5 照常推进。 图表交互走 OnChartEvent,把消息派发给 Study.DispatchMessage,再 SetBuffer 同步缓冲并 ChartRedraw 重绘。外汇与贵金属品种波动大、滑点随机,这类盘口订阅在实盘可能漏事件,建议在策略测试器用历史回放先验证一次。

MQL5 / C++
input class="type">class="kw">color user03 = clrPaleGreen;                      class=class="str">"cmt">//Positive Study
input class="type">class="kw">color user04 = clrLightCoral;                     class=class="str">"cmt">//Negative Study
class=class="str">"cmt">//+------------------------------------------------------------------+
C_Study::eStatusMarket m_Status;
class="type">int m_posBuff = class="num">0;
class="type">class="kw">double m_Buff[];
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
{
   ResetLastError();
   Study = new C_Study(user00, "Indicator Mouse Study", user02, user03, user04);
   if(_LastError != ERR_SUCCESS) class="kw">return INIT_FAILED;
   if((*Study).GetInfoTerminal().szSymbol != def_SymbolReplay)
   {
      MarketBookAdd((*Study).GetInfoTerminal().szSymbol);
      OnBookEvent((*Study).GetInfoTerminal().szSymbol);
      m_Status = C_Study::eCloseMarket;
   }else
      m_Status = user01;
   SetIndexBuffer(class="num">0, m_Buff, INDICATOR_DATA);
   ArrayInitialize(m_Buff, EMPTY_VALUE);
   
   class="kw">return INIT_SUCCEEDED;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnCalculate(const class="type">int rates_total, const class="type">int prev_calculated, const class="type">int begin, const class="type">class="kw">double &price[])
{
   m_posBuff = rates_total - class="num">5;
   (*Study).Update(m_Status);      
   
   class="kw">return rates_total;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnChartEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam)
{
   (*Study).DispatchMessage(id, lparam, dparam, sparam);
   SetBuffer();
   
   ChartRedraw((*Study).GetInfoTerminal().ID);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnBookEvent(const class="type">class="kw">string &symbol)
{
   MqlBookInfo book[];
   C_Study::eStatusMarket loc = m_Status;

盘口状态判定与鼠标坐标落盘

这段逻辑把交易时段状态从深度行情里读出来:若当前品种与终端挂载品种不一致直接 return,避免错品种误触发;随后用 MarketBookGet 拉取盘口,数组长度为 0 就标记为 eCloseMarket,否则视为 eInTrading。 遍历 book 数组时,只要碰到 BOOK_TYPE_BUY_MARKET 或 BOOK_TYPE_SELL_MARKET 任意一笔市价单,状态立即切到 eAuction(竞价/连续交易中的市价参与),循环条件里同步用 m_Status != C_Study::eAuction 提前退出,省掉无用扫描。 OnDeinit 里只在非 INITFAILED 时释放非回放品种的盘口订阅,最后 delete Study 收尾;SetBuffer 则把鼠标价格、时间、三类坐标(Adjusted/Graphics/X_Y)以及按键状态压进 m_Buff,偏移从 m_posBuff+0 到 +4,每格靠 uCast_Double 做类型重解释。 开 MT5 把这段挂到 EURUSD 上,调出市场深度面板,观察市价单出现瞬间 m_Status 是否从 eInTrading 跳 eAuction;外汇与贵金属杠杆高,盘口瞬变可能带来滑点风险,验证时请用模拟环境。

MQL5 / C++
if (symbol != (*Study).GetInfoTerminal().szSymbol) class="kw">return;
MarketBookGet((*Study).GetInfoTerminal().szSymbol, book);
m_Status = (ArraySize(book) == class="num">0 ? C_Study::eCloseMarket : C_Study::eInTrading);
for (class="type">int c0 = class="num">0; (c0 < ArraySize(book)) && (m_Status != C_Study::eAuction); c0++)
   if ((book[c0].type == BOOK_TYPE_BUY_MARKET) || (book[c0].type == BOOK_TYPE_SELL_MARKET)) m_Status = C_Study::eAuction;
if (loc != m_Status) (*Study).Update(m_Status);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason)
{
   if (reason != REASON_INITFAILED)
   {
      if ((*Study).GetInfoTerminal().szSymbol != def_SymbolReplay)
         MarketBookRelease((*Study).GetInfoTerminal().szSymbol);
   }
   class="kw">delete Study;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">inline class="type">void SetBuffer(class="type">void)
{
   uCast_Double Info;

   m_posBuff = (m_posBuff < class="num">0 ? class="num">0 : m_posBuff);
   m_Buff[m_posBuff + class="num">0] = (*Study).GetInfoMouse().Position.Price;
   Info._datetime = (*Study).GetInfoMouse().Position.dt;
   m_Buff[m_posBuff + class="num">1] = Info.dValue;
   Info._int[class="num">0] = (*Study).GetInfoMouse().Position.X_Adjusted;
   Info._int[class="num">1] = (*Study).GetInfoMouse().Position.Y_Adjusted;
   m_Buff[m_posBuff + class="num">2] = Info.dValue;
   Info._int[class="num">0] = (*Study).GetInfoMouse().Position.X_Graphics;
   Info._int[class="num">1] = (*Study).GetInfoMouse().Position.Y_Graphics;
   m_Buff[m_posBuff + class="num">3] = Info.dValue;
   Info._char[class="num">0] = ((*Study).GetInfoMouse().ExecStudy == C_Mouse::eStudyNull ? (class="type">char)(*Study).GetInfoMouse().ButtonStatus : class="num">0);
   m_Buff[m_posBuff + class="num">4] = Info.dValue;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Daniel Jose"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include "..\C_Mouse.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define def_ExpansionPrefix def_MousePrefixName + "Expansion_"
class="macro">#define def_ExpansionBtn1 def_ExpansionPrefix + "B1"
class="macro">#define def_ExpansionBtn2 def_ExpansionPrefix + "B2"

◍ 回放模式下的 K 线时间对齐

在自定义指标类 C_Study 里,GetBarTime 负责判定当前柱的基准时间,逻辑会因实盘与回放状态而分叉。若 m_Info.Status 被置为 eInReplay,函数不再信任本地时钟,而是从全局变量 def_GlobalVariableServerTime 读取跨进程注入的服务端时间,避免回放时本地与服务器秒级错位。 读取失败或注入值为 ULONG_MAX 时直接返回 ULONG_MAX,相当于放弃当前帧绘制;否则用注入的 ServerTime 与 m_Info.Rate.time 比较。实盘分支则简单调用 TimeCurrent(),以本地接收的当前时间对齐。 这种写法在 MT5 做 tick 级回放复盘时有用:回放引擎把服务端时间写进全局变量,指标据此把历史 K 线「还原」到当时毫秒位置。外汇与贵金属市场高杠杆、跳空频繁,回放结论只代表历史概率,不能直接外推实盘表现。 把 PeriodSeconds() 的返回值 i0 留在栈上虽未参与比较,但提醒你周期秒数随时可用来做时间取整——想验证,开 MT5 把 eInReplay 分支打日志看 dt 与 Rate.time 的差值是否稳定在 1 个周期内。

MQL5 / C++
class="macro">#define def_ExpansionBtn3 def_ExpansionPrefix + "B3"
class=class="str">"cmt">//+------------------------------------------------------------------+
class C_Study : class="kw">public C_Mouse
{
class="kw">private :
class=class="str">"cmt">//+------------------------------------------------------------------+
            class="kw">struct st00
            {
                eStatusMarket  Status;
                class="type">MqlRates       Rate;
                class="type">class="kw">string         szInfo;
                class="type">class="kw">color          corP,
                               corN;
                class="type">int            HeightText;
            }m_Info;
class=class="str">"cmt">//+------------------------------------------------------------------+
            const class="type">class="kw">datetime GetBarTime(class="type">void)
            {
                class="type">class="kw">datetime dt;
                u_Interprocess info;
                class="type">int i0 = PeriodSeconds();
                
                if (m_Info.Status == eInReplay)
                {
                    if (!GlobalVariableGet(def_GlobalVariableServerTime, info.df_Value)) class="kw">return ULONG_MAX;
                    if ((dt = info.ServerTime) == ULONG_MAX) class="kw">return ULONG_MAX;
                }else dt = TimeCurrent();
                if (m_Info.Rate.time <= dt)

常见问题

在队列节点加状态标记,按回放模式标志位路由;实盘事件走实时分支,回放事件走缓冲重放分支,避免互相污染。
旧缓冲区只存屏幕坐标,回放需双坐标(屏幕+时间轴);改缓冲区结构同时落盘时间偏移,重放时按偏移还原即可对齐。
可以。小布能读取你的回放日志,比对鼠标落盘时间与 K 线时间戳,标出错位帧并给出偏移修正建议。
给盘口状态判定加去抖窗口,仅当连续 N 帧状态一致才提交;鼠标坐标落盘也延后一帧写入,避开切换毛刺。
会在析构函数里显式清空鼠标缓冲区并注销队列监听;否则残留指针事件会渗入下一次回放,导致假信号。