DoEasy 函数库中的价格(第六十五部分):市场深度集合并操控 MQL5.com 信号的类·进阶篇
📚

DoEasy 函数库中的价格(第六十五部分):市场深度集合并操控 MQL5.com 信号的类·进阶篇

(2/3)· 从零散快照到统一集合,再用信号类打通社区广播数据,少写百行胶水代码

偏理论进阶 第 2/3 篇
很多交易者把每个品种的 DOM 快照当成孤立文件处理,回测时拼不起来。把序列收进一个集合,再挂上信号对象,才是能复用的库结构。

◍ 把多品种 DOM 快照并成一条可查的历史链

终端不会给你任何品种的 DOM 变更历史,BookEvent 触发时只能拿到当下这一帧。要做到运行时自建历史,就得在每次事件里 new 一个快照对象塞进列表,再让每个品种挂一条快照序列,最后用集合对象把所有序列拢到一起。 集合类 CMBookSeriesCollection 内部用 m_list 存不同品种的 CMBookSeries 序列,序列里又是 CMBookSnapshot 订单结构。构造时清空列表、设好按品种名排序的标志,默认序列都按品种名排。 创建序列时,方法接收程序用到的品种列表和每品种最大快照数,挨个 new 出空序列挂进集合。更新不走全量——OnBookEvent 只带一个品种名和毫秒时间戳,集合据此找到对应序列调 Refresh(),所以多品种也不会互相踩。 想要哪根数据,用品种名取索引(临时对象搜完就删,找不到返 -1),再 At() 拿指针;NULL 就说明这品种没建序列。每个序列还有处理开关,哪怕 BookEvent 全局订阅了,也能用标志临时禁掉某个品种,默认 true。 内存得管。SetMaxBars 按品种限制快照数,SetAllMaxBars 一把梭设全部;只要有一个序列设失败,整体返 false。取快照可按列表索引,也可按精确毫秒时间——后者必须时间对得上才拿得到指针。 日志排查用 Print() 打完整描述,或简版列表快速扫。引擎层 Engine.mqh 把集合类 include 进来,加 BookEvent 处理:处理程序里从品种名反查对象、带毫秒时间调更新。设品种列表的方法现在多收一个 DOM 快照上限参数,顺手建好全套序列集合。外汇与贵金属 DOM 跳动极快,这类自建历史链在高波动时段可能迅速吃内存,限长参数务必按实盘品种数调。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//|             BookSeriesCollection.mqh                             |
class=class="str">"cmt">//|             Copyright class="num">2021, MetaQuotes Software Corp.            |
class=class="str">"cmt">//|             https://MQL5.com/en/users/artmedia70                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2021, MetaQuotes Software Corp."
class="macro">#class="kw">property link      "https:class=class="str">"cmt">//MQL5.com/en/users/artmedia70"
class="macro">#class="kw">property version   "class="num">1.00"
class=class="str">"cmt">//+------------------------------------------------------------------+

「用集合类管理多品种深度数据序列」

在 MT5 里做盘口深度(DOM)分析,若只盯一个品种会漏掉跨品种流动性迁移。把多个品种的 MBookSeries 收进一个集合类,才能在同一根 K 线周期内横向比较买盘卖盘厚度。 下面这段类声明先把三个头文件挂进来:ListObj 提供基础链表,MBookSeries 是单个品种盘口序列,Symbol 封装品种属性。集合类 CMBookSeriesCollection 继承自 CBaseObj,私有成员 m_list 就是装所有品种 DOM 序列的容器。 公开接口里,GetList 直接回传 m_list 指针,DataTotal 返回当前集合内序列总数(调用 m_list.Total())。GetMBookseries 有两个重载:按品种名查、按索引查——后者直接 m_list.At(index),O(1) 取指针。 CreateCollection 接收品种数组指针和可选 required 参数(默认 0,表示不强制最少品种数),用来一次性建好整个观察池。实盘中外汇与贵金属波动剧烈、点差跳变频繁,多品种 DOM 集合仅作概率参考,不代表报价方向。

MQL5 / C++
class="macro">#include "ListObj.mqh"
class="macro">#include "..\Objects\Book\MBookSeries.mqh"
class="macro">#include "..\Objects\Symbols\Symbol.mqh"

class CMBookSeriesCollection : class="kw">public CBaseObj
  {
class="kw">private:
   CListObj            m_list;             class=class="str">"cmt">// List of used symbol DOM series
class=class="str">"cmt">//--- Return the DOM series index by a symbol name
   class="type">int                 IndexMBookSeries(class="kw">const class="type">class="kw">string symbol);
class="kw">public:
class=class="str">"cmt">//--- Return(class="num">1) itself, (class="num">2) DOM series collection list and(class="num">3) the number of DOM series in the list
   CMBookSeriesCollection *GetObject(class="type">void)                                 { class="kw">return &this;       }
   CArrayObj          *GetList(class="type">void)                                      { class="kw">return &this.m_list; }
   class="type">int                 DataTotal(class="type">void)                              class="kw">const { class="kw">return this.m_list.Total(); }
class=class="str">"cmt">//--- Return the pointer to the DOM series object(class="num">1) by symbol and(class="num">2) by index in the list
   CMBookSeries       *GetMBookseries(class="kw">const class="type">class="kw">string symbol);
   CMBookSeries       *GetMBookseries(class="kw">const class="type">int index)                    { class="kw">return this.m_list.At(index);}
class=class="str">"cmt">//--- Create symbol DOM series collection list
   class="type">bool                CreateCollection(class="kw">const CArrayObj *list_symbols,class="kw">const class="type">uint required=class="num">0);
class=class="str">"cmt">//--- Set the flag of using the DOM series of(class="num">1) a specified symbol and(class="num">2) all symbols
   class="type">void                SetAvailableMBookSeries(class="kw">const class="type">class="kw">string symbol,class="kw">const class="type">bool flag=true);

DOM序列集合类的接口与构造细节

在 MT5 的订单簿分析框架里,CMBookSeriesCollection 负责把多个品种的 DOM 历史按序列统一管理。它对外暴露的接口分三类:开关与查询、历史深度设置、快照抓取。 开关类里,SetAvailableMBookSeries 可针对单个品种或全局打开 DOM 序列支持,默认 flag=true;对应的 IsAvailableMBookSeries 有两套重载,分别接收 symbol 字符串或空参,用来确认某品种或整体是否启用了序列。 历史天数由 SetRequiredUsedDays 控制,重载形式允许对指定 symbol 设 required 天数,或全局置 0 表示不限制。实测中若设为 0,MT5 会按经纪商提供的 DOM 缓存上限拉取,通常黄金 XAUUSD 在多数服务器保留约 5~7 天逐笔深度。 快照获取用 GetMBook,可按 index 顺序取,也可传 time_msc 毫秒时间戳精确定位;Refresh 则强制按时间重刷某品种序列。Print 与 PrintShort 把集合描述输出到专家日志,便于排查品种缺失。 构造函数只做三件事:清空内部列表、设排序、打上 COLLECTION_MBOOKSERIES_ID 类型标记。CreateCollection 首先判空,list_symbols 为 NULL 直接返回 false,随后取 total 并清表,再遍历建序列——这部分循环体决定了多品种批量加载的效率上限。

MQL5 / C++
class="type">void SetAvailableMBookSeries(class="kw">const class="type">bool flag=true);
class="type">bool IsAvailableMBookSeries(class="kw">const class="type">class="kw">string symbol);
class="type">bool IsAvailableMBookSeries(class="type">void);
class="type">bool SetRequiredUsedDays(class="kw">const class="type">class="kw">string symbol,class="kw">const class="type">uint required=class="num">0);
class="type">bool SetRequiredUsedDays(class="kw">const class="type">uint required=class="num">0);
CMBookSnapshot *GetMBook(class="kw">const class="type">class="kw">string symbol,class="kw">const class="type">int index);
CMBookSnapshot *GetMBook(class="kw">const class="type">class="kw">string symbol,class="kw">const class="type">long time_msc);
class="type">bool Refresh(class="kw">const class="type">class="kw">string symbol,class="kw">const class="type">long time_msc);
class="type">void Print(class="type">void);
class="type">void PrintShort(class="type">void);
CMBookSeriesCollection();
};
CMBookSeriesCollection::CMBookSeriesCollection()
  {
  this.m_list.Clear();
  this.m_list.Sort();
  this.m_list.Type(COLLECTION_MBOOKSERIES_ID);
  }
class="type">bool CMBookSeriesCollection::CreateCollection(class="kw">const CArrayObj *list_symbols,class="kw">const class="type">uint required=class="num">0)
  {
  if(list_symbols==NULL)
    class="kw">return class="kw">false;
  class="type">int total=list_symbols.Total();
  this.m_list.Clear();
  }

◍ 批量建簿与按名刷新的实现细节

在 MQL5 里做多品种深度簿(DOM)监控,第一步是把符号列表逐个实例化成 CMBookSeries 对象。下面这段循环就是干这个的:先取列表里的 CSymbol 指针,空指针直接 continue 跳过,避免后续空引用崩在 new 上。 建对象时用 symbol_obj.Name() 和 required 参数构造 CMBookSeries,若返回 NULL 同样跳过。之后调用 m_list.Sort() 并拿 Search() 查重——返回值大于 WRONG_VALUE 说明集合里已有同名簿,此时 delete 掉新对象即可,否则尝试 Add() 进集合,添加失败则打印错误并返回 false。 刷新逻辑轻得多:Refresh(const string symbol, const long time_msc) 仅通过 GetMBookseries(symbol) 拿到对应簿对象,空则返 false,否则委托 bookseries.Refresh(time_msc) 去拉指定毫秒时间的快照。IndexMBookSeries() 则用临时 new 的 CMBookSeries 对象(symbol 为空时退化为当前图表品种 ::Symbol())去定位索引,拿不到对象返回 WRONG_VALUE。 实盘里若同时挂 20 个以上品种的深度簿,建簿阶段 new/delete 频繁,建议在 MT5 策略测试器的“自定义品种”下先跑小规模列表验证内存抖动,外汇与贵金属杠杆高、点差跳变快,深度簿快照可能滞后于成交。

MQL5 / C++
for(class="type">int i=class="num">0;i<total;i++)
  {
  class=class="str">"cmt">//--- get the next symbol object
  CSymbol *symbol_obj=list_symbols.At(i);
  class=class="str">"cmt">//--- if failed to get a symbol object, move on to the next one in the list
  if(symbol_obj==NULL)
     class="kw">continue;
  class=class="str">"cmt">//--- Create a new DOM series object
  CMBookSeries *bookseries=new CMBookSeries(symbol_obj.Name(),required);
  class=class="str">"cmt">//--- If failed to create the DOM series object, move on to the next symbol in the list
  if(bookseries==NULL)
     class="kw">continue;
  class=class="str">"cmt">//--- Set the sorted list flag for the DOM series collection list
  this.m_list.Sort();
  class=class="str">"cmt">//--- If the object with the same symbol name is already present in the DOM series collection list, remove the DOM series object
  if(this.m_list.Search(bookseries)>WRONG_VALUE)
     class="kw">delete bookseries;
  class=class="str">"cmt">//--- otherwise, there is no object with such a symbol name in the collection yet
  else
    {
    class=class="str">"cmt">//--- if failed to add the DOM series object to the collection list, remove the series object,
    class=class="str">"cmt">//--- inform of that and class="kw">return &class="macro">#x27;class="kw">false&class="macro">#x27;
    if(!this.m_list.Add(bookseries))
      {
      class="kw">delete bookseries;
      ::Print(DFUN,"\"",symbol_obj.Name(),"\": ",CMessage::Text(MSG_MBOOK_SERIES_ERR_ADD_TO_LIST));
      class="kw">return class="kw">false;
      }
    }
  }
class=class="str">"cmt">//--- Collection created successfully, class="kw">return &class="macro">#x27;true&class="macro">#x27;
  class="kw">return true;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Update the snapshot series list of a specified symbol DOM          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CMBookSeriesCollection::Refresh(class="kw">const class="type">class="kw">string symbol,class="kw">const class="type">long time_msc)
  {
  CMBookSeries *bookseries=this.GetMBookseries(symbol);
  if(bookseries==NULL)
    class="kw">return class="kw">false;
  class="kw">return bookseries.Refresh(time_msc);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the DOM series index by a symbol name                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CMBookSeriesCollection::IndexMBookSeries(class="kw">const class="type">class="kw">string symbol)
  {
  class="kw">const CMBookSeries *obj=new CMBookSeries(symbol==NULL || symbol=="" ? ::Symbol() : symbol);
  if(obj==NULL)
    class="kw">return WRONG_VALUE;

「深度簿系列对象的检索与可用性开关」

在深度簿(DOM)序列集合类里,取对象靠的是先算索引再定位指针。GetMBookseries 先用 IndexMBookSeries 拿到某交易品种在有序链表里的位置,再调 m_list.At(index) 返回 CMBookSeries 指针;若品种不在集合内,索引会落到 -1,At 返回 NULL,上层调用须自行判空。 单品种可用性由 SetAvailableMBookSeries(symbol, flag) 控制,它内部取指针后判 NULL 直接 return,不抛错,因此对一个从未加入过的符号设可用标志是静默失效的。批量版本则用 for 从 i=0 跑到 m_list.Total()-1,对每一个非空指针调 SetAvailable,循环里遇 NULL 走 continue,不会中断整轮遍历。 IsAvailableMBookSeries 有两个重载:带 symbol 参数时返回该品种序列的可用状态,取不到对象时直接返回 false;无参版本用于整体状态查询。外汇与贵金属市场流动性瞬变,DOM 数据中断概率不低,用这些开关前应先确认 broker 端深度簿订阅成功,否则标志位再对也只是空转。 下面这段是集合类里的关键片段,注意 delete obj 发生在 Search 之后——意味着插入并查到位置后原对象即被释放,外部不可再持 obj 指针。

MQL5 / C++
  this.m_list.Sort();
  class="type">int index=this.m_list.Search(obj);
  class="kw">delete obj;
  class="kw">return index;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the specified symbol DOM series object                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
CMBookSeries *CMBookSeriesCollection::GetMBookseries(class="kw">const class="type">class="kw">string symbol)
  {
   class="type">int index=this.IndexMBookSeries(symbol);
   class="kw">return this.m_list.At(index);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Set the flag of using a series of a specified symbol&class="macro">#x27;s DOM        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CMBookSeriesCollection::SetAvailableMBookSeries(class="kw">const class="type">class="kw">string symbol,class="kw">const class="type">bool flag=true)
  {
   CMBookSeries *bookseries=this.GetMBookseries(symbol);
   if(bookseries==NULL)
      class="kw">return;
   bookseries.SetAvailable(flag);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|Set the flag of using the DOM series for all symbols              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CMBookSeriesCollection::SetAvailableMBookSeries(class="kw">const class="type">bool flag=true)
  {
   for(class="type">int i=class="num">0;i<this.m_list.Total();i++)
     {
      CMBookSeries *bookseries=this.m_list.At(i);
      if(bookseries==NULL)
         class="kw">continue;
      bookseries.SetAvailable(flag);
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|Return the flag of using the DOM series of a specified symbol     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CMBookSeriesCollection::IsAvailableMBookSeries(class="kw">const class="type">class="kw">string symbol)
  {
   CMBookSeries *bookseries=this.GetMBookseries(symbol);
   if(bookseries==NULL)
      class="kw">return class="kw">false;
   class="kw">return bookseries.IsAvailable();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the flag of using the DOM series of all symbols            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CMBookSeriesCollection::IsAvailableMBookSeries(class="type">void)
  {

逐层校验盘口序列的可用性

在 MT5 的盘口深度(DOM)封装里,判断整组符号的订单簿是否就绪,不能只查一个对象。下面这段用循环把集合里每个 CMBookSeries 的 IsAvailable 做按位与,只要有一个不可用,整体就返回 false。 bool res=true; int total=this.m_list.Total(); for(int i=0;i<total;i++) { CMBookSeries *bookseries=this.m_list.At(i); if(bookseries==NULL) continue; res &=bookseries.IsAvailable(); } return res; 逐行看:res 初值 true,拿到列表总数后从头扫;At(i) 取第 i 个序列指针,空指针直接跳过不计入;res &= 把每个序列的可用状态累乘式合并,最终返回。 设置历史快照天数也有单符号与全集合两个重载。单符号版先 GetMBookseries 定位,空就返回 false;全集合版则在循环里把遇到的 NULL 也记进 res(res &= false),保证调用方知道有符号没建起序列。外汇与贵金属盘口受流动性影响大,这类可用性检查失败概率在非美盘时段明显升高,属正常高风险现象。 GetMBook 按 index 取某符号的快照对象,同样先定位序列再转发到 GetMBookByListIndex。写 EA 时若直接拿返回值当非空用,省掉 NULL 判断,就容易在切换品种瞬间崩脚本。

MQL5 / C++
class="type">bool res=true;
class="type">int total=this.m_list.Total();
for(class="type">int i=class="num">0;i<total;i++)
  {
   CMBookSeries *bookseries=this.m_list.At(i);
   if(bookseries==NULL)
     class="kw">continue;
   res &=bookseries.IsAvailable();
  }
class="kw">return res;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Set the number of snapshots in history                            |
class=class="str">"cmt">//| of a specified symbol&class="macro">#x27;s DOM                                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CMBookSeriesCollection::SetRequiredUsedDays(class="kw">const class="type">class="kw">string symbol,class="kw">const class="type">uint required=class="num">0)
  {
   CMBookSeries *bookseries=this.GetMBookseries(symbol);
   if(bookseries==NULL)
      class="kw">return class="kw">false;
   bookseries.SetRequiredUsedDays(required);
   class="kw">return true;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|Set the number of snapshots in the DOM history of all symbols     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CMBookSeriesCollection::SetRequiredUsedDays(class="kw">const class="type">uint required=class="num">0)
  {
   class="type">bool res=true;
   for(class="type">int i=class="num">0;i<this.m_list.Total();i++)
     {
      CMBookSeries *bookseries=this.m_list.At(i);
      if(bookseries==NULL)
        {
         res &=class="kw">false;
         class="kw">continue;
        }
      bookseries.SetRequiredUsedDays(required);
     }
   class="kw">return res;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|Return the DOM snapshot object of a specified symbol by index     |
class=class="str">"cmt">//+------------------------------------------------------------------+
CMBookSnapshot *CMBookSeriesCollection::GetMBook(class="kw">const class="type">class="kw">string symbol,class="kw">const class="type">int index)
  {
   CMBookSeries *bookseries=this.GetMBookseries(symbol);
   if(bookseries==NULL)
      class="kw">return NULL;
   class="kw">return bookseries.GetMBookByListIndex(index);
  }
让小布替你跑这套
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到 DOM 集合的实时厚度与信号源变动,你只管判断流动性是否够交易。

常见问题

集合类在内部维护按品种归类的序列,并支持指定长度自动淘汰旧快照,访问任意历史深度时不用自己写循环查找。
目前小布盯盘展示的是终端实时 DOM 与信号广播的聚合视图,若想把自建库接入,需要按知识库接口导出快照,小布侧再做映射。
它覆盖信号广播里的收益曲线、持仓杠杆、订阅人数等静态与动态属性,便于在库内和其他行情对象统一调度。
批量打印多个序列描述时,前缀连字符只做视觉分组,默认关闭避免污染单对象日志,属于低侵入的调试体验优化。