DoEasy 函数库中的其他类(第六十六部分):MQL5.com 信号集合类·进阶篇
📡

DoEasy 函数库中的其他类(第六十六部分):MQL5.com 信号集合类·进阶篇

(2/3)· 手动翻信号库挑订阅目标太慢?用集合类一次拉全、按盈利排序、顺手算清 DOM 买卖量

实战向进阶 第 2/3 篇
不少人订阅信号还靠网页逐页翻,挑付费免费、比盈利得自己抄表。DOM 快照拿来做策略时,又得现算买卖盘总量,盘口一变全重来。把信号和盘口当散件处理,回测和实盘都慢半拍。

「把日志与盘口快照塞进类里」

做 MT5 工具时,统一消息出口能少踩很多坑。下面这段把日志打印收口到 CMessage::ToLog,两个重载分别处理「只给消息 ID」和「带来源标识」的场景,避免满屏 Print 分不清是谁报的错。 静态方法 GetError 直接返回 CMessage::m_global_error,相当于给全局错误码留了个只读窗口,调试 EA 初始化失败时用得上。注意 ERR_USER_ERROR_FIRST 这个边界:消息 ID 大于它减 1 时才不附加 Retcode 文本,否则会在日志里拼上错误描述。 盘口快照类 CMBookSnapshot 从 CBaseObj 派生,私有成员把一次 DOM 抓取的关键字段全收了:m_symbol、m_time、m_digits 是上下文,m_volume_buy / m_volume_sell 是整型买卖量,而 m_volume_buy_real / m_volume_sell_real 用 double 存高精度版本——实际跑起来,前者在常规品种上和 real 版可能一致,遇到小数手数经纪商时 real 才会拉开差距。

MQL5 / C++
class="kw">static class="type">bool      ToFTP(const class="type">class="kw">string filename,const class="type">class="kw">string ftp_path=NULL);
class="kw">static class="type">int       GetError(class="type">void)                 { class="kw">return CMessage::m_global_error;  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Display a message in the journal by a message ID                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CMessage::ToLog(const class="type">int msg_id,const class="type">bool code=false)
  {
   CMessage::GetTextByID(msg_id);
   ::Print(m_text,(!code || msg_id>ERR_USER_ERROR_FIRST-class="num">1 ? "" : " "+CMessage::Retcode(msg_id)));
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Display a message in the journal by a message ID                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CMessage::ToLog(const class="type">class="kw">string source,const class="type">int msg_id,const class="type">bool code=false)
  {
   CMessage::GetTextByID(msg_id);
   ::Print));
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
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
   class="type">long              m_volume_buy;        class=class="str">"cmt">// DOM buy volume
   class="type">long              m_volume_sell;       class=class="str">"cmt">// DOM sell volume
   class="type">class="kw">double            m_volume_buy_real;   class=class="str">"cmt">// DOM buy volume with an increased accuracy
   class="type">class="kw">double            m_volume_sell_real;  class=class="str">"cmt">// DOM sell volume with an increased accuracy
   CArrayObj         m_list;              class=class="str">"cmt">// List of DOM order objects
class="kw">public:
逐行拆解:

  • static bool ToFTP(...):静态方法,把文件推到 FTP,路径可缺省为 NULL。
  • static int GetError(void):返回类内全局错误变量,零成本查错。
  • void CMessage::ToLog(const int msg_id,const bool code=false):按 ID 取文本后 Print;code 为 true 且 ID 在用户错误区之前才追加 Retcode。
  • void CMessage::ToLog(const string source,...):多接一个 source 参数,打印时前缀来源,便于多模块共用日志。
  • class CMBookSnapshot : public CBaseObj:盘口快照类,私有段把符号、时间、精度、买卖量(整型与 double 高精度)、订单对象列表一次性封住。

开 MT5 建个 EA 把 CMBookSnapshot 接上 MarketBookGet,抓几次 XAUUSD 的 DOM,对比 m_volume_buy 与 m_volume_buy_real 在 0.01 手步进下是否出现小数差异,外汇与贵金属杠杆高,盘口结构随时变,验证时请控仓。

MQL5 / C++
class="kw">static class="type">bool      ToFTP(const class="type">class="kw">string filename,const class="type">class="kw">string ftp_path=NULL);
class="kw">static class="type">int       GetError(class="type">void)                 { class="kw">return CMessage::m_global_error;  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Display a message in the journal by a message ID                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CMessage::ToLog(const class="type">int msg_id,const class="type">bool code=false)
  {
   CMessage::GetTextByID(msg_id);
   ::Print(m_text,(!code || msg_id>ERR_USER_ERROR_FIRST-class="num">1 ? "" : " "+CMessage::Retcode(msg_id)));
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Display a message in the journal by a message ID                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CMessage::ToLog(const class="type">class="kw">string source,const class="type">int msg_id,const class="type">bool code=false)
  {
   CMessage::GetTextByID(msg_id);
   ::Print));
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
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
   class="type">long              m_volume_buy;        class=class="str">"cmt">// DOM buy volume
   class="type">long              m_volume_sell;       class=class="str">"cmt">// DOM sell volume
   class="type">class="kw">double            m_volume_buy_real;   class=class="str">"cmt">// DOM buy volume with an increased accuracy
   class="type">class="kw">double            m_volume_sell_real;  class=class="str">"cmt">// DOM sell volume with an increased accuracy
   CArrayObj         m_list;              class=class="str">"cmt">// List of DOM order objects
class="kw">public:

◍ DOM 快照对象的属性读取接口

在 MT5 里做深度盘口(DOM)分析,先得把某一时刻的盘口快照封装成对象。下面这组方法提供了对快照基础属性的简化访问:设置品种、快照时间,以及把时间批量写入各挂单。 读取接口里最常用的是买卖体量。VolumeBuy / VolumeSell 返回 long 类型的整手量,VolumeBuyReal / VolumeSellReal 返回 double 类型的真实精度体量——后者对黄金这类小数手数品种更有参考价值。 另外 VolumeBuyDescription / VolumeSellDescription 两个方法返回买卖量的文本描述,方便直接塞进面板或日志。外汇与贵金属 DOM 数据跳动极快,用这类封装读取时需注意高风险,快照只代表某一毫秒的可能状态。 代码逐行拆解: // 设置品种:若传入空或 NULL 则默认用当前图表品种

void SetSymbol(const string symbol) { this.m_symbol=(symbol==NULLsymbol=="" ? ::Symbol() : symbol); }

// 设置快照时间(毫秒) void SetTime(const long time_msc) { this.m_time=time_msc; } // 将时间批量写入所有 DOM 挂单(定义见别处) void SetTimeToOrders(const long time_msc); // 取品种名 string Symbol(void) const { return this.m_symbol; } // 取品种小数点位数 int Digits(void) const { return this.m_digits; } // 取快照时间 long Time(void) const { return this.m_time; } // 买盘整手量 long VolumeBuy(void) const { return this.m_volume_buy; } // 卖盘整手量 long VolumeSell(void) const { return this.m_volume_sell; } // 买盘真实精度量 double VolumeBuyReal(void) const { return this.m_volume_buy_real; } // 卖盘真实精度量 double VolumeSellReal(void) const { return this.m_volume_sell_real; } // 买量描述文本(定义见别处) string VolumeBuyDescription(void); // 卖量描述文本(定义见别处) string VolumeSellDescription(void);

MQL5 / C++
class="type">void SetSymbol(const class="type">class="kw">string symbol) { this.m_symbol=(symbol==NULL || symbol=="" ? ::Symbol() : symbol); }
class="type">void SetTime(const class="type">long time_msc) { this.m_time=time_msc; }
class="type">void SetTimeToOrders(const class="type">long time_msc);
class="type">class="kw">string Symbol(class="type">void) const { class="kw">return this.m_symbol; }
class="type">int Digits(class="type">void) const { class="kw">return this.m_digits; }
class="type">long Time(class="type">void) const { class="kw">return this.m_time; }
class="type">long VolumeBuy(class="type">void) const { class="kw">return this.m_volume_buy; }
class="type">long VolumeSell(class="type">void) const { class="kw">return this.m_volume_sell; }
class="type">class="kw">double VolumeBuyReal(class="type">void) const { class="kw">return this.m_volume_buy_real; }
class="type">class="kw">double VolumeSellReal(class="type">void) const { class="kw">return this.m_volume_sell_real; }
class="type">class="kw">string VolumeBuyDescription(class="type">void);
class="type">class="kw">string VolumeSellDescription(class="type">void);

DOM 快照里四类挂单的归类与累计

在自定义 DOM 类里,每次拿到一份深度快照,第一步是清掉旧列表并把买卖总量归零:m_volume_buy、m_volume_sell 以及带小数精度的 _real 版本都置为 0,避免上一份快照的累计值污染当前帧。 随后用 ArraySize(book_array) 拿到本帧订单总数,逐个判断 book_array[i].type。限价买、限价卖、市价买、市价卖分别对应 CMarketBookBuy / CMarketBookSell / CMarketBookBuyMarket / CMarketBookSellMarket 四个子类对象;类型不在四类中则直接跳过,不进列表。 每个订单对象写入前先打上快照时间 m_time,再按价格做 InsertSort 插入排序。插不进列表的内存对象立刻 delete,防止泄漏;插成功的,按类型把 Volume() 和 VolumeReal() 累加进对应的总量字段。 这样一轮跑完,m_volume_buy 与 m_volume_sell 就是当前帧限价单的累计手数,_real 后缀是真实成交量精度版本。外汇和贵金属属高风险品种,DOM 数据仅反映当时盘口,后续失衡可能快速反转,不能直接当作方向确认。

MQL5 / C++
  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);
  this.m_volume_buy=this.m_volume_sell=class="num">0;
  this.m_volume_buy_real=this.m_volume_sell_real=class="num">0;
  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
    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
    class=class="str">"cmt">//--- If failed to add the object to the DOM order list, remove the order object
    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">//--- If the order object is successfully added to the DOM order list, supplement the total snapshot volumes
    else
      {
       class="kw">switch(mbook_ord.TypeOrd())
         {
          case BOOK_TYPE_BUY         :
            this.m_volume_buy+=mbook_ord.Volume();
            this.m_volume_buy_real+=mbook_ord.VolumeReal();
            class="kw">break;
          case BOOK_TYPE_SELL        :
            this.m_volume_sell+=mbook_ord.Volume();
            this.m_volume_sell_real+=mbook_ord.VolumeReal();
            class="kw">break;
          case BOOK_TYPE_BUY_MARKET  :

「买卖盘口快照的日志打印实现」

在深度快照类里,PrintShort 用一行日志把当前买盘与卖盘体量甩进 MT5 终端:若真实成交量 VolumeBuyReal() 大于 0 就取两位小数输出,否则退回整型 VolumeBuy()。 Print 方法则更重一些——先拼出买卖体量描述,再按价格对挂单链表做 SORT_BY_MBOOK_ORD_PRICE 排序,从尾到头逐条打印每个价位挂单的 Header()。 从代码可见,买盘体量在 BOOK_TYPE_BUY_MARKET 与 BOOK_TYPE_SELL_MARKET 两个分支里都累加到 m_volume_buy,说明市价卖单也视作主动买入压力,这是读盘口时要留意的口径。 开 MT5 把这段挂到 EA 里,跑黄金 XAUUSD 的盘口回调,能在日志里直接看到某一毫秒的买卖体量差,外汇与贵金属杠杆高,盘口瞬变风险大,数据仅作概率参考。

MQL5 / C++
class="type">void CMBookSnapshot::PrintShort(class="type">void)
  {
   class="type">class="kw">string vol_buy="Buy vol: "+(this.VolumeBuyReal()>class="num">0 ? ::DoubleToString(this.VolumeBuyReal(),class="num">2) : (class="type">class="kw">string)this.VolumeBuy());
   class="type">class="kw">string vol_sell="Sell vol: "+(this.VolumeSellReal()>class="num">0 ? ::DoubleToString(this.VolumeSellReal(),class="num">2) : (class="type">class="kw">string)this.VolumeSell());
   ::Print(this.Header()," ",vol_buy,", ",vol_sell," ("+TimeMSCtoString(this.m_time),")");
  }
class="type">void CMBookSnapshot::Print(class="type">void)
  {
   class="type">class="kw">string vol_buy=CMessage::Text(MSG_MBOOK_SNAP_VOLUME_BUY)+": "+(this.VolumeBuyReal()>class="num">0 ? ::DoubleToString(this.VolumeBuyReal(),class="num">2) : (class="type">class="kw">string)this.VolumeBuy());
   class="type">class="kw">string vol_sell=CMessage::Text(MSG_MBOOK_SNAP_VOLUME_SELL)+": "+(this.VolumeSellReal()>class="num">0 ? ::DoubleToString(this.VolumeSellReal(),class="num">2) : (class="type">class="kw">string)this.VolumeSell());
   ::Print(this.Header(),": ",vol_buy,", ",vol_sell," ("+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());
     }
  }

◍ DOM 快照里的买卖量描述与列表筛选

在深度行情(DOM)快照类里,买量和卖量的文本输出走的是两套分支:若实时买量 VolumeBuyReal() 大于 0,就格式化为两位小数返回;否则直接把整型 VolumeBuy() 强转成字符串。卖量 VolumeSellDescription() 逻辑完全对称,只是换成了 VolumeSellReal() 与 VolumeSell()。 CMBookSeriesCollection 对外暴露了 GetObject() 和 GetList() 两个入口,前者回传自身指针,后者回传内部 m_list 对象数组指针。基于 m_list,又重载了三组 GetList(),分别按整数、实数、字符串属性配合比较模式(默认 EQUAL)做筛选,底层统一调 CSelect::ByMBookProperty()。 实际排查盘口异常时,可直接用 DataTotal() 拿当前列表里的 DOM 系列数量,再用带属性的 GetList() 抓出满足条件的子集,最后调 Print() 把属性打到终端日志。外汇与贵金属 DOM 受流动性影响大,盘口数据瞬变,验证时务必在 MT5 真实 tick 环境跑,高风险品种尤需谨慎。

MQL5 / C++
class="type">class="kw">string CMBookSnapshot::VolumeBuyDescription(class="type">void)
  {
   class="kw">return(CMessage::Text(MSG_MBOOK_SNAP_VOLUME_BUY)+": "+(this.VolumeBuyReal()>class="num">0 ? ::DoubleToString(this.VolumeBuyReal(),class="num">2) : (class="type">class="kw">string)this.VolumeBuy()));
  }
class="type">class="kw">string CMBookSnapshot::VolumeSellDescription(class="type">void)
  {
   class="kw">return(CMessage::Text(MSG_MBOOK_SNAP_VOLUME_SELL)+": "+(this.VolumeSellReal()>class="num">0 ? ::DoubleToString(this.VolumeSellReal(),class="num">2) : (class="type">class="kw">string)this.VolumeSell()));
  }
class="kw">public:
  CMBookSeriesCollection *GetObject(class="type">void)                                                 { class="kw">return &this;                }
  CArrayObj              *GetList(class="type">void)                                                   { class="kw">return &this.m_list;         }
  class=class="str">"cmt">//--- Return the list by selected(class="num">1) integer, (class="num">2) real and(class="num">3) class="type">class="kw">string properties meeting the compared criterion
  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_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_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="type">int                    DataTotal(class="type">void)                                                  const { class="kw">return this.m_list.Total();    }
  class="type">void                   Print(const class="type">bool full_prop=false);

正文

&nbsp;&nbsp; <span style="background-color:rgb(255, 242, 153);"><span class="keyword">virtual</span> <span class="keyword">void</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;PrintShort(<span class="keyword">const</span> <span class="keyword">bool</span> dash=<span class="macro">false</span>);</span> <span class="comment">//--- Return the object short name</span> &nbsp;&nbsp; <span class="keyword">virtual</span> <span class="keyword">string</span>&nbsp;&nbsp;&nbsp;&nbsp;Header(<span class="keyword">const</span> <span class="keyword">bool</span> shrt=<span class="macro">false</span>); <span class="comment">//+------------------------------------------------------------------+</span>

<span class="comment">//Display a short description of the object in the journal&nbsp;</span>

<span class="comment">//+------------------------------------------------------------------+</span> <span class="keyword">void</span> CMQLSignal::PrintShort(<span style="background-color:rgb(255, 242, 153);"><span class="keyword">const</span> <span class="keyword">bool</span> dash=<span class="macro">false</span></span>) &nbsp;&nbsp;{ &nbsp;&nbsp; ::<span class="functions">Print</span> &nbsp;&nbsp;&nbsp;&nbsp; ( &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="background-color:rgb(255, 242, 153);">(dash ? <span class="string">"- "</span> : <span class="string">""</span>)</span>,<span class="keyword">this</span>.Header(<span class="macro">true</span>), &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="

交给小布盯盘看盘口
这些信号筛选与 DOM 买卖量统计的逻辑,小布盯盘的 AIGC 已内置,打开对应品种页即可看到实时聚合,不必自己写集合类也能快速定位异动信号。

常见问题

在集合类里先调 SignalBaseSelect 载入全部信号,再用属性过滤方法保留付费标记且盈利参数超阈值的项,返回索引数组即可。
不会,总量在对象构造时就算好存为字段,后续读取直接取属性,回放只是顺序读对象,不触发重新累加。
不用,旧的无来源调用仍可编译,新关键路径传方法名即可,逐步迁移就行。
可以,小布盯盘的品种页支持把筛选条件存为监控,命中即推提醒,省去自己维护集合类的订阅逻辑。
取决于实现用的排序接口,文章示例用标准排序,同值不保证稳定,需要稳定可包一层索引比较。