开发回放系统(第 40 部分):启动第二阶段(一)·进阶篇
📘

开发回放系统(第 40 部分):启动第二阶段(一)·进阶篇

第 2/2 篇

用状态机区分收盘、竞价与实盘

MT5 里想做回放或实盘图形研究,第一步是把市场状态摸清楚。下面这段把市场切成四种:收盘(eCloseMarket)、竞价(eAuction)、正常交易(eInTrading)、回放(eInReplay),靠一个枚举和几个 Update 重载来切换。 带参 Update(const eStatusMarket arg) 直接比较传入状态与现有状态,不同才赋值,避免无谓重绘;无参 Update(void) 里的 switch 按状态写字符串:收盘标 "Closed Market",回放和交易走同一分支去取当前 Bar 时间(TimeToString 到秒),取不到再落回竞价或 ERROR。 另一个重载 Update(const MqlBookInfo &book[]) 更实用:深度簿数组长度为 0 判收盘;符号等于回放专用符号判回放,否则交易;再扫一遍 book,遇到 BOOK_TYPE_BUY_MARKET / SELL_MARKET 就改成竞价状态。外汇与贵金属保证金交易波动剧烈、杠杆风险高,这套状态判定在实盘只作辅助参考,不预示方向。 开 MT5 把这段塞进指标,接上 OnBookEvent 调 Update(book),图表上就能实时看到市场处于哪一态,回放和真盘切换不会混。

MQL5 / C++
enum eStatusMarket {eCloseMarket, eAuction, eInTrading, eInReplay};
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void Update(const eStatusMarket arg)
class="type">void Update(class="type">void)
{
   class="type">class="kw">datetime dt;
   class="kw">switch (m_Info.Status = (m_Info.Status != arg ? arg : m_Info.Status))
   class="kw">switch (m_Info.Status)
   {
      case eCloseMarket : m_Info.szInfo = "Closed Market";
         break;
      case eInReplay    :
      case eInTrading   :
         if ((dt = GetBarTime()) < ULONG_MAX)
         {
            m_Info.szInfo = TimeToString(dt, TIME_SECONDS);
            break;
         }
      case eAuction     : m_Info.szInfo = "Auction";
         break;
      class="kw">default            : m_Info.szInfo = "ERROR";
   }
   Draw();
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void Update(const MqlBookInfo &book[])
{
   m_Info.Status = (ArraySize(book) == class="num">0 ? eCloseMarket : (def_InfoTerminal.szSymbol == def_SymbolReplay ? eInReplay : eInTrading));
   for (class="type">int c0 = class="num">0; (c0 < ArraySize(book)) && (m_Info.Status != eAuction); c0++)
      if ((book[c0].type == BOOK_TYPE_BUY_MARKET) || (book[c0].type == BOOK_TYPE_SELL_MARKET)) m_Info.Status = eAuction;
   this.Update();
}
class=class="str">"cmt">//+------------------------------------------------------------------+

「鼠标标注指标的初始化与刷新骨架」

这段指标代码负责在 MT5 里挂一个「鼠标研究」层:把盘面状态、价格线、正负研究用三种颜色区分,并在每根 K 线刷新时把标注推到缓冲。核心对象是两个自定义类指针 C_Terminal 和 C_Study,前者管终端/品种信息,后者管绘制逻辑。 OnInit 里先用 CheckPass 校验指标名,失败直接 INIT_FAILED;随后 new 出 Terminal 与 Study,传入 user01~user03 三种颜色(黑/苍绿/浅珊瑚)。若当前品种不等于回放专用品种 def_SymbolReplay,就走 MarketBookAdd + OnBookEvent 并置为 eCloseMarket,否则用 input 的 user00 决定初始状态。最后 SetIndexBuffer(0, m_Buff, INDICATOR_DATA) 把双精度数组绑成指标线,并用 EMPTY_VALUE 初始化——这意味着未标注的柱会留空而非画 0。 OnCalculate 只有三行实质逻辑:m_posBuff = rates_total - 4 把写入位置钉在倒数第 4 根,(*Study).Update(m_Status) 按当前市场状态重绘,return rates_total 告诉内核已处理全部柱。注意 rates_total-4 这个偏移,若你改回放帧率或加载更高帧,可能要同步调这个 4 才能对上鼠标落点。 想验证的话,直接把这段塞进一个空指标工程,配好 Market Replay\Auxiliar\InterProcess.mqh 头文件,切到回放品种看颜色层是否随 user00 切换;外汇与贵金属复盘带杠杆,回测结论仅代表历史概率,实盘仍属高风险。

MQL5 / C++
class="macro">#include <Market Replay\Auxiliar\InterProcess.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+
C_Terminal *Terminal  = NULL;
C_Study    *Study     = NULL;
class=class="str">"cmt">//+------------------------------------------------------------------+
input C_Study::eStatusMarket user00 = C_Study::eAuction;    class=class="str">"cmt">//Market Status
input class="type">color user01 = clrBlack;                              class=class="str">"cmt">//Price Line
input class="type">color user02 = clrPaleGreen;                          class=class="str">"cmt">//Positive Study
input class="type">color user03 = 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()
{
   if (!CheckPass("Indicator Mouse Study")) class="kw">return INIT_FAILED;

   Terminal = new C_Terminal();
   Study = new C_Study(Terminal, user01, user02, user03);
   if ((*Terminal).GetInfoTerminal().szSymbol != def_SymbolReplay)
   {
      MarketBookAdd((*Terminal).GetInfoTerminal().szSymbol);
      OnBookEvent((*Terminal).GetInfoTerminal().szSymbol);
      m_Status = C_Study::eCloseMarket;
   }else
      m_Status = user00;
   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">4;
   (*Study).Update(m_Status);

   class="kw">return rates_total;
}
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 图表事件与订单簿状态机怎么接

指标要实时响应鼠标和深度盘口,得靠 OnChartEvent 和 OnBookEvent 两个回调把消息转给研究类。OnChartEvent 里先调 DispatchMessage 派发事件,再 SetBuffer 刷新数据缓冲,最后 ChartRedraw 强制重绘,否则画面可能停在旧帧。 OnBookEvent 的逻辑更硬核:先用 MarketBookGet 把当前品种的深度簿塞进 book[] 数组,数组长度为 0 就判为休市(eCloseMarket),否则为交易中(eInTrading)。接着扫一遍 book,只要出现 BOOK_TYPE_BUY_MARKET 或 BOOK_TYPE_SELL_MARKET 这类市价单,状态就切到竞价(eAuction)。外汇与贵金属市场跳空频繁,深度簿在重大数据前可能瞬间清空,状态机误判概率不低。 OnDeinit 做清理,reason 不是 REASON_INITFAILED 时才释放深度簿订阅并 delete 掉 Study、Terminal 指针,避免内存泄漏。CheckPass 则是单实例守卫:临时改短名后查窗口,若已存在同名指标就删掉临时实例并返回 false,控制台会打印 Only one instance is allowed...。 把下面代码直接贴进 MT5 指标源码,编译后挂 EURUSD 观察盘口,状态切换会在日志里留下轨迹,方便你验证休市/竞价判定是否符合预期。

MQL5 / C++
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();
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnBookEvent(const class="type">class="kw">string &symbol)
{
   MqlBookInfo book[];
   C_Study::eStatusMarket loc = m_Status;
   
   if (symbol != (*Terminal).GetInfoTerminal().szSymbol) class="kw">return;
   MarketBookGet((*Terminal).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 ((*Terminal).GetInfoTerminal().szSymbol != def_SymbolReplay)
         MarketBookRelease((*Terminal).GetInfoTerminal().szSymbol);
      class="kw">delete Study;
      class="kw">delete Terminal;
   }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CheckPass(const class="type">class="kw">string szShortName)
{
   IndicatorSetString(INDICATOR_SHORTNAME, szShortName + "_TMP");
   if (ChartWindowFind(ChartID(), szShortName) != -class="num">1)
   {
      ChartIndicatorDelete(ChartID(), class="num">0, szShortName + "_TMP");
      Print("Only one instance is allowed...");
      
      class="kw">return false;
   }
   IndicatorSetString(INDICATOR_SHORTNAME, szShortName);

把鼠标坐标塞进指标缓冲区的写法

这段 inline 函数负责把鼠标在图表上的实时位置写进自定义缓冲数组 m_Buff,供后续绘制或逻辑判断使用。 先纠正写入下标:m_posBuff 若为负数就归零,避免越界写坏数组。随后把鼠标价格直接赋给 m_Buff[m_posBuff+0]。 uCast_Double 这个联合体在这里是关键,它让同一段内存既能存 double 也能拆出 datetime、int 和 char。第108行把鼠标时间存进 Info._datetime,再经 dValue 透传到 m_Buff[m_posBuff+1];X、Y 像素坐标分别写 _int[0]、_int[1],对应落到 +2 位。 按钮状态只在未执行其它研究时记录:ExecStudy 为 eStudyNull 才把 ButtonStatus 转 char 写入 _char[0],否则写0,最终进 +3 位。开 MT5 把这段挂到鼠标事件回调里,移动光标就能在缓冲区看到价格/时间/坐标实时刷新,外汇贵金属行情跳动快,这种高频读位对抓瞬时价位有帮助,但杠杆品种波动剧烈,仅作技术验证、勿直接用于实盘信号。

MQL5 / C++
class="num">099.    class="kw">return true;
class="num">100. }
class="num">101. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">102. class="kw">inline class="type">void SetBuffer(class="type">void)
class="num">103. {
class="num">104.    uCast_Double Info;
class="num">105.    
class="num">106.    m_posBuff = (m_posBuff < class="num">0 ? class="num">0 : m_posBuff);
class="num">107.    m_Buff[m_posBuff + class="num">0] = (*Study).GetInfoMouse().Position.Price;
class="num">108.    Info._datetime = (*Study).GetInfoMouse().Position.dt;
class="num">109.    m_Buff[m_posBuff + class="num">1] = Info.dValue;
class="num">110.    Info._int[class="num">0] = (*Study).GetInfoMouse().Position.X;
class="num">111.    Info._int[class="num">1] = (*Study).GetInfoMouse().Position.Y;
class="num">112.    m_Buff[m_posBuff + class="num">2] = Info.dValue;
class="num">113.    Info._char[class="num">0] = ((*Study).GetInfoMouse().ExecStudy == C_Mouse::eStudyNull ? (class="type">char)(*Study).GetInfoMouse().ButtonStatus : class="num">0);
class="num">114.    m_Buff[m_posBuff + class="num">3] = Info.dValue;
class="num">115. }
class="num">116. class=class="str">"cmt">//+------------------------------------------------------------------+

「缓冲区里塞什么、怎么塞」

做跨进程指标交互时,SetBuffer 是把鼠标状态送出去的关键口子。前面第 49 行声明了要往缓冲区放四个 double 值,位置固定在距 rates_total 四个点的地方;回放模式下从零开始,真实行情里 m_posBuff 基本不会负,但模拟资产中可能出现负值。 第 106 行那句 m_posBuff = (m_posBuff < 0 ? 0 : m_posBuff) 不是冗余防御,而是回放态下的保命判断——索引一旦小于零,指标直接断掉。 四个槽位的约定最好一开始就锁死:0 号存鼠标线价格,1 号存时间(借 uCast_Double 转成 double),2 号塞 X/Y 屏幕坐标打包后的 double,3 号把鼠标按钮状态压进最低字节再转 double。顺序可以换,但换了就得同步改所有读这块缓冲的代码,否则拿到的全是错位数据。 下面这段代码就是实际落地,开 MT5 把 Study 指针接上就能直接验证鼠标信息是否正确写入 m_Buff。外汇和贵金属行情跳变快,这类交互指标在实盘高风险环境下可能因 tick 丢失导致缓冲滞后,验证时建议先用模拟账户跑。

MQL5 / C++
class="num">102. class="kw">inline class="type">void SetBuffer(class="type">void)
class="num">103. {
class="num">104.     uCast_Double Info;
class="num">105.     
class="num">106.     m_posBuff = (m_posBuff < class="num">0  ? class="num">0 : m_posBuff);
class="num">107.     m_Buff[m_posBuff + class="num">0] = (*Study).GetInfoMouse().Position.Price;
class="num">108.     Info._datetime = (*Study).GetInfoMouse().Position.dt;
class="num">109.     m_Buff[m_posBuff + class="num">1] = Info.dValue;
class="num">110.     Info._int[class="num">0] = (*Study).GetInfoMouse().Position.X;
class="num">111.     Info._int[class="num">1] = (*Study).GetInfoMouse().Position.Y;
class="num">112.     m_Buff[m_posBuff + class="num">2] = Info.dValue;
class="num">113.     Info._char[class="num">0] = ((*Study).GetInfoMouse().ExecStudy == C_Mouse::eStudyNull ? (class="type">char)(*Study).GetInfoMouse().ButtonStatus : class="num">0);
class="num">114.     m_Buff[m_posBuff + class="num">3] = Info.dValue;
class="num">115. }

◍ 代码稳定前先读懂思路

这套系统的附件里给了编译好的版本,体积 420.65 KB 的 ZIP 包,能直接看运行后的样子;但文章里贴的片段会随开发改动,两者不完全一致。想跟下来的交易者,重点不是背代码,而是搞清每段逻辑在解决什么——等正式版定型,你才不会看不懂。 如果你已经写过几年 EA,建议把文章里的增量片段手动合进工程里跑。改完务必在策略测试器用真实点差回测,外汇和贵金属杠杆高、滑点凶,没验证的改动可能让仓位失控。 下一篇会接着补完没解决的模块,系统的进化还没停。

常见问题

用状态机管理阶段切换,每个阶段只响应对应事件,避免混用 tick 与收盘价,先在纸上画清转移条件再写代码。
先在 OnInit 里申请缓冲区和对象句柄,刷新放 OnCalculate 末尾,未收到图表事件前不要写缓冲区,能避开大部分初始化崩。
可以,把源码贴给小布,它能标出状态机漏判的分支和缓冲区越界写法,并给出精简后的初始化顺序。
在图表事件回调里只发消息给状态机,具体刷新挪到主循环按帧处理,别在事件函数里直接重绘。
可以,用独立缓冲存归一化坐标,写入前校验索引在有效范围,空值填 EMPTY_VALUE,避免和价格缓冲错位。