MQL5 酷客宝典: 实现您自己的市场深度·进阶篇
📊

MQL5 酷客宝典: 实现您自己的市场深度·进阶篇

(2/3)·标准 DOM 只够看不够用,本文把事件模型、封装类与面板绘制一次打通

含代码示例 第 2/3 篇
直接用 MarketBookGet 裸接二级报价的人,常在 tick 风暴里漏掉事件边界,导致盘口计算错位。把低级函数硬塞进 EA 主循环,滑点和偏离度反而算不清。真正的坑不在取数,在何时取、怎么缓存。

DOM 快照怎么从事件里抠出来

在 MT5 里抓深度-of-market 数据,入口是 OnBookEvent(const string &symbol) 这个系统回调。每次某交易品种盘口变动,终端就会触发它,你只能拿到 symbol 名,真正的买卖挂单数组得从自己维护的 BookOnSi.MarketBook[] 里读。 下面这段是最小可用骨架:回调里直接取 MarketBook[0] 塞进 MqlBookInfo 结构,随后用 InfoGetDouble(MBOOK_BEST_ASK_PRICE) 拿最优卖价。注意 MarketBook[0] 未必是卖单开头,只是数组首元素,索引语义要靠后面枚举厘清。

MQL5 / C++
class="type">void OnBookEvent(class="kw">const class="type">class="kw">string &symbol)
  {
class=class="str">"cmt">//---
   MqlBookInfo info = BookOnSi.MarketBook[class="num">0];
  }
class="type">class="kw">double best_ask = BookOnSi.InfoGetDouble(MBOOK_BEST_ASK_PRICE);
盘口属性分两套枚举:ENUM_MBOOK_INFO_INTEGER 管索引和深度,比如 MBOOK_BEST_ASK_INDEX 是最佳卖价位置、MBOOK_DEPTH_BID 是买侧挂单级数;ENUM_MBOOK_INFO_DOUBLE 管价格,含 MBOOK_BEST_ASK_PRICE、MBOOK_BEST_BID_PRICE 以及 MBOOK_AVERAGE_SPREAD(买卖平均点差)。外汇和贵金属盘口薄时深度可能只有个位数,点差跳动快,属高风险品种,读数要加概率视角而非当确定值。 实际定位最优买卖价有个提速写法:先 FindBestBid 失败就全数组扫,遇到 BOOK_TYPE_BUY 系列就把 m_best_ask_index 设为 i-1 并回退调用。这样多数情况从已知卖价反查买价,少做整遍循环。
MQL5 / C++
class="type">void CMarketBook::SetBestAskAndBidIndex(class="type">void)
{
   if(!FindBestBid())
   {
      class="type">int bookSize = ArraySize(MarketBook);
      for(class="type">int i = class="num">0; i < bookSize; i++)
      {
         if((MarketBook[i].type == BOOK_TYPE_BUY) || (MarketBook[i].type == BOOK_TYPE_BUY_MARKET))
         {
            m_best_ask_index = i-class="num">1;
            FindBestBid();
            class="kw">break;
         }
      }
   }
}
开盘口类 CMarketBook 时,建议先在策略测试器用真实tick回放验证:EURUSD 在伦敦时段盘口深度可能到 15 级买+15 级卖,而夜间可能缩到 3 级以内,索引逻辑若写死会越界。

MQL5 / C++
class="type">void OnBookEvent(class="kw">const class="type">class="kw">string &symbol)
  {
class=class="str">"cmt">//---
   MqlBookInfo info = BookOnSi.MarketBook[class="num">0];
  }
class="type">class="kw">double best_ask = BookOnSi.InfoGetDouble(MBOOK_BEST_ASK_PRICE);
enum ENUM_MBOOK_INFO_INTEGER
{
   MBOOK_BEST_ASK_INDEX,      class=class="str">"cmt">// 最佳卖出价索引
   MBOOK_BEST_BID_INDEX,      class=class="str">"cmt">// 最佳买入价索引
   MBOOK_LAST_ASK_INDEX,      class=class="str">"cmt">// 最坏卖出价索引
   MBOOK_LAST_BID_INDEX,      class=class="str">"cmt">// 最坏买入价索引
   MBOOK_DEPTH_ASK,           class=class="str">"cmt">// 卖出级别数量
   MBOOK_DEPTH_BID,           class=class="str">"cmt">// 买入级别数量
   MBOOK_DEPTH_TOTAL         class=class="str">"cmt">// DOM 级别总数
};
enum ENUM_MBOOK_INFO_DOUBLE
{
   MBOOK_BEST_ASK_PRICE,      class=class="str">"cmt">// 最佳卖出价
   MBOOK_BEST_BID_PRICE,      class=class="str">"cmt">// 最佳买入价
   MBOOK_LAST_ASK_PRICE,      class=class="str">"cmt">// 最坏卖出价
   MBOOK_LAST_BID_PRICE,      class=class="str">"cmt">// 最坏买入价
   MBOOK_AVERAGE_SPREAD      class=class="str">"cmt">// 卖出价与买入价之间的平均点差
};
class="type">void CMarketBook::SetBestAskAndBidIndex(class="type">void)
{
   if(!FindBestBid())
   {
      class="type">int bookSize = ArraySize(MarketBook);
      for(class="type">int i = class="num">0; i < bookSize; i++)
      {
         if((MarketBook[i].type == BOOK_TYPE_BUY) || (MarketBook[i].type == BOOK_TYPE_BUY_MARKET))
         {
            m_best_ask_index = i-class="num">1;
            FindBestBid();
            class="kw">break;
         }
      }
   }
}

◍ 从挂单簿切片算量价偏离

深度簿里 best ask 之后紧邻的那一档未必就是 best bid,代码先校验 m_best_ask_index 指向的必须是 BOOK_TYPE_SELL 或 BOOK_TYPE_SELL_MARKET,否则直接返回 false 认为结构无效。 确认卖侧后,用 bestBid = m_best_ask_index+1 去取下一档,再判断它是否属于 BOOK_TYPE_BUY / BOOK_TYPE_BUY_MARKET;命中才把 m_best_bid_index 记下并返回 true,这一步保证了 ask/bid 配对顺序不被乱序簿打乱。 GetDeviationByVol 做的是另一件事:给定 vol 手数,沿 ask 或 bid 方向把挂单 volume 逐档乘 price 累加,直到 vol 被吃光。若循环结束 volume_exe 仍大于 0,说明簿上流动性不够吞下该量,函数返回 -1.0 表示偏离无限。 正常走完时 avrg_price 除以 vol 得到平均成交价,这个数值和现价的差就是量加权偏离。外汇与贵金属盘口薄时常抽空,返回 -1.0 的概率不低,实盘调用前应先判返回值再决定策略。

MQL5 / C++
   (MarketBook[m_best_ask_index].type == BOOK_TYPE_SELL ||
    MarketBook[m_best_ask_index].type == BOOK_TYPE_SELL_MARKET);
   if(!isBestAsk)class="kw">return false;
   class="type">int bestBid = m_best_ask_index+class="num">1;
   class="type">bool isBestBid = bestBid >= class="num">0 && bestBid < m_depth_total &&
      (MarketBook[bestBid].type == BOOK_TYPE_BUY ||
       MarketBook[bestBid].type == BOOK_TYPE_BUY_MARKET);
   if(isBestBid)
   {
      m_best_bid_index = bestBid;
      class="kw">return true;
   }
   class="kw">return false;
}
class="type">int bestBid = m_best_ask_index+class="num">1;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| MarketBook 的方向                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
enum ENUM_MBOOK_SIDE
{
   MBOOK_ASK,           class=class="str">"cmt">// 卖出方
   MBOOK_BID            class=class="str">"cmt">// 买入方
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 获取交易量的偏离值。返回 -class="num">1.0 如果偏离是无限 (流动性不足)           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CMarketBook::GetDeviationByVol(class="type">long vol, ENUM_MBOOK_SIDE side)
{
   class="type">int best_ask = InfoGetInteger(MBOOK_BEST_ASK_INDEX);
   class="type">int last_ask = InfoGetInteger(MBOOK_LAST_ASK_INDEX);
   class="type">int best_bid = InfoGetInteger(MBOOK_BEST_BID_INDEX);
   class="type">int last_bid = InfoGetInteger(MBOOK_LAST_BID_INDEX);
   class="type">class="kw">double avrg_price = class="num">0.0;
   class="type">long volume_exe = vol;
   if(side == MBOOK_ASK)
   {
      for(class="type">int i = best_ask; i >= last_ask; i--)
      {
         class="type">long currVol = MarketBook[i].volume < volume_exe ?
                        MarketBook[i].volume : volume_exe ;
         avrg_price += currVol * MarketBook[i].price;
         volume_exe -= MarketBook[i].volume;
         if(volume_exe <= class="num">0)class="kw">break;
      }
   }
   else
   {
      for(class="type">int i = best_bid; i <= last_bid; i++)
      {
         class="type">long currVol = MarketBook[i].volume < volume_exe ?
                        MarketBook[i].volume : volume_exe ;
         avrg_price += currVol * MarketBook[i].price;
         volume_exe -= MarketBook[i].volume;
         if(volume_exe <= class="num">0)class="kw">break;
      }
   }
   if(volume_exe > class="num">0)
      class="kw">return -class="num">1.0;
   avrg_price/= (class="type">class="kw">double)vol;

「从深度快照里拆出偏离值」

上面那段返回逻辑干的事很直接:给定挂单方向,算出平均价与最优买卖价的差。side 为 MBOOK_ASK 时,deviation = avrg_price - MarketBook[best_ask].price;反之用最优买价减平均价,结果就是单边的价格偏离距离。 真正要把深度跑起来,得先挂上 CMarketBook 类。用 #include <Trade\MarketBook.mqh> 引头文件,再以 CMarketBook Book(Symbol()) 绑定当前品种,行情刷新靠 Book.Refresh() 拉取。 PrintMbookInfo() 里几个整型字段是盯盘第一手数据:MBOOK_DEPTH_TOTAL 是深度总档位,MBOOK_DEPTH_ASK / MBOOK_DEPTH_BID 分开看买卖侧厚度,MBOOK_BEST_ASK_INDEX 和 MBOOK_BEST_BID_INDEX 定位最优价在数组里的下标。双精度里 MBOOK_BEST_ASK_PRICE、MBOOK_BEST_BID_PRICE、MBOOK_LAST_ASK_PRICE 分别对应最优卖、最优买和末档卖价。 外汇与贵金属的盘口深度在新闻市容易瞬间抽薄,用这套接口读到的档位数量可能为 0,实盘验证前先在模拟盘跑 PrintMbookInfo 看品种实际供给。

MQL5 / C++
class="type">class="kw">double deviation = class="num">0.0;
if(side == MBOOK_ASK)
      deviation = avrg_price - MarketBook[best_ask].price;
else
      deviation = MarketBook[best_bid].price - avrg_price;
class="kw">return deviation;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                     TestMarketBook.mq5 |
class=class="str">"cmt">//|            Copyright class="num">2015, MetaQuotes Software Corp. |
class=class="str">"cmt">//|                     [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2015, MetaQuotes Software Corp."
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#include <Trade\MarketBook.mqh>     class=class="str">"cmt">//  包含 CMarketBook 类
CMarketBook Book(Symbol());        class=class="str">"cmt">// 以当前金融工具初始化类
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
   PrintMbookInfo();
   class="kw">return INIT_SUCCEEDED;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 脚本程序开始函数                                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTimer()
  {
class=class="str">"cmt">//---
  
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 打印 MarketBook 信息                                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void PrintMbookInfo()
  {
   Book.Refresh();                class=class="str">"cmt">// 更新市场深度状态。
   class=class="str">"cmt">/* 获取基础统计 */
   class="type">int total=Book.InfoGetInteger(MBOOK_DEPTH_TOTAL);
   class="type">int total_ask = Book.InfoGetInteger(MBOOK_DEPTH_ASK);
   class="type">int total_bid = Book.InfoGetInteger(MBOOK_DEPTH_BID);
   class="type">int best_ask = Book.InfoGetInteger(MBOOK_BEST_ASK_INDEX);
   class="type">int best_bid = Book.InfoGetInteger(MBOOK_BEST_BID_INDEX);
   printf("市场深度总数: "+(class="type">class="kw">string)total);
   printf("卖出价位数量: "+(class="type">class="kw">string)total_ask);
   printf("买入价位数量: "+(class="type">class="kw">string)total_bid);
   printf("最佳卖出价索引: "+(class="type">class="kw">string)best_ask);
   printf("最佳买入价索引: "+(class="type">class="kw">string)best_bid);
   
   class="type">class="kw">double best_ask_price = Book.InfoGetDouble(MBOOK_BEST_ASK_PRICE);
   class="type">class="kw">double best_bid_price = Book.InfoGetDouble(MBOOK_BEST_BID_PRICE);
   class="type">class="kw">double last_ask = Book.InfoGetDouble(MBOOK_LAST_ASK_PRICE);

从市场深度回读里抓报价边界

在 MT5 的 Market Book 回调里,除了拿最优买卖价,还能顺手把边界价格和平均点差读出来。下面这段把最后成交买价、平均点差以及四个极端报价打印到日志,方便你核对盘口厚度。 double last_bid = Book.InfoGetDouble(MBOOK_LAST_BID_PRICE); double avrg_spread = Book.InfoGetDouble(MBOOK_AVERAGE_SPREAD); printf("最佳卖出价: " + DoubleToString(best_ask_price, Digits())); printf("最佳买入价: " + DoubleToString(best_bid_price, Digits())); printf("最坏卖出价: " + DoubleToString(last_ask, Digits())); printf("最坏买入价: " + DoubleToString(last_bid, Digits())); printf("平均点差: " + DoubleToString(avrg_spread, Digits())); } 逐行看:前两行用 InfoGetDouble 取末次买价和平均点差;随后五个 printf 分别输出最优卖、最优买、最差卖、最差买和平均点差,Digits() 控制小数位跟品种精度对齐。 实际在 OFZ2-9.15 的 D1 测试里,2015.06.16 17:13:23 那一刻输出显示:最优买 9840、最优卖 9910、最差买 9831、最差卖 9999,平均点差 70。盘口共 9 档,卖价 7 档、买价 2 档,买卖厚度明显不对称,这类倾斜在贵金属跳空前后也常出现,外汇和贵金属杠杆高,盘口薄时滑点概率会放大。 把这段代码塞进你的 BookEvent 回调,跑一天就知道自己常做的品种盘口边界长什么样,不用凭感觉估滑点。

MQL5 / C++
class="type">class="kw">double last_bid = Book.InfoGetDouble(MBOOK_LAST_BID_PRICE);
class="type">class="kw">double avrg_spread = Book.InfoGetDouble(MBOOK_AVERAGE_SPREAD);

printf("最佳卖出价: " + DoubleToString(best_ask_price, Digits()));
printf("最佳买入价: " + DoubleToString(best_bid_price, Digits()));
printf("最坏卖出价: " + DoubleToString(last_ask, Digits()));
printf("最坏买入价: " + DoubleToString(last_bid, Digits()));
printf("平均点差: " + DoubleToString(avrg_spread, Digits()));
}

◍ 把市场深度做成一个可隐藏的图表面板

在 MT5 里做 DOM 面板,基础选指标而不是 EA,原因很直接:一个图表只能挂一个 EA,但指标可以无限叠加。若用 EA 做面板,同一图表就没法再跑交易 EA,实战中极不方便。面板默认放在图表左上角,避开右上角交易 EA 的图标区域,用同一个箭头按钮切换显示和隐藏。 指标代码里 OnCalculate 留空即可,因为面板不消费价格序列,indicator_plots 设 0。真正驱动它的是 OnBookEvent,启动时用 MarketBookAdd 订阅当前图表品种,才能收到深度变动。 初版只用一个 CBookPanel 类,里面先放箭头和 'MarketBook' 标签。所有元素都从 CNode 基类派生,基类自带 Show / Hide 和唯一实例名,方便用标准函数建图形对象。此时点击箭头只会弹出灰色画布,还没填数据。 接着在 OnChartEvent 里接管点击,CNode 扩展出 CArrayObj 容器装子节点。单元格用 CBookCeil 类,建对象时指定类型:买限价蓝、卖限价粉,文本用 OBJ_TEXT_LABEL,底色用 OBJ_RECTANGLE_LABEL。CreateCeils 按 DOM 索引动态生成单元,编译后就能看到带价位和量的初版表格。 直接在单元格 Refresh 里算直方条最省事,但 40 档深度每档都全量迭代,一次刷新就 800 次循环,太浪费。改法是把最大量计算和买/卖总量统计塞进 CMarketBook 内的 CBookCalculation 子类,用 m_calculate 标志做延迟计算——只有 Reset 被调用才重算,两次刷新间最多迭代一次。 优化后把直方条宽度计算挪到面板层,CBookLine 子类按 best_bid 索引乘 15 像素再加 19 像素常量画分割线。莫斯科交易所还推送实时限价单数和交易量,通过 SymbolInfoInteger / Double 取,再包进 CMarketBook 的 InfoGet 系列,集中访问更顺手。外汇和贵金属波动剧烈、杠杆高风险大,这类 DOM 面板仅作盘口观察,不构成方向判断。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 MarketBook.mq5 |
class=class="str">"cmt">//|                     Copyright class="num">2015, MetaQuotes Software Corp. |
class=class="str">"cmt">//|                                             [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2015, MetaQuotes Software Corp."

「把深度报价挂上图表事件链」

想在 MT5 主图直接看市场深度,核心不是画指标线,而是把 MarketBook 事件接进图表生命周期。下面这段指标骨架用 indicator_plots 0 声明自己不画任何缓冲线,纯粹当事件容器用。 OnInit 里只做一件事:MarketBookAdd(Symbol()) 把当前品种订阅进深度簿。返回 INIT_SUCCEEDED 后,只要盘口有变动,终端就会触发 OnBookEvent,这里转手调 Panel.Refresh() 重绘面板。 OnChartEvent 负责把点击、拖拽等 GUI 交互透传给 Panel.Event(),随后 ChartRedraw() 强制刷新。OnCalculate 故意返回 rates_total 而不做价格计算——它在本例只是保活接口,真实渲染全在面板类里。 外汇与贵金属盘口薄时常抽空,深度数据延迟或断流都可能发生,挂此面板前先确认券商开了 MarketBook 权限,否则 OnBookEvent 不会触发。

MQL5 / C++
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#class="kw">property indicator_chart_window
class="macro">#class="kw">property indicator_plots class="num">0
class="macro">#include <Trade\MarketBook.mqh>
class="macro">#include "MBookPanel.mqh"
CBookPanel Panel;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 自定义指标初始化函数                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- 指标缓存区映射
   MarketBookAdd(Symbol());
class=class="str">"cmt">//---
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| MarketBook 变化事件                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnBookEvent(class="kw">const class="type">class="kw">string &symbol)
  {
   Panel.Refresh();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 图表事件                                                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnChartEvent(class="kw">const class="type">int id,      class=class="str">"cmt">// 事件表示符
                  class="kw">const class="type">long& lparam,  class=class="str">"cmt">// 长整形事件参数
                  class="kw">const class="type">class="kw">double& dparam, class=class="str">"cmt">// 双精度形事件参数
                  class="kw">const class="type">class="kw">string& sparam) class=class="str">"cmt">// 字符串形事件参数
  {
   Panel.Event(id,lparam,dparam,sparam);
   ChartRedraw();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 自定义指标迭代函数                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnCalculate(class="kw">const class="type">int rates_total,
                class="kw">const class="type">int prev_calculated,
                class="kw">const class="type">int begin,
                class="kw">const class="type">class="kw">double &price[])
  {
class=class="str">"cmt">//---
class=class="str">"cmt">//--- 返回 prev_calculated 值用于下次调用
   class="kw">return(rates_total);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
把盘口诊断交给小布盯盘
这些 DOM 偏离与最大滑点诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到,你只管判断流动性是否异常。

常见问题

MarketBookGet 按品种返回 MqlBookInfo 数组,每个元素含买卖侧、价格与成交量,是二级报价的最低层入口。
前值预测能平滑 tick 噪声,倾向在盘口跳动过快时给出更稳的参考位,降低误触发概率。
可以,小布已内置基于成交量的偏离诊断,无需你自己写 CMarketBook 类就能看最大滑点估计。
实时重算最大交易量在深盘口会拖慢刷新,迭代法只增量更新,可能显著降 CPU 占用。
它反映挂单密度,结合价格层可判断支撑阻力强度,但外汇贵金属高风险,仅作概率参考。