读取当前的市场深度数据

成功执行 MarketBookAdd 函数后,MQL 程序可以在 OnBookEvent 事件到达后使用 MarketBookGet 函数查询订单簿状态。MarketBookGet 函数使用指定交易品种的市场深度值填充通过引用传递的 MqlBookInfo 结构体数组。

bool MarketBookGet(string symbol, MqlBookInfo &book[])

对于接收数组,可以预先为足够数量的记录分配内存。如果动态数组的大小为零或不足,终端本身将为其分配内存。

该函数返回成功 (true) 或错误 (false) 的指示。

MarketBookGet 通常直接在 OnBookEvent 处理程序代码中使用,也可以该函数所调用的函数中使用。

关于市场深度价格水平的单独记录存储在 MqlBookInfo 结构体中。

struct MqlBookInfo 

   ENUM_BOOK_TYPE type;            // request type 
   double         price;           // price 
   long           volume;          // volume 
   double         volume_real;     // volume with increased accuracy 
};

枚举 ENUM_BOOK_TYPE 包含以下元素。

标识符

说明

BOOK_TYPE_SELL

限价卖出请求

BOOK_TYPE_BUY

限价买入请求

BOOK_TYPE_SELL_MARKET

市价卖出请求

BOOK_TYPE_BUY_MARKET

市价买入请求

在订单簿中,卖出订单位于其上半部分,买入订单位于下半部分。通常,这样可形成从高价到低价的元素排列顺序。换言之,在索引 0 处是最高价格,最后一个条目为最低价格,中间的价格逐渐降低。在这种情况下,各个价格水平之间的最小价格步长为 SYMBOL_TRADE_TICK_SIZE,但是,交易量为零的层级不会被转换,也就是说,相邻元素之间的间隔可能会很大。

在终端用户界面中,订单簿窗口提供了启用/禁用 Advanced Mode 的选项,在该模式下,零交易量的层级也会显示出来。但是默认在标准模式下,这些层级是隐藏的(在表中被跳过)。

实践中,订单簿的内容有时会与公布的规则相矛盾。特别是,一些限价买入或卖出请求可能落入订单簿的另一半部分(可能有人以不利的高价买入或以不利的低价卖出,但提供商也可能会出现数据汇总错误)。由于遵守了优先级“所有卖出订单在上方,所有买入订单在下方”,因此会违反订单簿中的价格顺序(参见以下示例)。此外,在订单簿的上半部分和下半部分均可以发现价格(层级)的重复值。

理论上,买入卖出价格在订单簿中间重合是正常情况。这表示零点差。但遗憾的是,在订单簿深度较大时会出现重复的价格层级。

当我们说订单薄的“半区”时,不应从字面上理解。根据流动性,供给和需求层级的数量可能不匹配。总之,订单簿并不是对称的。

MQL 程序必须检查订单簿(尤其是价格排序订单)是否正确,并准备好处理潜在的偏差。

有一些异常情况虽然不算严重,但在算法中仍应该予以考虑,具体包括:

  • 连续相同的订单簿(无变化)
  • 空白订单簿
  • 包含一个层级的订单簿

以下是从一个经纪商获得的真实市场深度片段。字母 'S' 和 'B' 分别代表卖出和买入请求的价格。

请注意,买入和卖出层级实际上是有重叠的:尽管乍一看并不明显,因为订单簿中的所有 'S' 记录都是专门置于上半部分(接收数组的开头),而 'B' 记录置于下半部分(数组的结尾)。但如果仔细看就会发现:元素 20 和 21 中的买入价格分别为 143.23 和 138.86,高于所有卖出报价。并且,与此同时,元素 18 和 19 中的销售价格为 134.62 和 133.55,低于所有买入报价。

...
10 S 138.48 652
11 S 138.47 754
12 S 138.45 2256
13 S 138.43 300
14 S 138.42 14
15 S 138.40 1761
16 S 138.39 670    // Duplicate
17 S 138.11 200
18 S 134.62 420    // Low
19 S 133.55 10627  // Low
 
20 B 143.23 9564   // High
21 B 138.86 533    // High
22 B 138.39 739    // Duplicate
23 B 138.38 106
24 B 138.31 100
25 B 138.25 29
26 B 138.24 6072
27 B 138.23 571
28 B 138.21 17
29 B 138.20 201
30 B 138.19 1
...

此外,138.39 价格同时出现在上半部分(索引号为 16)和下半部分(索引号为 22)。

订单簿中的错误最有可能出现在极端条件下:剧烈波动或缺乏流动性。

我们使用 MarketBookDisplay.mq5 指标检查订单簿的接收情况。其用于订阅参数 WorkSymbol 中指定交易品种的市场深度事件(如果你在此处留空,则假定为当前图表的工作交易品种)。

input string WorkSymbol = ""// WorkSymbol (if empty, use current chart symbol)
   
const string _WorkSymbol = StringLen(WorkSymbol) == 0 ? _Symbol : WorkSymbol;
int digits;
   
void OnInit()
{
   PRTF(MarketBookAdd(_WorkSymbol));
   digits = (int)SymbolInfoInteger(_WorkSymbolSYMBOL_DIGITS);
   ...
}
   
void OnDeinit(const int)
{
   Comment("");
   PRTF(MarketBookRelease(_WorkSymbol));
}

OnBookEvent 处理程序是在用于处理事件的代码中定义的,其中调用了 MarketBookGet,生成的 MqlBookInfo 数组的所有元素均作为多行注释输出。

void OnBookEvent(const string &symbol)
{
   if(symbol == _WorkSymbol// take only order books of the requested symbol
   {
      MqlBookInfo mbi[];
      if(MarketBookGet(symbolmbi)) // getting the current order book
      {
         ...
         int half = ArraySize(mbi) / 2// estimate of the middle of the order book
         bool correct = true;
         // collect information about levels and volumes in one line (with hyphens)
         string s = "";
         for(int i = 0i < ArraySize(mbi); ++i)
         {
            s += StringFormat("%02d %s %s %d %g\n"i,
               (mbi[i].type == BOOK_TYPE_BUY ? "B" : 
               (mbi[i].type == BOOK_TYPE_SELL ? "S" : "?")),
               DoubleToString(mbi[i].pricedigits),
               mbi[i].volumembi[i].volume_real);
               
            if(i > 0// look for the middle of the order book as a change in request type
            {
               if(mbi[i - 1].type == BOOK_TYPE_SELL
                  && mbi[i].type == BOOK_TYPE_BUY)
               {
                  half = i// this is the middle, because there has been a type change
               }
               
               if(mbi[i - 1].price <= mbi[i].price)
               {
                  correct = false// reverse order = data problem
               }
            }
         }
         Comment(s + (!correct ? "\nINCORRECT BOOK" : ""));
         ...
      }
   }
}

由于订单簿变化相当快,追踪注释不是很方便。因此,我们将向该指标添加几个缓冲区,其中我们将把订单簿的两个部分内容显示为直方图:分别为卖出和买入。零柱线对应于形成点差的中心层级。随着条柱线数量的增加,“市场深度”也在增加,也就是说,此处会显示越来越远的价格水平:在上面的直方图中,意味着买入订单的价格较低,而在下面的直方图中,卖出订单的价格较高。

#property indicator_separate_window
#property indicator_plots 2
#property indicator_buffers 2
   
#property indicator_type1   DRAW_HISTOGRAM
#property indicator_color1  clrDodgerBlue
#property indicator_width1  2
#property indicator_label1  "Buys"
   
#property indicator_type2   DRAW_HISTOGRAM
#property indicator_color2  clrOrangeRed
#property indicator_width2  2
#property indicator_label2  "Sells"
   
double buys[], sells[];

我们能以标准和扩展模式(即,跳过或显示零交易量的价格水平)可视化订单簿,以及以手数或单位的分数显示交易量本身。这两种选择与内置的市场深度窗口功能类似。

input bool AdvancedMode = false;
input bool ShowVolumeInLots = false;

我们在 OnInit 中设置缓冲区并获取一些交易品种特性(稍后我们会用到)。

int depthdigits;
double tickcontract;
   
void OnInit()
{
   ...
   // setting indicator buffers
   SetIndexBuffer(0buys);
   SetIndexBuffer(1sells);
   ArraySetAsSeries(buystrue);
   ArraySetAsSeries(sellstrue);
   // getting the necessary symbol properties
   depth = (int)PRTF(SymbolInfoInteger(_WorkSymbolSYMBOL_TICKS_BOOKDEPTH));
   tick = SymbolInfoDouble(_WorkSymbolSYMBOL_TRADE_TICK_SIZE);
   contract = SymbolInfoDouble(_WorkSymbolSYMBOL_TRADE_CONTRACT_SIZE);
}

我们将缓冲区填充添加到处理程序 OnBookEvent 中。

#define VOL(V) (ShowVolumeInLots ? V / contract : V)
   
void OnBookEvent(const string &symbol)
{
   if(symbol == _WorkSymbol// take only order books of the requested symbol
   {
      MqlBookInfo mbi[];
      if(MarketBookGet(symbolmbi)) // getting the current order book
      {
         // clear the buffers to the depth with 10 times the margin of the maximum depth,
         // because extended mode can have a lot of empty elements
         for(int i = 0i <= depth * 10; ++i)
         {
            buys[i] = EMPTY_VALUE;
            sells[i] = EMPTY_VALUE;
         }
         ...// further along we form and display the comment as before
         if(!correctreturn;
         
         // filling buffers with data
         if(AdvancedMode// show skips enabled
         {
            for(int i = 0i < ArraySize(mbi); ++i)
            {
               if(i < half)
               {
                  int x = (int)MathRound((mbi[i].price - mbi[half - 1].price) / tick);
                  sells[x] = -VOL(mbi[i].volume_real);
               }
               else
               {
                  int x = (int)MathRound((mbi[half].price - mbi[i].price) / tick);
                  buys[x] = VOL(mbi[i].volume_real);
               }
            }
         }
         else // standard mode: show only significant elements
         {
            for(int i = 0i < ArraySize(mbi); ++i)
            {
               if(i < half)
               {
                  sells[half - i - 1] = -VOL(mbi[i].volume_real);
               }
               else
               {
                  buys[i - half] = VOL(mbi[i].volume_real);
               }
            }
         }
      }
   }
}

下图演示了在设置 AdvancedMode=trueShowVolumeInLots=true 的情况下指标的工作方式。

USDCNH 图表上 MarketBookDisplay.mq5 指标中订单簿的内容
USDCNH 图表上 MarketBookDisplay.mq5 指标中订单簿的内容

买入显示为正值(顶部的蓝色柱线),卖出显示为负值(底部的红色柱线)。为了清楚起见,右边有一个标准的市场深度窗口,具有相同的设置(高级模式下,交易量以手数显示),因此你可以确保这些值匹配。

需要注意的是,该指标的重新绘制速度可能不够快,无法与内置订单簿保持同步。这并不意味着 MQL 程序没有及时接收到事件,而只是异步图表呈现的副作用。工作算法通常包括分析处理和订单簿下单,而不包括可视化。

在这种情况下,更新图表是在调用 Comment 函数时隐式请求的。