DoEasy 函数库中的价格(第六十四部分):市场深度,DOM 快照类和快照序列对象·进阶篇
(2/3)· 从单次 OnBookEvent 到数万帧 DOM 快照链,解决盘口状态难复盘的工程痛点
◍ 从深度报单里筛最大挂量的那一行
做价格行为分析时,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 一起释放掉。
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_* 传进去就能拿到列表下标,省得自己写遍历。外汇与贵金属盘口跳动快、滑点大,这类极值筛选仅作结构参考,不代表流动性方向。
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 策略测试器里先跑一遍空单薄验证。
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] 这样的行。外汇与贵金属杠杆高,盘口数据瞬变,这类打印仅作调试,不代表任何方向判断。
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 | </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>) { <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>+ <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>; } <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 盘口跳动频繁,这类快照对象配合后续序列类,才可能做出挂单撤单频率的真实统计,但外汇贵金属杠杆高,任何基于盘口的分析都只是概率参考。
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