市场深度抽象请求类:从零搭建 DOM 订单对象模型(基础篇)
📊

市场深度抽象请求类:从零搭建 DOM 订单对象模型(基础篇)

(1/3)· 很多交易者盯着买一卖一却看不懂 DOM 全貌,本篇用基类+衍生类把订单拆明白

案例拆解新手友好 第 1/3 篇
MarketBookGet 返回的不只是价格,而是四种订单类型的组合。只盯盘口第一档,等于把 DOM 里大半信息主动屏蔽掉。先理清订单抽象结构,后续诊断才有地基。

「给市场深度建一个抽象请求类」

在 DoEasy 函数库的价格系列里,市场深度(DOM)不再只是终端里那张买卖挂单表,而是可以被对象化请求的数据源。2021 年 4 月 5 日发布的这一版实现,把 DOM 访问抽象成一个请求类,衍生类再去具体描述某一类订单对象,这样上层逻辑不必关心底层是即时深度还是聚合深度。 抽象订单对象类只定义接口与必要属性,例如价格、成交量、方向;衍生类负责把真实的市场深度快照映射进来。这种写法让后续扩展(比如只抓最优五档、或过滤掉小于某手数的挂单)变成改一个衍生类的事,而不是动核心循环。 打开 MT5 自建脚本,引用该抽象类后,你能直接拿到 DOM 每档的 pricevolume 做高频统计——外汇与贵金属的 DOM 跳动快、滑点大,实盘验证前请先在策略测试器用历史 tick 跑一遍,高风险品种尤其要控仓。

把 DOM 拆成可订阅的对象模型

做市场深度(DOM)操控,本质和之前写的函数库类没两样,区别在于我们要先建一套 DOM 特征数据模型:在 OnBookEvent() 触发时,用 MarketBookGet() 把当前品种的挂单拉进来。每一次 DOM 变动,处理程序会为所有订阅了该品种的事件各发一次通知。 DOM 的类结构可以分成四层:单笔订单对象(描述某一笔挂单)、某品种单次快照的模型对象(一次 OnBookEvent 里并发拿到的全部订单集合)、由每次快照构成的单一品种时间序列、以及跨品种 DOM 时间序列的集合类。今天先落地第一层——订单对象类,并验证在当前品种上订阅后能否正常取到数据。 MqlBookInfo 结构给了订单的原始字段:类型来自 ENUM_BOOK_TYPE、价格、交易量、以及更高精度的交易量。枚举里实际只有四种单子——卖单、市价卖单、买单、市价买单。为了快速把供给和需求分开,我们在原有属性上补一个方向标记(买/卖),这样一次快照进来就能立刻切两半。 基类做抽象订单,再派生四个带类型规则的子类,这套构造思路在库开发最前面两篇就定过调了。动手前得先在 Data.mqh 里补 DOM 相关的消息索引,并在 Symbol.mqh 中加上订阅/取消订阅 DOM 失败的错误文本;TickSeries.mqh 里则删掉原先为了调试贴在图表上的注释,把上次报价时间直接写进 m_last_time,少一层无谓开销。 下面这段是消息枚举里新增的 DOM 订阅与错误项,以及即时报价序列类的部分消息声明,注意高亮两行是本次加的订阅错误反馈:

MQL5 / C++
  MSG_SYM_EVENT_SYMBOL_ADD,                    class=class="str">"cmt">// Added symbol to Market Watch window
  MSG_SYM_EVENT_SYMBOL_DEL,                    class=class="str">"cmt">// Symbol removed from Market Watch window
  MSG_SYM_EVENT_SYMBOL_SORT,                   class=class="str">"cmt">// Changed location of symbols in Market Watch window
  MSG_SYM_SYMBOLS_MODE_CURRENT,                class=class="str">"cmt">// Work with current symbol only
  MSG_SYM_SYMBOLS_MODE_DEFINES,                class=class="str">"cmt">// Work with predefined symbol list
  MSG_SYM_SYMBOLS_MODE_MARKET_WATCH,           class=class="str">"cmt">// Work with Market Watch window symbols
  MSG_SYM_SYMBOLS_MODE_ALL,                    class=class="str">"cmt">// Work with full list of all available symbols
  MSG_SYM_SYMBOLS_BOOK_ADD,                    class=class="str">"cmt">// Subscribed to Depth of Market 
  MSG_SYM_SYMBOLS_BOOK_DEL,                    class=class="str">"cmt">// Unsubscribed from Depth of Market 
  MSG_SYM_SYMBOLS_MODE_BOOK,                   class=class="str">"cmt">// Subscription to Depth of Market
  MSG_SYM_SYMBOLS_ERR_BOOK_ADD,                class=class="str">"cmt">// Error subscribing to DOM
  MSG_SYM_SYMBOLS_ERR_BOOK_DEL,                class=class="str">"cmt">// Error unsubscribing from DOM

class=class="str">"cmt">//--- CAccount
class=class="str">"cmt">//--- CTickSeries
  MSG_TICKSERIES_TEXT_TICKSERIES,              class=class="str">"cmt">// Tick series
  MSG_TICKSERIES_ERR_GET_TICK_DATA,            class=class="str">"cmt">// Failed to get tick data
  MSG_TICKSERIES_FAILED_CREATE_TICK_DATA_OBJ,  class=class="str">"cmt">// Failed to create tick data object
  MSG_TICKSERIES_FAILED_ADD_TO_LIST,           class=class="str">"cmt">// Failed to add tick data object to list
  MSG_TICKSERIES_TEXT_IS_NOT_USE,              class=class="str">"cmt">// Tick series not used. Set the flag using SetAvailable()
  MSG_TICKSERIES_REQUIRED_HISTORY_DAYS,        class=class="str">"cmt">// Requested number of days

class=class="str">"cmt">//--- CMarketBookOrd

◍ 深度映射DOM字段与订阅事件

在 MQL5 的行情深度(DOM)相关封装类里,挂单方向的字段标签必须和终端内部枚举一一对齐。下面这组映射把俄文原串和英文输出绑定,覆盖了「Order in Depth of Market / Volume / Volume Real / Buy side / Sell side」以及限价单、市价单的买卖类别,共 9 个 DOM 订单字段。 订阅状态机也同样用字符串表管理:从「Added symbol to Market Watch」到「Subscribed to Depth of Market」,再到背景高亮的「错误订阅/退订」空串回执,一共 11 条市场观察与 DOM 事件描述。实盘跑 EA 时若 BookAdd() 返回 false,优先排查最后两条错误回执是否触发。 外汇与贵金属的 DOM 数据受流动性断层影响明显,订阅失败概率在重大数据发布前后会显著抬升,属正常现象而非代码缺陷。

MQL5 / C++
  MSG_MBOOK_ORD_TEXT_MBOOK_ORD,                                                                     class=class="str">"cmt">// Order in DOM
  MSG_MBOOK_ORD_VOLUME,                                                                               class=class="str">"cmt">// Volume
  MSG_MBOOK_ORD_VOLUME_REAL,                                                                          class=class="str">"cmt">// Extended accuracy volume
  MSG_MBOOK_ORD_STATUS_BUY,                                                                           class=class="str">"cmt">// Buy side
  MSG_MBOOK_ORD_STATUS_SELL,                                                                          class=class="str">"cmt">// Sell side
  MSG_MBOOK_ORD_TYPE_SELL,                                                                            class=class="str">"cmt">// Sell order
  MSG_MBOOK_ORD_TYPE_BUY,                                                                             class=class="str">"cmt">// Buy order 
  MSG_MBOOK_ORD_TYPE_SELL_MARKET,                                                                     class=class="str">"cmt">// Sell order by Market
  MSG_MBOOK_ORD_TYPE_BUY_MARKET,                                                                      class=class="str">"cmt">// Buy order by Market
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
  {"В окно \"Обзор рынка\" добавлен символ","Added symbol to \"Market Watch\" window"},
  {"Из окна \"Обзор рынка\" удалён символ","Removed from \"Market Watch\" window"},
  {"Изменено расположение символов в окне \"Обзор рынка\"","Changed arrangement of symbols in \"Market Watch\" window"},
  {"Работа только с текущим символом","Work only with the current symbol"},
  {"Работа с предопределённым списком символов","Work with predefined list of symbols"},
  {"Работа с символами из окна \"Обзор рынка\"","Working with symbols from \"Market Watch\" window"},
  {"Работа с полным списком всех доступных символов","Work with full list of all available symbols"},
  {"Осуществлена подписка на стакан цен ","Subscribed to Depth of Market"},
  {"Осуществлена отписка от стакан цен ","Unsubscribed from Depth of Market"},
  {"Подписка на стакан цен","Subscription to Depth of Market"},
  {"Ошибка при подписке на стакан цен",""},
  {"Ошибка при отписке от стакан цен",""},
class=class="str">"cmt">//--- CAccount
class=class="str">"cmt">//--- CMarketBookOrd
  {"Заявка в стакане цен","Order in Depth of Market"},
  {"Объем","Volume"},
  {"Объем c повышенной точностью","Volume Real"},
  {"Сторона Buy","Buy side"},
  {"Сторона Sell","Sell side"},
  {"Заявка на продажу","Sell order"},
  {"Заявка на покупку","Buy order"},
  {"Заявка на продажу по рыночной цене","Sell order at market price"},
  {"Заявка на покупку по рыночной цене","Buy order at market price"},
  };
class=class="str">"cmt">//+---------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Subscribe to the Depth of Market                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CSymbol::BookAdd(class="type">void)
  {

「深度簿订阅与tick刷新的底层开关」

在 MT5 里抓盘口深度,第一步是向终端注册符号的市场深度簿。MQL5 用 MarketBookAdd() 订阅、MarketBookRelease() 退订,而 MQL4 环境直接走 false/true 短路,因为老平台根本没有 DOM 接口。 订阅结果会写回对象的两个地方:m_book_subscribed 标志位和 m_long_prop[SYMBOL_PROP_BOOKDEPTH_STATE]。成功就打印添加日志,失败则通过 GetLastError() 把具体错误码带出来,方便你排查品种是否支持深度簿(部分外汇经纪商对 XAUUSD 可能不推盘口)。 退订时若标志位本来就是关的,函数直接返回 true,避免重复释放。真正调用 MarketBookRelease() 后,无论成败都会同步刷新状态位——这点很关键,否则对象内部会以为还挂着簿,后续刷新逻辑可能空转。 tick 系列刷新靠 CopyTicksRange(),从 m_last_time+1 毫秒拉到最新。参数 COPY_TICKS_ALL 表示全类型 tick 都收,total>0 才进循环建对象;如果某秒没新 tick,total=0,列表原样不动,不会报错但也不会补数据。外汇和贵金属波动快,这种增量拉取能压低内存,但高频场景下漏 tick 的概率会随经纪商延迟上升,属正常风险。

MQL5 / C++
this.m_book_subscribed=(class="macro">#ifdef __MQL5__ ::MarketBookAdd(this.m_name) class="macro">#else false class="macro">#endif);
this.m_long_prop[SYMBOL_PROP_BOOKDEPTH_STATE]=this.m_book_subscribed;
if(this.m_book_subscribed)
   ::Print(CMessage::Text(MSG_SYM_SYMBOLS_BOOK_ADD)+" "+this.m_name);
else
   ::Print(CMessage::Text(MSG_SYM_SYMBOLS_ERR_BOOK_ADD)+": "+CMessage::Text(::GetLastError()));
class="kw">return this.m_book_subscribed;
}
class="type">bool CSymbol::BookClose(class="type">void)
  {
  if(!this.m_book_subscribed)
     class="kw">return true;
  class="type">bool res=( class="macro">#ifdef __MQL5__ ::MarketBookRelease(this.m_name) class="macro">#else true class="macro">#endif );
  if(res)
     {
     this.m_long_prop[SYMBOL_PROP_BOOKDEPTH_STATE]=this.m_book_subscribed=false;
     ::Print(CMessage::Text(MSG_SYM_SYMBOLS_BOOK_DEL)+" "+this.m_name);
     }
  else
   {
     this.m_long_prop[SYMBOL_PROP_BOOKDEPTH_STATE]=this.m_book_subscribed=true;
     ::Print(CMessage::Text(MSG_SYM_SYMBOLS_ERR_BOOK_DEL)+": "+CMessage::Text(::GetLastError()));
   }
  class="kw">return res;
  }
class="type">void CTickSeries::Refresh(class="type">void)
  {
  class="type">MqlTick ticks_array[];
  if(IsNewTick())
     {
     class="type">int err=ERR_SUCCESS;
     class="type">int total=::CopyTicksRange(this.Symbol(),ticks_array,COPY_TICKS_ALL,this.m_last_time+class="num">1,class="num">0);
     if(total>class="num">0)
       {

增量刷新与容量裁剪的落地写法

在自定义 tick 序列类里,Refresh() 只做一件事:从上一帧记录的 m_last_time+1 毫秒开始,把新到达的 tick 增量拉进内存。CopyTicksRange 的第四个参数传 this.m_last_time+1、第五个参数传 0,意味着只取该时间点之后的全部 tick 类型(COPY_TICKS_ALL),不重复消费已经处理过的行情。 循环里每取到一个 MqlTick,就调 CreateNewTickObj 生成一个 CDataTick 对象塞进链表;若某次返回 NULL 直接 break,避免半截数据污染序列。同时把 end_time 记成 ticks_array 最后一笔的 time_msc,作为下一轮刷新的起点——AUDUSD 这类活跃品种在伦敦-纽约重叠时段每秒可能推十几笔 tick,不滚动这个时间窗就会重复拷贝整段历史。 内存不是无限用的。代码里设了 TICKSERIES_MAX_DATA_TOTAL 作为上限,一旦 DataTotal() 超出,就从链表头部按超出量 m_list_ticks.Total()-TICKSERIES_MAX_DATA_TOTAL 循环 Delete(j) 裁掉最老的 tick 对象。外汇与贵金属tick流受流动性与滑点影响波动剧烈,实盘跑这段代码前建议在策略测试器用真实tick数据确认上限值不会让近期价格行为样本被过早丢弃。

MQL5 / C++
for(class="type">int i=class="num">0;i<total;i++)
  {
  class=class="str">"cmt">//--- Create the tick object and add it to the list
  CDataTick *tick_obj=this.CreateNewTickObj(ticks_array[i]);
  if(tick_obj==NULL)
    class="kw">break;
  class=class="str">"cmt">//--- Write the last tick time for subsequent copying of newly arrived ticks
  class="type">long end_time=ticks_array[::ArraySize(ticks_array)-class="num">1].time_msc;
  if(this.Symbol()=="AUDUSD")
    Comment(DFUN,this.Symbol(),", copied=",total,", m_last_time=",TimeMSCtoString(m_last_time),", end_time=",TimeMSCtoString(end_time),", total=",DataTotal());
  this.m_last_time=end_time;
  }
  class=class="str">"cmt">//--- If the number of ticks in the list exceeds the class="kw">default maximum number,
  class=class="str">"cmt">//--- remove the calculated number of tick objects from the end of the list
  if(this.DataTotal()>TICKSERIES_MAX_DATA_TOTAL)
    {
    class="type">int total_del=m_list_ticks.Total()-TICKSERIES_MAX_DATA_TOTAL;
    for(class="type">int j=class="num">0;j<total_del;j++)
      this.m_list_ticks.Delete(j);
    }
}

class="type">void CTickSeries::Refresh(class="type">void)
  {
  class="type">MqlTick ticks_array[];
  if(IsNewTick())
    {
    class=class="str">"cmt">//--- Copy ticks from m_last_time time+class="num">1 ms to the end of history
    class="type">int err=ERR_SUCCESS;
    class="type">int total=::CopyTicksRange(this.Symbol(),ticks_array,COPY_TICKS_ALL,this.m_last_time+class="num">1,class="num">0);
    class=class="str">"cmt">//--- If the ticks have been copied, create new tick data objects and add them to the list in the loop by their number
    if(total>class="num">0)
      {
      for(class="type">int i=class="num">0;i<total;i++)
        {
        class=class="str">"cmt">//--- Create the tick object and add it to the list
        CDataTick *tick_obj=this.CreateNewTickObj(ticks_array[i]);
        if(tick_obj==NULL)

◍ 用最后 tick 时间截断旧数据防溢出

在 tick 接收循环的末尾,先记下本次取到的最新 tick 时间戳,供下一轮只拷贝新到达的 tick。代码用 ticks_array[ArraySize(ticks_array)-1].time_msc 取数组末位,毫秒级时间写入 m_last_time,避免每轮全量重拉。 当链表里的 tick 对象总数超过 TICKSERIES_MAX_DATA_TOTAL 这个上限时,要从头部删掉多出来的部分。先算 total_del = 当前总数 - 上限,再从索引 0 开始循环 Delete(j),把最旧的 tick 逐条扔掉。 在 MT5 里把 TICKSERIES_MAX_DATA_TOTAL 设成比如 100000,跑一晚上高频品种,若发现内存曲线被压平,就说明这段裁剪逻辑生效了。外汇与贵金属 tick 流速受事件影响波动极大,链表无上限会拖垮 EA,实盘前务必在策略测试器用真实tick验证裁剪阈值。

MQL5 / C++
            class="kw">break;
            class=class="str">"cmt">//--- Write the last tick time for subsequent copying of newly arrived ticks
            this.m_last_time=ticks_array[::ArraySize(ticks_array)-class="num">1].time_msc;
            }
            class=class="str">"cmt">//--- If the number of ticks in the list exceeds the class="kw">default maximum number,
            class=class="str">"cmt">//--- remove the calculated number of tick objects from the end of the list
            if(this.DataTotal()>TICKSERIES_MAX_DATA_TOTAL)
              {
               class="type">int total_del=m_list_ticks.Total()-TICKSERIES_MAX_DATA_TOTAL;
               for(class="type">int j=class="num">0;j<total_del;j++)
                  this.m_list_ticks.Delete(j);
              }
           }
        }
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
交给小布盯盘看盘口
这些 DOM 订单分类与供需划分逻辑,小布盯盘的 AIGC 已内置,打开对应品种页即可看到实时买卖压力分布,你只需判读方向。

常见问题

订单类型来自 ENUM_BOOK_TYPE 枚举,配合价格、交易量及高精度交易量;本篇额外加入方向状态以便归为买/卖两类。
基类统一描述订单通用数据,四个衍生类各自约束一种订单类型规则,结构清晰也方便后续扩展其他 DOM 操作。
在处理程序里调用 MarketBookGet,按品种返回该次事件下的所有订单对象,再交由 DOM 模型对象类打包成集合。
可以,小布盯盘对品种页的 DOM 做了买卖方向聚合,省去自己写抽象类的步骤,适合快速验证本文的模型思路。
每次 OnBookEvent 激活时输入的单一品种订单快照会作为序列的一项,连续堆叠形成该品种的深度变化时间序列。