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

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

(2/3)· 从单次 OnBookEvent 到数万帧 DOM 快照链,解决盘口状态难复盘的工程痛点

案例拆解 第 2/3 篇
很多交易者把 MarketBookGet 返回的数组当成一次性画面,事件一闪就丢了上下文。把每次盘口变动固化成带时间戳的快照,才有可能在事后还原流动性是怎么撤单、挂单的。

◍ 从深度报单里筛最大挂量的那一行

做价格行为分析时,DOM(深度报价)里哪一档挂单量最大,往往暗示着短线支撑或阻力的密集区。下面两个重载函数就是在一组 CMarketBookOrd 对象里,按整数或实数属性找出最大值所在的索引。 第一个 FindMBookMax 接收 ENUM_MBOOK_ORD_PROP_INTEGER 类型属性,比如挂单笔数;第二个接收 ENUM_MBOOK_ORD_PROP_DOUBLE,适合挂单体积这类带小数的字段。两者都从 index=0 起步,从 i=1 开始向后比,遇到更大的就更新 index,最终返回最大属性对应的列表下标。 若传入空指针或列表总数为 0,函数直接回 WRONG_VALUE(-1),调用层要用这个值判断查找失败。外汇与贵金属市场流动性瞬变,DOM 快照有毫秒级偏差,这类极值筛选只能作为概率参考,不能视为确定价位。 顺带看前面的 ByMBookProperty:它按字符串属性加比较模式从源列表挑对象,新建的 CArrayObj 设了 FreeMode(false),意思是析构时不自动删元素,避免把源列表里的 CMarketBookOrd 一起释放掉。

MQL5 / C++
CArrayObj *CSelect::ByMBookProperty(CArrayObj *list_source,ENUM_MBOOK_ORD_PROP_STRING class="kw">property,class="type">class="kw">string value,ENUM_COMPARER_TYPE mode)
  {
   if(list_ class="kw">return NULL;            class=class="str">"cmt">// 源列表为空直接返回空
   CArrayObj *list=new CArrayObj();              class=class="str">"cmt">// 新建结果列表
   if(list==NULL) class="kw">return NULL;                   class=class="str">"cmt">// 内存分配失败返回空
   list.FreeMode(false);                         class=class="str">"cmt">// 不自动释放元素,保护源对象
   ListStorage.Add(list);                        class=class="str">"cmt">// 登记到存储容器便于统一管理
   for(class="type">int i=class="num">0; i<list_source.Total(); i++)      class=class="str">"cmt">// 遍历源列表每一个元素
     {
      CMarketBookOrd *obj=list_source.At(i);     class=class="str">"cmt">// 取第 i 个深度报单对象
      if(!obj.SupportProperty(class="kw">property)) class="kw">continue;class=class="str">"cmt">// 不支持该属性则跳过
      class="type">class="kw">string obj_prop=obj.GetProperty(class="kw">property); class=class="str">"cmt">// 读取对象的字符串属性
      if(CompareValues(obj_prop,value,mode)) list.Add(obj); class=class="str">"cmt">// 按比较模式匹配则加入结果
     }
   class="kw">return list;                                  class=class="str">"cmt">// 返回筛选后的列表
  }
class="type">int CSelect::FindMBookMax(CArrayObj *list_source,ENUM_MBOOK_ORD_PROP_INTEGER class="kw">property)
  {
   if(list_ class="kw">return WRONG_VALUE;     class=class="str">"cmt">// 空指针返回错误值
   class="type">int index=class="num">0;                                  class=class="str">"cmt">// 假设第0个最大
   CMarketBookOrd *max_obj=NULL;                 class=class="str">"cmt">// 暂存最大对象
   class="type">int total=list_source.Total();                class=class="str">"cmt">// 列表总长度
   if(total==class="num">0) class="kw">return WRONG_VALUE;              class=class="str">"cmt">// 空列表返回错误值
   for(class="type">int i=class="num">1; i<total; i++)                    class=class="str">"cmt">// 从第二个开始比
     {
      CMarketBookOrd *obj=list_source.At(i);     class=class="str">"cmt">// 当前对象
      class="type">long obj1_prop=obj.GetProperty(class="kw">property);  class=class="str">"cmt">// 当前整数属性
      max_obj=list_source.At(index);             class=class="str">"cmt">// 暂存的最大对象
      class="type">long obj2_prop=max_obj.GetProperty(class="kw">property);class=class="str">"cmt">// 最大对象的整数属性
      if(CompareValues(obj1_prop,obj2_prop,MORE)) index=i; class=class="str">"cmt">// 更大则更新下标
     }
   class="kw">return index;                                 class=class="str">"cmt">// 返回最大属性所在索引
  }
class="type">int CSelect::FindMBookMax(CArrayObj *list_source,ENUM_MBOOK_ORD_PROP_DOUBLE class="kw">property)
  {
   if(list_ class="kw">return WRONG_VALUE;     class=class="str">"cmt">// 空指针返回错误值
   class="type">int index=class="num">0;                                  class=class="str">"cmt">// 假设第0个最大
   CMarketBookOrd *max_obj=NULL;                 class=class="str">"cmt">// 暂存最大对象
   class="type">int total=list_source.Total();                class=class="str">"cmt">// 列表总长度
   if(total==class="num">0) class="kw">return WRONG_VALUE;              class=class="str">"cmt">// 空列表返回错误值
   for(class="type">int i=class="num">1; i<total; i++)                    class=class="str">"cmt">// 从第二个开始比
     {
      CMarketBookOrd *obj=list_source.At(i);     class=class="str">"cmt">// 当前对象
      class="type">class="kw">double obj1_prop=obj.GetProperty(class="kw">property);class=class="str">"cmt">// 当前实数属性
      max_obj=list_source.At(index);             class=class="str">"cmt">// 暂存的最大对象
      class="type">class="kw">double obj2_prop=max_obj.GetProperty(class="kw">property);class=class="str">"cmt">// 最大对象的实数属性
      if(CompareValues(obj1_prop,obj2_prop,MORE)) index=i; class=class="str">"cmt">// 更大则更新下标
     }
   class="kw">return index;                                 class=class="str">"cmt">// 返回最大属性所在索引
  }

「在深度报单列表里挑极值索引」

做盘口微观结构分析时,经常要从一整列 CMarketBookOrd 对象里捞出某个属性最大或最小的那个档位。下面这组 CSelect 成员函数就是干这个的:按字符串、整数、双精度三种属性类型分别提供 FindMBookMax / FindMBookMin 重载。 以字符串最大值查找为例,函数先判空,list_source 为 NULL 或 Total() 返回 0 都直接回 WRONG_VALUE;随后从索引 0 起步,用 CompareValues 做 MORE 比较,遇到更大的就更新 index。整数与双精度版本逻辑完全一致,只是把比较符号换成 LESS 且属性类型对应 long / double。 实盘里若想抓「卖一价离现价最远的冰山单」或「成交量最小的挂单档」,直接把对应 ENUM_MBOOK_ORD_PROP_* 传进去就能拿到列表下标,省得自己写遍历。外汇与贵金属盘口跳动快、滑点大,这类极值筛选仅作结构参考,不代表流动性方向。

MQL5 / C++
class="type">int CSelect::FindMBookMax(CArrayObj *list_source,ENUM_MBOOK_ORD_PROP_STRING class="kw">property)
  {
   if(list_ class="kw">return WRONG_VALUE;
   class="type">int index=class="num">0;
   CMarketBookOrd *max_obj=NULL;
   class="type">int total=list_source.Total();
   if(total==class="num">0) class="kw">return WRONG_VALUE;
   for(class="type">int i=class="num">1; i<total; i++)
     {
      CMarketBookOrd *obj=list_source.At(i);
      class="type">class="kw">string obj1_prop=obj.GetProperty(class="kw">property);
      max_obj=list_source.At(index);
      class="type">class="kw">string obj2_prop=max_obj.GetProperty(class="kw">property);
      if(CompareValues(obj1_prop,obj2_prop,MORE)) index=i;
     }
   class="kw">return index;
  }
class="type">int CSelect::FindMBookMin(CArrayObj* list_source,ENUM_MBOOK_ORD_PROP_INTEGER class="kw">property)
  {
   class="type">int index=class="num">0;
   CMarketBookOrd *min_obj=NULL;
   class="type">int total=list_source.Total();
   if(total==class="num">0) class="kw">return WRONG_VALUE;
   for(class="type">int i=class="num">1; i<total; i++)
     {
      CMarketBookOrd *obj=list_source.At(i);
      class="type">long obj1_prop=obj.GetProperty(class="kw">property);
      min_obj=list_source.At(index);
      class="type">long obj2_prop=min_obj.GetProperty(class="kw">property);
      if(CompareValues(obj1_prop,obj2_prop,LESS)) index=i;
     }
   class="kw">return index;
  }
class="type">int CSelect::FindMBookMin(CArrayObj* list_source,ENUM_MBOOK_ORD_PROP_DOUBLE class="kw">property)
  {
   class="type">int index=class="num">0;
   CMarketBookOrd *min_obj=NULL;
   class="type">int total=list_source.Total();
   if(total== class="num">0) class="kw">return WRONG_VALUE;
   for(class="type">int i=class="num">1; i<total; i++)
     {
      CMarketBookOrd *obj=list_source.At(i);
      class="type">class="kw">double obj1_prop=obj.GetProperty(class="kw">property);
      min_obj=list_source.At(index);
      class="type">class="kw">double obj2_prop=min_obj.GetProperty(class="kw">property);
      if(CompareValues(obj1_prop,obj2_prop,LESS)) index=i;
     }
   class="kw">return index;
  }

DOM 订单对象的极值查找与快照构造

在深度行情(DOM)分析里,定位某属性最小的挂单是常见需求。下面这个函数从订单对象数组里捞出指定字符串属性最小的那个,返回其下标;若数组为空直接给 WRONG_VALUE,避免后续越界。 int CSelect::FindMBookMin(CArrayObj* list_source,ENUM_MBOOK_ORD_PROP_STRING property) { int index=0; CMarketBookOrd *min_obj=NULL; int total=list_source.Total(); if(total==0) return WRONG_VALUE; for(int i=1; i<total; i++) { CMarketBookOrd *obj=list_source.At(i); string obj1_prop=obj.GetProperty(property); min_obj=list_source.At(index); string obj2_prop=min_obj.GetProperty(property); if(CompareValues(obj1_prop,obj2_prop,LESS)) index=i; } return index; } 逐行看:index 初始为 0,拿数组总数 total;空数组直接返回错误值。从 i=1 开始遍历,每次取第 i 个和第 index 个对象的同属性字符串,用 CompareValues 比大小,若前者 LESS 则更新 index。最终 index 就是最小属性所在位置。 订单对象本身靠参数化构造函数落地。CMarketBookOrd 构造时先抓品种精度 m_digits = SymbolInfoInteger(symbol, SYMBOL_DIGITS),再把状态、类型、整数与真实成交量、价格全部写进属性表;符号为空时回退到当前图表品种。注意订单时间不在构造参数里,统一归 DOM 快照类管,所以这里把 MBOOK_ORD_PROP_TIME_MSC 重置为 0。 PrintShort 和 Header 都带 symbol 开关参数,默认 false 不打印品种名,调用时传 true 才在日志里带上符号——做多品种盯盘时这个开关能少刷不少噪音。外汇与贵金属 DOM 跳动极快,这类封装只是降低出错概率,实盘仍属高风险,参数逻辑建议在 MT5 策略测试器里先跑一遍空单薄验证。

MQL5 / C++
class="type">int CSelect::FindMBookMin(CArrayObj* list_source,ENUM_MBOOK_ORD_PROP_STRING class="kw">property)
  {
   class="type">int index=class="num">0;
   CMarketBookOrd *min_obj=NULL;
   class="type">int total=list_source.Total();
   if(total==class="num">0) class="kw">return WRONG_VALUE;
   for(class="type">int i=class="num">1; i<total; i++)
     {
      CMarketBookOrd *obj=list_source.At(i);
      class="type">class="kw">string obj1_prop=obj.GetProperty(class="kw">property);
      min_obj=list_source.At(index);
      class="type">class="kw">string obj2_prop=min_obj.GetProperty(class="kw">property);
      if(CompareValues(obj1_prop,obj2_prop,LESS)) index=i;
     }
   class="kw">return index;
  }

CMarketBookOrd::CMarketBookOrd(class="kw">const ENUM_MBOOK_ORD_STATUS status,class="kw">const MqlBookInfo &book_info,class="kw">const class="type">class="kw">string symbol)
  {
   this.m_digits=(class="type">int)::SymbolInfoInteger(symbol,SYMBOL_DIGITS);
   this.SetProperty(MBOOK_ORD_PROP_STATUS,status);
   this.SetProperty(MBOOK_ORD_PROP_TYPE,book_info.type);
   this.SetProperty(MBOOK_ORD_PROP_VOLUME,book_info.volume);
   this.SetProperty(MBOOK_ORD_PROP_PRICE,book_info.price);
   this.SetProperty(MBOOK_ORD_PROP_VOLUME_REAL,book_info.volume_real);
   this.SetProperty(MBOOK_ORD_PROP_SYMBOL,(symbol==NULL || symbol=="" ? ::Symbol() : symbol));
   this.SetProperty(MBOOK_ORD_PROP_TIME_MSC,class="num">0);
  }

◍ 订单簿对象的日志输出与表头拼接

深度行情(Market Book)类里,Header() 方法决定了对象在日志里长什么样。基类 CMarketBookOrd::Header 只拼类型描述,symbol 参数为 true 时才追加品种名,比如传入 EURUSD 会输出 Limit "EURUSD" 这类形态。 PrintShort() 直接调 Header 后丢给终端 Print,等于一行速览。下方几个派生类(买挂单、市价买、卖挂单)重写了 Header,在类型文字后硬拼了价格与成交量:价格用 Digits() 控制小数位,成交量用 DoubleToString(...,2) 固定两位小数。 实盘里把 symbol 设 true 跑一趟,能在专家日志里直接看到 Buy: 1.08562 [1.20] 这样的行。外汇与贵金属杠杆高,盘口数据瞬变,这类打印仅作调试,不代表任何方向判断。

MQL5 / C++
class="type">class="kw">string CMarketBookOrd::Header(class="kw">const class="type">bool symbol=false)
  {
   class="kw">return this.TypeDescription()+(symbol ? " \""+this.Symbol()+"\"" : "");
  }
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 CMarketBookOrd::PrintShort(class="kw">const class="type">bool symbol=false)
  {
   ::Print(this.Header(symbol));
  }
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 CMarketBookBuy::Header(class="kw">const class="type">bool symbol=false)
  {
   class="kw">return CMessage::Text(MSG_MBOOK_ORD_TYPE_BUY)+(symbol ? " \""+this.Symbol() : "")+
          ": "+::DoubleToString(this.Price(),this.Digits())+" ["+::DoubleToString(this.VolumeReal(),class="num">2)+"]";
  }
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 CMarketBookBuyMarket::Header(class="kw">const class="type">bool symbol=false)
  {
   class="kw">return CMessage::Text(MSG_MBOOK_ORD_TYPE_BUY_MARKET)+(symbol ? " \""+this.Symbol() : "")+
          ": "+::DoubleToString(this.Price(),this.Digits())+" ["+::DoubleToString(this.VolumeReal(),class="num">2)+"]";
  }
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 CMarketBookSell::Header(class="kw">const class="type">bool symbol=false)
  {
   class="kw">return CMessage::Text(MSG_MBOOK_ORD_TYPE_SELL)+(symbol ? " \""+this.Symbol() : "")+
          ": "+::DoubleToString(this.Price(),this.Digits())+" ["+::DoubleToString(this.VolumeReal(),class="num">2)+"]";
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

「正文」

<span class="comment">//+------------------------------------------------------------------+</span>

<span class="comment">//Return the object short name &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>

<span class="comment">//+------------------------------------------------------------------+</span> <span class="keyword">string</span> CMarketBookSellMarket::Header(<span style="background-color:rgb(255, 242, 153);"><span class="keyword">const</span> <span class="keyword">bool</span> symbol=<span class="macro">false</span></span>) &nbsp;&nbsp;{ &nbsp;&nbsp; <span class="keyword">return</span> CMessage::Text(MSG_MBOOK_ORD_TYPE_SELL_MARKET)+<span style="background-color:rgb(255, 242, 153);">(symbol ? <span class="string">" \""</span>+<span class="keyword">this</span>.<span class="functions">Symbol</span>() : <span class="string">""</span>)</span>+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="string">": "</span>+::<span class="functions">DoubleToString</span>(<span class="keyword">this</span>.Price(),<span class="keyword">this</span>.<span class="functions">Digits</span>())+<span class="string">" ["</span>+::<span class="functions">DoubleToString</span>(<span class="keyword">this</span>.VolumeReal(),<span class="number">2</span>)+<span class="string">"]"</span>; &nbsp;&nbsp;} <span class="comment">//+--------------------------

把每次盘口变动存成可回溯的快照对象

OnBookEvent() 每触发一次,就拿到一份 MqlBookInfo 数组,也就是那一刻的市场深度(DOM)。如果只当场读、不留存,事后想统计买卖挂单厚度变化就不可能。做法是把这次快照封装成 CMBookSnapshot 对象,里面挂一个 CArrayObj 列表,列表里每一项都是 CMarketBookOrd 的派生类实例(买限、卖限、市价买、市价卖等)。 类放在 \MQL5\Include\DoEasy\Objects\Book\MarketBookSnapshot.mqh,基类用 CBaseObj,同时把四种具体盘口订单类的头文件 include 进来。私密段只存四个东西:品种名、快照毫秒时间、品种小数位、订单对象列表。公开段给的是按属性取列表、比较两个快照、排序搜索这些标准库对象通用能力。 参数化构造函数接收 MqlBookInfo 数组后,逐条 new 出对应类型的订单对象塞进 m_list;默认构造则留白,方便先建空壳再补属性做列表查找。由于 OnBookEvent 是同毫秒收到整批,我额外写了一个给列表里所有订单统一打时间戳的方法——循环里挨个取对象、写 m_time,保证一个快照内的订单时间完全一致,逻辑上才自洽。 日志侧有三个方法够用:Brief() 拼一句含品种和描述的短串;PrintShort() 打「对象名+毫秒时间」;Print() 先打快照标头,再循环列每个订单描述。实盘里黄金 XAUUSD 盘口跳动频繁,这类快照对象配合后续序列类,才可能做出挂单撤单频率的真实统计,但外汇贵金属杠杆高,任何基于盘口的分析都只是概率参考。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 MarketBookSnapshot.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 "..\..\Services\Select.mqh"
class="macro">#include "MarketBookBuy.mqh"
class="macro">#include "MarketBookSell.mqh"
class="macro">#include "MarketBookBuyMarket.mqh"
class="macro">#include "MarketBookSellMarket.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| "DOM snapshot" class                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CMBookSnapshot : class="kw">public CBaseObj
  {
class="kw">private:
   class="type">class="kw">string            m_symbol;            class=class="str">"cmt">// Symbol
   class="type">long              m_time;              class=class="str">"cmt">// Snapshot time
   class="type">int               m_digits;            class=class="str">"cmt">// Symbol&class="macro">#x27;s Digits
   CArrayObj         m_list;              class=class="str">"cmt">// List of DOM order objects
class="kw">public:
class=class="str">"cmt">//--- Return(class="num">1) itself and(class="num">2) the list of DOM order objects
把盘口回放交给小布盯盘
这些 DOM 快照序列的诊断逻辑小布盯盘的 AIGC 已内置,打开对应品种页即可看到盘口厚度随时间的回放,你只管判断流动性异动是否值得跟。

常见问题

在本文的封装逻辑里,每次处理程序激活并调用 MarketBookGet 后,都会构造一个快照对象并压入该品种的序列列表,因此逐帧是可追溯的。
文中示例用约 20 万帧覆盖一两个交易日,实际可按品种活跃度和内存权衡,参数化设置最大快照数更稳。
可以,小布盯盘对应品种页已内置盘口快照回放与厚度统计,不需要你自己从零搭 DoEasy 那套类结构。
订单本身没有时间字段,但快照有接收时刻;把毫秒级时间挂到订单属性上,序列搜索和排序就能直接按时间轴走。
本篇先做单品种快照类和序列类,集合类在下一篇才封装,相当于每个品种一条链、多条链再归到一个总容器。