DoEasy 函数库中的价格(第六十四部分):市场深度,DOM 快照类和快照序列对象·综合运用
📘

DoEasy 函数库中的价格(第六十四部分):市场深度,DOM 快照类和快照序列对象·综合运用

第 3/3 篇

「DOM 快照对象的接口与取数入口」

在 MT5 的订单簿深度(DOM)分析类里,CMBookSnapshot 把整张盘口快照封装成对象,外部只需拿指针就能遍历。GetObject() 返回自身地址,GetList() 返回内部 m_list 这个 CArrayObj 容器,意味着盘口挂单是以对象数组形式驻留内存的。 真正好用的是三组重载 GetList:分别按 double / long / string 类型的 ENUM_MBOOK_ORD_PROP_* 属性做条件筛选,默认比较模式是 EQUAL,也可传 ENUM_COMPARER_TYPE 改大于、小于等。比如按某档价格或成交量挑单,不用自己写循环。 GetMBookByListIndex(uint) 按数组下标直接取 CMarketBookOrd 指针,DataTotal() 返回当前列表长度(int)。这两个配合,就能在 EA 里用 for(int i=0;i<snap.DataTotal();i++) 扫全档。 Compare() 被 virtual 修饰,逻辑只有一行:拿 this.Time() 和传入节点的 Time() 比,早返回 -1、晚返回 1、同刻返回 0。这说明快照对象本身可被排序,按时间轴排 DOM 变化很直接。外汇与贵金属盘口受流动性冲击大,这类接口只反映当时簿记,实操高风险。

MQL5 / C++
CMBookSnapshot      *GetObject(class="type">void)                                                  { class="kw">return &this;  }
CArrayObj           *GetList(class="type">void)                                                  { class="kw">return &m_list; }
class=class="str">"cmt">//--- Return the list of DOM order objects by selected(class="num">1) class="type">class="kw">double, (class="num">2) integer and(class="num">3) class="type">class="kw">string class="kw">property satisfying the compared condition
CArrayObj           *GetList(ENUM_MBOOK_ORD_PROP_DOUBLE class="kw">property,class="type">class="kw">double value,ENUM_COMPARER_TYPE mode=EQUAL){ class="kw">return CSelect::ByMBookProperty(this.GetList(),class="kw">property,value,mode); }
CArrayObj           *GetList(ENUM_MBOOK_ORD_PROP_INTEGER class="kw">property,class="type">long value,ENUM_COMPARER_TYPE mode=EQUAL) { class="kw">return CSelect::ByMBookProperty(this.GetList(),class="kw">property,value,mode); }
CArrayObj           *GetList(ENUM_MBOOK_ORD_PROP_STRING class="kw">property,class="type">class="kw">string value,ENUM_COMPARER_TYPE mode=EQUAL){ class="kw">return CSelect::ByMBookProperty(this.GetList(),class="kw">property,value,mode); }
class=class="str">"cmt">//--- (class="num">1) Return the DOM order object by index in the list and(class="num">2) the order list size
CMarketBookOrd   *GetMBookByListIndex(class="kw">const class="type">uint index)                   { class="kw">return this.m_list.At(index);  }
class="type">int                DataTotal(class="type">void)                                      class="kw">const { class="kw">return this.m_list.Total();     }
class=class="str">"cmt">//--- The comparison method for searching and sorting DOM snapshot objects by time
class="kw">virtual class="type">int        Compare(class="kw">const CObject *node,class="kw">const class="type">int mode=class="num">0) class="kw">const
                   {
                     class="kw">const CMBookSnapshot *compared_obj=node;
                     class="kw">return(this.Time()<compared_obj.Time() ? -class="num">1 : this.Time()>compared_obj.Time() ? class="num">1 : class="num">0);
                   }
class=class="str">"cmt">//--- Return the DOM snapshot change
class="type">class="kw">string             Header(class="type">void);
class=class="str">"cmt">//--- Display(class="num">1) description and(class="num">2) class="type">short description of a DOM snapshot
class="type">void               Print(class="type">void);
class="type">void               PrintShort(class="type">void);
class=class="str">"cmt">//--- Constructors
                   CMBookSnapshot(){;}
                   CMBookSnapshot(class="kw">const class="type">class="kw">string symbol,class="kw">const class="type">long time,MqlBookInfo &book_array[]);

DOM 快照对象的构造与订单归类

在 MT5 里抓取市场深度(DOM),第一步是把 MqlBookInfo 结构数组固化成一个快照对象。下面这段构造函数做了三件事:绑定品种、清空旧列表、按类型把每一笔挂单封装成对应的订单类实例。 构造时若传入的 symbol 为空串或 NULL,SetSymbol 会回退到当前图表品种 ::Symbol(),这点在多品种 EA 里容易踩坑——你以为挂在 XAUUSD 上,实际可能吞了 EURUSD 的盘口。 循环里用 ArraySize(book_array) 拿总数,再按 book_array[i].type 分流:BOOK_TYPE_BUY / SELL 是限价挂单,BOOK_TYPE_BUY_MARKET / SELL_MARKET 是市价排队单。任一 new 失败返回 NULL 就直接 continue,不阻断其余订单入库。 外汇与贵金属 DOM 流动性瞬变,基于快照做决策属高风险行为,价格行为可能快速反转。

MQL5 / C++
  class="type">void            SetSymbol(class="kw">const class="type">class="kw">string symbol)    { this.m_symbol=(symbol==NULL || symbol=="" ? ::Symbol() : symbol); }
  class="type">void            SetTime(class="kw">const class="type">long time_msc)      { this.m_time=time_msc; }
  class="type">void            SetTimeToOrders(class="kw">const class="type">long time_msc);
class=class="str">"cmt">//--- Return(class="num">1) a DOM symbol, (class="num">2) symbol&class="macro">#x27;s Digits and(class="num">3) a snapshot time
  class="type">class="kw">string          Symbol(class="type">void)                   class="kw">const { class="kw">return this.m_symbol; }
  class="type">int             Digits(class="type">void)                   class="kw">const { class="kw">return this.m_digits; }
  class="type">long            Time(class="type">void)                     class="kw">const { class="kw">return this.m_time;  }
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Parametric constructor                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
CMBookSnapshot::CMBookSnapshot(class="kw">const class="type">class="kw">string symbol,class="kw">const class="type">long time,MqlBookInfo &book_array[]) : m_time(time)
  {
  class=class="str">"cmt">//--- Set a symbol
   this.SetSymbol(symbol);
  class=class="str">"cmt">//--- Clear the list
   this.m_list.Clear();
  class=class="str">"cmt">//--- In the loop by the structure array
   class="type">int total=::ArraySize(book_array);
   for(class="type">int i=class="num">0;i<total;i++)
    {
     class=class="str">"cmt">//--- Create order objects of the current DOM snapshot depending on the order type
     CMarketBookOrd *mbook_ord=NULL;
     class="kw">switch(book_array[i].type)
      {
       case BOOK_TYPE_BUY         : mbook_ord=new CMarketBookBuy(this.m_symbol,book_array[i]);         class="kw">break;
       case BOOK_TYPE_SELL        : mbook_ord=new CMarketBookSell(this.m_symbol,book_array[i]);        class="kw">break;
       case BOOK_TYPE_BUY_MARKET  : mbook_ord=new CMarketBookBuyMarket(this.m_symbol,book_array[i]);   class="kw">break;
       case BOOK_TYPE_SELL_MARKET : mbook_ord=new CMarketBookSellMarket(this.m_symbol,book_array[i]);  class="kw">break;
       class="kw">default: class="kw">break;
      }
     if(mbook_ord==NULL)
      class="kw">continue;
     class=class="str">"cmt">//--- Set the DOM snapshot time for the order

◍ DOM 快照对象的打印与时间同步

CMBookSnapshot 类把某一时刻的订单簿状态封装成对象,Header() 直接返回带品种名的短标签,例如对 EURUSD 会输出 "EURUSD" DOM snapshot,方便在日志里快速定位。PrintShort() 则在此基础上附上毫秒级时间戳,实测一条记录形如 "EURUSD" DOM snapshot (2021.02.09 22:16:24.557),这种粒度对复盘微秒级价差跳动有用。 Print() 方法先按价格重排链表,再从高价向低价倒序遍历输出每一档的订单头信息;若某节点取空则跳过,避免脏数据中断打印。SetTimeToOrders() 则把外部传入的 time_msc 批量写回链表内所有 CMarketBookOrd 实例,保证整张快照的时间基准一致。 开 MT5 自建一个 CMBookSnapshot 实例,挂 EURUSD 的 OnBookEvent 里调用 PrintShort(),你能直接看到盘口快照写入journal的准确延迟,外汇与贵金属杠杆高,盘口重构错误可能放大滑点风险。

MQL5 / C++
   mbook_ord.SetTime(this.m_time);
   class=class="str">"cmt">//--- Set the sorted list flag for the list(by the price value) and add the current order object to it
   this.m_list.Sort(SORT_BY_MBOOK_ORD_PRICE);
   if(!this.m_list.InsertSort(mbook_ord))
      class="kw">delete mbook_ord;
   }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the object class="type">short name                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string CMBookSnapshot::Header(class="type">void)
  {
   class="kw">return CMessage::Text(MSG_MBOOK_SNAP_TEXT_SNAPSHOT)+" \""+this.Symbol();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
EURUSD DOM snapshot
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Display a class="type">short description of the object in the journal          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CMBookSnapshot::PrintShort(class="type">void)
  {
   ::Print(this.Header()," ("+TimeMSCtoString(this.m_time),")");
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
"EURUSD" DOM snapshot(class="num">2021.02.class="num">09 class="num">22:class="num">16:class="num">24.557)
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Display object properties in the journal                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CMBookSnapshot::Print(class="type">void)
  {
   ::Print(this.Header()," ("+TimeMSCtoString(this.m_time),"):");
   this.m_list.Sort(SORT_BY_MBOOK_ORD_PRICE);
   for(class="type">int i=this.m_list.Total()-class="num">1;i>WRONG_VALUE;i--)
     {
      CMarketBookOrd *ord=this.m_list.At(i);
      if(ord==NULL)
         class="kw">continue;
      ::Print(" - ",ord.Header());
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Set the specified time to all DOM orders                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CMBookSnapshot::SetTimeToOrders(class="kw">const class="type">long time_msc)
  {
   class="type">int total=this.m_list.Total();
   for(class="type">int i=class="num">0;i<total;i++)
     {
      CMarketBookOrd *ord=this.m_list.At(i);
      if(ord==NULL)
         class="kw">continue;
      ord.SetTime(time_msc);
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

「DOM 快照序列类的构造与更新逻辑」

DOM 快照序列类在思路上和时间序列、即时报价类接近,但有一个硬限制:它拿不到历史 DOM 数据,只能从实时事件里一点点累积。所以这类不提供列表创建方法,只留更新入口,运行时靠 OnBookEvent() 触发补数据。 在 \MQL5\Include\DoEasy\Objects\Book\ 下新建 MBookSeries.mqh,基类用 CBaseObj,并 include 之前的 MarketBookSnapshot.mqh。类里私有成员很直白:m_symbol 存品种,m_amount 是序列里实际用的快照数,m_required 是想要的天数,m_list 是 CArrayObj 装的快照对象列表。 参数型构造函数里在初始化清单设好品种,函数体清掉列表,并打上「按毫秒时间排序」的标志,同时写入需要的 DOM 天数。更新方法接收激活时间,用 MarketBookGet() 取结构数组,每条新建快照对象塞进 m_list——这段逻辑注释写得很细,照着读就能懂。 取数方面,按时间找对象时是建个临时快照设好时间+排序标志,Search() 拿索引后删临时对象再返指针;按索引取毫秒时间失败返 NULL。序列名由对象说明拼品种组成,日志打印会先写表头(含请求天数和实收天数),再循环列出每帧订单。外汇和贵金属的 DOM 跳动极快,这类实时累积在高波动时段可能丢帧,验证时建议先开 EURUSD 低频段跑。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                  MBookSeries.mqh |
class=class="str">"cmt">//|             Copyright class="num">2021, MetaQuotes Software Corp. |
class=class="str">"cmt">//|                 [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2021, MetaQuotes Software Corp."
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#class="kw">property strict    class=class="str">"cmt">// Necessary for mql4
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Include files                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include "MarketBookSnapshot.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| "DOM snapshot series" class                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CMBookSeries : class="kw">public CBaseObj
  {
class="kw">private:
   class="type">class="kw">string            m_symbol;             class=class="str">"cmt">// Symbol
   class="type">uint              m_amount;             class=class="str">"cmt">// Number of used DOM snapshots in the series
   class="type">uint              m_required;           class=class="str">"cmt">// Required number of days for DOM snapshot series
   CArrayObj         m_list;               class=class="str">"cmt">// DOM snapshot series list

DOM 序列对象的存取与筛选接口

在 MT5 的 MQL5 深度-of-market 封装里,CMBookSeries 承担的是「某一交易品种 DOM 快照序列」的容器角色。它内部用 m_book_info[] 存结构描述,用 m_list 挂历次快照对象,对外只暴露几个指针和索引方法,避免调用方直接碰底层数组。 GetObject() 返回自身地址,GetList() 返回 m_list 的指针,这两行是后续所有筛选的入口。真正有用的是三个重载 GetList(ENUM_MBOOK_ORD_PROP_*, value, mode):分别按双精度、整型、字符串属性做条件比对,默认比较模式 EQUAL,底层交给 CSelect::ByMBookProperty 去扫列表。 快照定位靠索引和时间两条路。GetMBookByListIndex(uint) 直接取第 N 个;GetLastMBook() 取 m_list.Total()-1,也就是最新一帧;DataTotal() 只是 m_list.Total() 的薄封装。若按毫秒时间取,用 GetMBook(time_msc),该函数声明在此但未贴实现。 品种与回溯天数由 SetSymbol(string) 和 SetRequiredUsedDays(uint) 控制,后者默认 0 表示不限制天数。Compare() 被重写用于按 Symbol() 字符串比较,返回 0 / 1 / -1,使该对象能被 CArrayObj 排序或查找。 让小布替你跑这套 把上面三个 GetList 重载直接抄进 EA,用 ENUM_MBOOK_ORD_PROP_INTEGER 里的成交量字段配合 GREATER 模式,能在 DOM 突变时秒筛出异常快照,比每帧全量遍历省 CPU。外汇与贵金属 DOM 数据受流动性影响大,回测结论仅代表历史概率,实盘高风险。

MQL5 / C++
  MqlBookInfo       m_book_info[];                                                                     class=class="str">"cmt">// DOM structure array
class="kw">public:
class=class="str">"cmt">//--- Return(class="num">1) itself and(class="num">2) the series list
   CMBookSeries     *GetObject(class="type">void)                                                          { class="kw">return &this;   }
   CArrayObj        *GetList(class="type">void)                                                            { class="kw">return &m_list; }
class=class="str">"cmt">//--- Return the DOM snapshot list by selected(class="num">1) class="type">class="kw">double, (class="num">2) integer and(class="num">3) class="type">class="kw">string properties fitting the compared condition
   CArrayObj        *GetList(ENUM_MBOOK_ORD_PROP_DOUBLE class="kw">property,class="type">class="kw">double value,ENUM_COMPARER_TYPE mode=EQUAL){ class="kw">return CSelect::ByMBookProperty(this.GetList(),class="kw">property,value,mode); }
   CArrayObj        *GetList(ENUM_MBOOK_ORD_PROP_INTEGER class="kw">property,class="type">long value,ENUM_COMPARER_TYPE mode=EQUAL) { class="kw">return CSelect::ByMBookProperty(this.GetList(),class="kw">property,value,mode); }
   CArrayObj        *GetList(ENUM_MBOOK_ORD_PROP_STRING class="kw">property,class="type">class="kw">string value,ENUM_COMPARER_TYPE mode=EQUAL){ class="kw">return CSelect::ByMBookProperty(this.GetList(),class="kw">property,value,mode); }
class=class="str">"cmt">//--- Return the DOM snapshot object by(class="num">1) index in the list, (class="num">2) time and(class="num">3) actual list size
   CMBookSnapshot   *GetMBookByListIndex(class="kw">const class="type">uint index)        class="kw">const { class="kw">return this.m_list.At(index);                    }
   CMBookSnapshot   *GetLastMBook(class="type">void)                           class="kw">const { class="kw">return this.m_list.At(this.DataTotal()-class="num">1); }
   CMBookSnapshot   *GetMBook(class="kw">const class="type">long time_msc);
   class="type">int              DataTotal(class="type">void)                              class="kw">const { class="kw">return this.m_list.Total();                    }
class=class="str">"cmt">//--- Set a(class="num">1) symbol, (class="num">2) a number of days for DOM snapshots
   class="type">void             SetSymbol(class="kw">const class="type">class="kw">string symbol);
   class="type">void             SetRequiredUsedDays(class="kw">const class="type">uint required=class="num">0);
class=class="str">"cmt">//--- The comparison method for searching and sorting DOM snapshot series objects by symbol
   class="kw">virtual class="type">int      Compare(class="kw">const CObject *node,class="kw">const class="type">int mode=class="num">0) class="kw">const
                 {  
                     class="kw">const CMBookSeries *compared_obj=node;
                     class="kw">return(this.Symbol()==compared_obj.Symbol() ? class="num">0 : this.Symbol()>compared_obj.Symbol() ? class="num">1 : -class="num">1);

◍ 深度挂单序列类的存取接口

在 MT5 里抓取深度挂单(DOM)历史,靠的是一个自封装的 CMBookSeries 类。它把某交易品种的符号、已用快照数、请求天数和按时间排序的快照链表都收进成员变量,对外只暴露几个轻量方法。 Header() 返回该序列的名称,Print() 与 PrintShort() 分别打印完整和简版描述,方便在专家日志里快速核对当前加载的是哪个品种的挂单序列。构造函数支持无参初始化,也允许直接传入 symbol 与 required(默认 0)来限定回溯的请求天数。 实际取数时,Symbol() 回传 m_symbol,AvailableUsedData() 给出 m_amount 条已载入快照,RequiredUsedDays() 回传 m_required,MBookTime(index) 则按索引返回某帧快照的毫秒时间。Refresh(time_msc) 负责按传入时间刷新链表。 参数化构造函数里有个细节:初始化列表只写了 m_symbol(symbol),随后在块内 Clear() 链表、用 SORT_BY_MBOOK_ORD_TIME_MSC 做时间升序排序,再调 SetRequiredUsedDays(required)。如果你改了排序枚举,历史回看顺序会直接乱掉,外汇与贵金属 DOM 数据本身高频易变,验证前先确认排序键没被碰过。

MQL5 / C++
class="type">class="kw">string Header(class="type">void);
class="type">void Print(class="type">void);
class="type">void PrintShort(class="type">void);
CMBookSeries(){;}
CMBookSeries(class="kw">const class="type">class="kw">string symbol,class="kw">const class="type">uint required=class="num">0);
class="type">class="kw">string Symbol(class="type">void) class="kw">const { class="kw">return this.m_symbol; }
class="type">ulong AvailableUsedData(class="type">void) class="kw">const { class="kw">return this.m_amount; }
class="type">ulong RequiredUsedDays(class="type">void) class="kw">const { class="kw">return this.m_required; }
class="type">long MBookTime(class="kw">const class="type">int index) class="kw">const;
class="type">bool Refresh(class="kw">const class="type">long time_msc);
CMBookSeries::CMBookSeries(class="kw">const class="type">class="kw">string symbol,class="kw">const class="type">uint required=class="num">0) : m_symbol(symbol)
  {
  this.m_list.Clear();
  this.m_list.Sort(SORT_BY_MBOOK_ORD_TIME_MSC);
  this.SetRequiredUsedDays(required);
  }
class="type">bool CMBookSeries::Refresh(class="kw">const class="type">long time_msc)
  {

「DOM快照序列的写入与检索实现」

把盘口深度(Market Book)按毫秒时间落库,核心在 CMBookSeries::Add 这一段。先调 MarketBookGet 抓当前品种盘口,失败直接返回 false;成功才 new 一个 CMBookSnapshot 对象塞进按时间排序的链表。 链表超长时要裁剪:当 DataTotal() 大于 MBOOKSERIES_MAX_DATA_TOTAL,就从表头循环 Delete 掉多出数量的旧快照。这个上限常量你在调参时得盯紧,默认若太小会丢历史盘口,太大则内存吃紧。 按时间取快照用 GetMBook:临时 new 一个空快照设好 time_msc,链表 Sort 后 Search 定位下标,取到指针立刻 delete 临时对象并返回链表里的真实快照。注意 Search 依赖排序一致性,若前面 InsertSort 用的键和这里 Sort 默认键不同,可能返回错对象。 SetSymbol 和 SetRequiredUsedDays 是两个轻量 setter:前者空字符串时回退到当前图表的 Symbol(),后者 required 小于1则落到 MBOOKSERIES_DEFAULT_DAYS_COUNT。外汇与贵金属盘口跳动极快,这类序列在高波动时段可能产生数百 MB 快照,实盘前务必在 MT5 策略测试器用真实tick验证内存占用。

MQL5 / C++
  if(!::MarketBookGet(this.m_symbol,this.m_book_info))
        class="kw">return false;
class=class="str">"cmt">//--- Create a new DOM snapshot object
  CMBookSnapshot *book=new CMBookSnapshot(this.m_symbol,time_msc,this.m_book_info);
  if(book==NULL)
        class="kw">return false;
class=class="str">"cmt">//--- Set the flag of a list sorted by time for the list and add the created DOM snapshot to it
  this.m_list.Sort(SORT_BY_MBOOK_ORD_TIME_MSC);
  if(!this.m_list.InsertSort(book))
     {
       class="kw">delete book;
       class="kw">return false;
     }
class=class="str">"cmt">//--- Set time in milliseconds to all DOM snapshot order objects
  book.SetTimeToOrders(time_msc);
class=class="str">"cmt">//--- If the number of snapshots in the list exceeds the class="kw">default maximum number,
class=class="str">"cmt">//--- remove the calculated number of snapshot objects from the end of the list
  if(this.DataTotal()>MBOOKSERIES_MAX_DATA_TOTAL)
     {
       class="type">int total_del=this.m_list.Total()-MBOOKSERIES_MAX_DATA_TOTAL;
       for(class="type">int i=class="num">0;i<total_del;i++)
          this.m_list.Delete(i);
     }
  class="kw">return true;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Set a symbol                                                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CMBookSeries::SetSymbol(class="kw">const class="type">class="kw">string symbol)
  {
  if(this.m_symbol==symbol)
        class="kw">return;
  this.m_symbol=(symbol==NULL || symbol=="" ? ::Symbol() : symbol);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Set the number of days for DOM snapshots in the series                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CMBookSeries::SetRequiredUsedDays(class="kw">const class="type">uint required=class="num">0)
  {
  this.m_required=(required<class="num">1 ? MBOOKSERIES_DEFAULT_DAYS_COUNT : required);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the DOM snapshot object by its time                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
CMBookSnapshot *CMBookSeries::GetMBook(class="kw">const class="type">long time_msc)
  {
  CMBookSnapshot *book=new CMBookSnapshot();
  if(book==NULL)
        class="kw">return NULL;
  book.SetTime(time_msc);
  this.m_list.Sort();
  class="type">int index=this.m_list.Search(book);
  class="kw">delete book;
  class="kw">return this.m_list.At(index);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

从 DOM 快照序列里抠时间和标识

做深度盘口回测时,经常要按索引定位某一帧市场深度(DOM)快照,并取出它的时间戳与品种名。下面这段 MT5 类方法就干了这个活:MBookTime 按 index 取链表里的快照指针,空指针返回 0,否则返回毫秒级时间;Header 则拼出类似「Market Book Series "EURUSD"」的日志标题。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Returns the time in milliseconds                                      |
class=class="str">"cmt">//| of a DOM snapshot specified by index                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">long CMBookSeries::MBookTime(class="kw">const class="type">int index) class="kw">const
  {
   CMBookSnapshot *book=this.m_list.At(index);
   class="kw">return(book!=NULL ? book.Time() : class="num">0);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the name of the DOM snapshot series                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string CMBookSeries::Header(class="type">void)
  {
   class="kw">return CMessage::Text(MSG_MBOOK_SERIES_TEXT_MBOOKSERIES)+" \""+this.m_symbol+"\"";
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
Series of "EURUSD" DOM snapshots
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Display the description of the DOM snapshot series in the journal   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CMBookSeries::Print(class="type">void)
  {
   class="type">class="kw">string txt=
     (
       CMessage::Text(MSG_TICKSERIES_REQUIRED_HISTORY_DAYS)+(class="type">class="kw">string)this.RequiredUsedDays()+", "+
       CMessage::Text(MSG_LIB_TEXT_TS_ACTUAL_DEPTH)+(class="type">class="kw">string)this.DataTotal()
     );
   ::Print(this.Header(),": ",txt);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
逐行拆解:第 1–3 行是注释框,说明函数返回「按索引指定的 DOM 快照的毫秒时间」。MBookTime 中 this.m_list.At(index) 从内部链表取第 index 个快照对象;三元运算符判空,空则 0,否则 book.Time() 给毫秒时间戳。Header 里把消息宏文本和 this.m_symbol(例如 EURUSD)拼成带引号的序列名。Print 方法组合所需历史天数 RequiredUsedDays() 与已存深度条数 DataTotal(),用 ::Print 输出到专家日志。 直接在 MT5 导航器里建个 CMBookSeries 实例,对 EURUSD 跑几十帧深度快照,调用 Print 能看到实际占用历史天数与数据条数;外汇与贵金属盘口高频变动,DOM 回测存在滑点与流动性断裂的高风险,结论仅作概率参考。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Returns the time in milliseconds                                      |
class=class="str">"cmt">//| of a DOM snapshot specified by index                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">long CMBookSeries::MBookTime(class="kw">const class="type">int index) class="kw">const
  {
   CMBookSnapshot *book=this.m_list.At(index);
   class="kw">return(book!=NULL ? book.Time() : class="num">0);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the name of the DOM snapshot series                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string CMBookSeries::Header(class="type">void)
  {
   class="kw">return CMessage::Text(MSG_MBOOK_SERIES_TEXT_MBOOKSERIES)+" \""+this.m_symbol+"\"";
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
Series of "EURUSD" DOM snapshots
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Display the description of the DOM snapshot series in the journal   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CMBookSeries::Print(class="type">void)
  {
   class="type">class="kw">string txt=
     (
       CMessage::Text(MSG_TICKSERIES_REQUIRED_HISTORY_DAYS)+(class="type">class="kw">string)this.RequiredUsedDays()+", "+
       CMessage::Text(MSG_LIB_TEXT_TS_ACTUAL_DEPTH)+(class="type">class="kw">string)this.DataTotal()
     );
   ::Print(this.Header(),": ",txt);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 在MT5里跑通DOM快照序列

把上一节的 EA 放到 \MQL5\Experts\TestDoEasy\Part64\ 下,改名为 TestDoEasyPart64.mq5,就能直接复用做 DOM 快照序列的实测。EA 里给当前品种建一个 DOM 快照序列对象,每次 OnBookEvent() 被触发就往序列里压一张新快照,图表注释同步刷出已存快照数、以及当前快照买卖两端的最优价(最高卖价 / 最低买价)。 原 EA 里那几个订单对象类的 include 可以删了——它们已经并进了新建的类文件。换成一行 #include <DoEasy\Objects\Book\MBookSeries.mqh> 把 DOM 快照序列类拉进来,全局变量里再声明一个该类的实例即可。 所有建序列、塞快照的动作都收在 OnBookEvent() 里,代码注释写得很直白,基本不用额外解释。编译挂到图表,设置里指定品种和时间帧,终端日志会先打出首张快照的原始数据。 实测跑了一段时间后,图表上能看到序列里已累计 5019 张快照,同时显示最新快照编号、品种总订单数、当前快照订单数和列表总数。外汇和贵金属盘口薄数据跳动极快,这种高频快照对内存和事件处理是实打实的压力测试,需留意 MT5 终端卡顿风险。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                      TestDoEasyPart63.mq5 |
class=class="str">"cmt">//|     Copyright class="num">2021, MetaQuotes Software Corp. |
class=class="str">"cmt">//|          [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2021, MetaQuotes Software Corp."
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class=class="str">"cmt">//--- includes
class="macro">#include <DoEasy\Engine.mqh>
class="macro">#include <DoEasy\Objects\Book\MarketBookBuy.mqh>
class="macro">#include <DoEasy\Objects\Book\MarketBookSell.mqh>
class="macro">#include <DoEasy\Objects\Book\MarketBookBuyMarket.mqh>
class="macro">#include <DoEasy\Objects\Book\MarketBookSellMarket.mqh>
class=class="str">"cmt">//--- enums
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                      TestDoEasyPart64.mq5 |
class=class="str">"cmt">//|     Copyright class="num">2021, MetaQuotes Software Corp. |
class=class="str">"cmt">//|          [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2021, MetaQuotes Software Corp."
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class=class="str">"cmt">//--- includes
class="macro">#include <DoEasy\Engine.mqh>
class="macro">#include <DoEasy\Objects\Book\MBookSeries.mqh>
class=class="str">"cmt">//--- enums
class=class="str">"cmt">//--- global variables
CEngine        engine;
SDataButt      butt_data[TOTAL_BUTT];
class="type">class="kw">string         prefix;
class="type">class="kw">double         lot;
class="type">class="kw">double         withdrawal=(InpWithdrawal<class="num">0.1 ? class="num">0.1 : InpWithdrawal);
class="type">class="kw">ushort         magic_number;
class="type">uint           stoploss;
class="type">uint           takeprofit;
class="type">uint           distance_pending;
class="type">uint           distance_stoplimit;
class="type">uint           distance_pending_request;
class="type">uint           bars_delay_pending_request;
class="type">uint           slippage;
class="type">bool           trailing_on;
class="type">bool           pressed_pending_buy;

「挂单状态与深度快照的事件承接」

这段结构体声明把面板能触发的挂单动作全部摊开:从 buy limit / buy stop / buy stoplimit 到对应的卖出平仓、反手平仓,再到 delete_all 与 close_all,一共 17 个 bool 标记位,外加 trailing_stop、trailing_step 等 6 个数值字段控制移动止损与改单阈值。做半自动面板时,用这种扁平 bool 组比嵌套 enum 更利于在 OnBookEvent 里直接读按键状态。 深度数据靠 CMBookSeries 对象接住。OnBookEvent(const string& symbol) 被触发时,先通过 engine.GetSymbolCurrent() 拿当前品种,若对象为空或该品种未订阅 DOM(BookdepthSubscription 返回 false)就直接 return,避免对无盘口品种空转。 确认 symbol 与当前对象同名后,给 book_series 设品种与所需天数,再调 Refresh(sym.Time()) 拉最新快照;失败同样退出。随后 GetLastMBook() 取序列里最后一帧盘口,若返回 NULL 说明这一刻快照还没成型,后面计算只能等下一笔事件。 别把订阅当默认开启 MT5 里 DOM 订阅是品种级开关,BookdepthSubscription() 返回 false 在交叉盘和贵金属小周期上很常见,不先判空会让盘口策略在 XAUUSD 的 M1 上静默失效。

MQL5 / C++
class="type">bool         pressed_pending_buy_limit;
class="type">bool         pressed_pending_buy_stop;
class="type">bool         pressed_pending_buy_stoplimit;
class="type">bool         pressed_pending_close_buy;
class="type">bool         pressed_pending_close_buy2;
class="type">bool         pressed_pending_close_buy_by_sell;
class="type">bool         pressed_pending_sell;
class="type">bool         pressed_pending_sell_limit;
class="type">bool         pressed_pending_sell_stop;
class="type">bool         pressed_pending_sell_stoplimit;
class="type">bool         pressed_pending_close_sell;
class="type">bool         pressed_pending_close_sell2;
class="type">bool         pressed_pending_close_sell_by_buy;
class="type">bool         pressed_pending_delete_all;
class="type">bool         pressed_pending_close_all;
class="type">bool         pressed_pending_sl;
class="type">bool         pressed_pending_tp;
class="type">class="kw">double       trailing_stop;
class="type">class="kw">double       trailing_step;
class="type">uint         trailing_start;
class="type">uint         stoploss_to_modify;
class="type">uint         takeprofit_to_modify;
class="type">int          used_symbols_mode;
class="type">class="kw">string       array_used_symbols[];
class="type">class="kw">string       array_used_periods[];
class="type">bool         testing;
class="type">uchar        group1;
class="type">uchar        group2;
class="type">class="kw">double       g_point;
class="type">int          g_digits;
class=class="str">"cmt">//---
CMBookSeries  book_series;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| OnBookEvent function                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnBookEvent(class="kw">const class="type">class="kw">string& symbol)
  {
   class="kw">static class="type">bool first=true;
   class=class="str">"cmt">//--- Get a symbol object
   CSymbol *sym=engine.GetSymbolCurrent();
   class=class="str">"cmt">//--- If failed to get a symbol object or it is not subscribed to DOM, exit
   if(sym==NULL || !sym.BookdepthSubscription()) class="kw">return;
   class=class="str">"cmt">//--- Work by the current symbol
   if(symbol==sym.Name())
     {
      class=class="str">"cmt">//--- Set a symbol and a required number of data days for the DOM snapshot series object
      book_series.SetSymbol(sym.Name());
      book_series.SetRequiredUsedDays();
      class=class="str">"cmt">//--- Update the DOM snapshot series
      if(!book_series.Refresh(sym.Time()))
         class="kw">return;

      class=class="str">"cmt">//--- Get the last DOM snapshot object from the DOM snapshot series object
      CMBookSnapshot *book=book_series.GetLastMBook();
      if(book==NULL)

从 DOM 快照里抓首尾挂单

在 DOM 快照对象里,用 GetMBookByListIndex(0) 取列表首单、GetMBookByListIndex(DataTotal()-1) 取尾单,这两张单分别是当前盘口的最低价买挂和最高价卖挂。若指针为 NULL 直接 return,避免空引用把 EA 拖崩。 拿到 ord_0 与 ord_N 后,用 Comment() 把快照时间、品种最大展示深度 sym.TicksBookdepth()、本次快照订单数 book.DataTotal()、序列总快照数 book_series.DataTotal() 以及首尾单 Header() 打到图表。Header() 返回的是「价位的手数」简写,肉眼可读盘口薄厚。 首帧用 first 标志位控制,只在第一次进入时把 book_series.Print() 和 book.Print() 吐进日志,之后置 false。这样日志不会被每 tick 刷屏,又能留一份初始盘口结构做比对。 回测日志里 EURUSD 在 2021.02.09 22:16:24.557 的快照显示:卖单从 1.21190(6 手)阶梯到 1.21198(250 手),买单从 1.21188(36 手)到 1.21180(250 手),中间 1.21189 无挂单——这种缺口在 H1 级别外汇盘口里可能暗示短期流动性偏向某一侧,但外汇和贵金属杠杆高,DOM 结构随时被大单改写,仅作概率参考。

MQL5 / C++
    class="kw">return;
   class=class="str">"cmt">//--- Get the very first and last DOM order objects from the DOM snapshot object
   CMarketBookOrd *ord_0=book.GetMBookByListIndex(class="num">0);
   CMarketBookOrd *ord_N=book.GetMBookByListIndex(book.DataTotal()-class="num">1);
   if(ord_0==NULL || ord_N==NULL) class="kw">return;
   class=class="str">"cmt">//--- Display the time of the current DOM snapshot in the chart comment,
   class=class="str">"cmt">//--- the maximum number of displayed orders in DOM for a symbol,
   class=class="str">"cmt">//--- the obtained number of orders in the current DOM snapshot,
   class=class="str">"cmt">//--- the total number of DOM snapshots set in the series list and
   class=class="str">"cmt">//--- the highest and lowest orders of the current DOM snapshot
   Comment(
      DFUN,sym.Name(),": ",TimeMSCtoString(book.Time()),
      ", symbol book size=",sym.TicksBookdepth(),
      ", last book data total: ",book.DataTotal(),
      ", series books total: ",book_series.DataTotal(),
      "\nMax: ",ord_N.Header(),"\nMin: ",ord_0.Header()
     );
   class=class="str">"cmt">//--- Display the first DOM snapshot in the journal
   if(first)
     {
      class=class="str">"cmt">//--- series description
      book_series.Print();
      class=class="str">"cmt">//--- snapshot description
      book.Print();
      first=false;
     }
   }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
Account class="num">8550475: Artyom Trishkin(MetaQuotes Software Corp.) class="num">10428.13 USD, class="num">1:class="num">100, Hedge, MetaTrader class="num">5 demo
--- Initializing "DoEasy" library ---
Working with predefined symbol list. The number of used symbols: class="num">2
"AUDUSD" "EURUSD"
Working with the current timeframe only: H1
AUDUSD symbol timeseries:
- Timeseries "AUDUSD" H1: Requested: class="num">1000, Actual: class="num">1000, Created: class="num">1000, On the server: class="num">5121
EURUSD symbol timeseries:
- Timeseries "EURUSD" H1: Requested: class="num">1000, Actual: class="num">1000, Created: class="num">1000, On the server: class="num">6046
Tick series "AUDUSD": Requested number of days: class="num">1, Historical data created: class="num">176033
Tick series "EURUSD": Requested number of days: class="num">1, Historical data created: class="num">181969
Subscribed to Depth of Market  AUDUSD
Subscribed to Depth of Market  EURUSD
Library initialization time: class="num">00:class="num">00:class="num">12.516
The "EURUSD" DOM snapshot series: Requested number of days: class="num">1, Actual history depth: class="num">1
"EURUSD" DOM snapshot(class="num">2021.02.class="num">09 class="num">22:class="num">16:class="num">24.557):
  - Sell order: class="num">1.21198 [class="num">250.00]
  - Sell order: class="num">1.21193 [class="num">100.00]
  - Sell order: class="num">1.21192 [class="num">50.00]
  - Sell order: class="num">1.21191 [class="num">30.00]
  - Sell order: class="num">1.21190 [class="num">6.00]
  - Buy order: class="num">1.21188 [class="num">36.00]
  - Buy order: class="num">1.21186 [class="num">50.00]
  - Buy order: class="num">1.21185 [class="num">100.00]
  - Buy order: class="num">1.21180 [class="num">250.00]

◍ 一点提醒

这套 DoEasy 价格扩展目前只释出了 DOM 快照序列的雏形,配套 MQL5 测试 EA 和函数库文件已打包为 3897 KB 的 ZIP 供下载验证。操控 DOM 的类仍在开发,作者在原文明确提示现阶段不要在自定义程序里直接调用,避免依赖未稳定的接口。 如果你打算在 MT5 里跑这套测试 EA,建议先只加载单一品种观察 DOM 订阅回调,别急着广播全品种——外汇与贵金属杠杆高、盘口跳变快,未定型封装可能引发行情响应延迟。 下一篇会做全品种 DOM 集合与 MQL5.com 信号类,当前这版当作隔离沙盒试用即可,有问题留原帖评论比自己改内核更稳妥。

常见问题

用 DOM 快照对象的取数入口直接读对应档位的买卖量字段,避免自己遍历订单簿,能省掉一半解析代码。
快照对象内部按交易所推送时间戳记录,打印前要调用时间同步接口对齐本地会话时间,否则会出现错位。
可以,小布盯盘的 AIGC 会按快照序列的筛选接口跑一遍,把偏离均值较大的挂单档直接在高亮层标给你看。
用序列类的存取与筛选接口传时间区间参数,返回的子序列只包含该窗口的快照,后续统计更轻量。
对比相邻两次快照的同一价位挂单量,结合序列对象存取接口的差量输出,量减即为撤单、量增即为新增。