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

Vasiliy Sokolov | 5 一月, 2016

内容目录


介绍

MQL5 语言持续发展, 并在每年当中提供更多交易信息的操作机会。其中一类交易数据是有关市场深度的信息。这是一个特殊的表格, 显示价格水平和限价指令的交易量。MetaTrader 5 有一个内置的市场深度用于显示限价单, 但是这还不够。首先, 您的 EA 必须要可以简单、便利的访问市场深度。当然, MQL5 语言有一些特别的功能可以操作这些信息, 但它们有一些低级功能需要附加的数学计算。

然而, 能够避免所有中间计算。您所有要做的就是编写一个特殊的类, 用于操作市场深度。所有复杂计算都在市场深度内执行, 且类本身可以提供操纵 DOM 价格和级别的便利方法。这个类能够以指标的形式简单地创建一个有效的面板, 它可以即刻反应出市场深度的价格状态:


图示. 1. 作为面板显示的市场深度

本文展示了如何利用市场深度 (DOM) 编程, 并介绍了 CMarketBook 类的操作原理, 它可扩展 MQL5 标准库的类, 并提供使用 DOM 的便利方法。

读完本文的第一章之后, 对于 MetaTrader 5 提供的规范市场深度所具有的令人印象深刻的能力更加清晰。我们不会在我们的指标里试图重复所有这些, 因为我们的任务将是完全不同的。通过实施创建用户友好的市场深度交易面板的例程, 我们将展示面向对象编程简单处理复杂数据结构的原理。我们将确保利用 MQL 5 从您的 EA 里直接访问市场深度不会很困难, 以及据此产生的视觉示意能够为我们提供便利。

 

第 1 章. MetaTrader 5 中的市场深度以及使用它的方法


1.1. MetaTrader 5 中的标准市场深度

MetaTrader 5 支持在集中式交易所交易, 并提供操纵市场深度的标准工具。首先, 它当然是一份限价订单的清单, 可在当下给出一份领先模式的表述。为了打开市场深度, 不许连接到一所支持 MetaTrader 5 的交易所, 并在菜单里选择 "视图" --> "市场深度" --> "金融工具名" 由分时图表和限价订单清单组成的弹出窗口将会出现:


图示. 2. MetaTrader 5 中的标准市场深度

MetaTrader 5 的标准市场深度可拥有丰富的功能。尤其是, 它允许显示以下内容

该列表提供的确认比市场深度的功能更令人印象深刻。让我们探寻如何通过程序化操作来访问它获取数据。首先, 您需要了解一些市场深度是如何建立的, 以及它的数据组织的关键概念。更多信息请阅读文章 "以莫斯科证券交易所衍生品市场为例的定价原理" 的 13 章. 买家和卖家的撮合。证交所市场深度"。我们不会花费太多时间来描述这张表格, 假设读者对此概念已经有足够的认知。

 

1.2. 操纵市场深度的事件模型

市场深度有一个动态的数据表。在快速的动态市场, 限价订单列表可能会在每秒变化很多次。因此, 您必须设法仅处理那些真正需要处理的信息, 否则数据传输的数量, 以及处理这些数据时 CPU 的负载可能会超过所应有的合理限度。这就是为什么 MetaTrader 5 需要一个特殊的事件模型来防止数据采集和处理未能如实利用。让我们对这种模式进行彻底检查。 

在市场上发生的任何事件, 例如新分时价到达, 或一笔交易事务的执行, 可通过调用与它相关的相应函数进行处理。例如, MQL 5 中一笔新分时价的到来, 一个特殊的 OnTick() 事件处理函数被调用。调整图表大小或位置会调用 OnChartEvent() 函数。该事件模型也适用于市场深度的变化。例如, 如果有人在市场深度里放置了一笔现价订单, 它的状态将会改变并调用一个特殊的 OnBookEvent() 函数。

由于在终端里有数十甚至数百个不同的拥有自己的市场深度的品种可用, 调用 OnBookEvent 函数的数量可能极其巨大, 且资源消耗密集。为了避免这种情况, 当运行的指标或 EA 需要获取金融工具的第二级报价时 (由市场深度提供的信息也要调用这种方式), 终端要提早注意。一个特殊的系统函数 MarketBookAdd 可用于这类目的。举例来说, 如果我们打算获取有关金融工具 Si-9.15 的市场深度信息 (USD/RUR 的期货合约, 过期在 2015 年 9 月), 我们需要在我们的 EA 或指标的 OnInit 函数里编写以下代码:

void OnInit()
{
   MarketBookAdd("Si-9.15");
}

利用这个函数我们创建了一个 "订阅" 并提请终端注意, 之后 EA 或指标将收到基于 Si-9.15 的市场深度变化事件通知。其它金融工具的市场深度变化不会干扰我们, 这将大大减少由程序所消耗的资源。

MarketBookAdd 的逆反函数是 MarketBookRelease 函数。反之, 我们 "退订" 来自市场深度的变化通知。程序员最好在 OnDeinit 段落里退订, 从而在 EA 或指标退出之前关闭数据的访问。

void OnDeinit(const int reason)
{
   MarketBookRelease("Si-9.15");
}

调用 MarketBookAdd 函数基本上意味着, 所关注金融工具的市场深度在变化的那一刻, 特殊的事件处理函数 OnBookEvent() 将被调用。这种简单的 EA 或指标操纵市场深度的方式包括三种系统函数:

我们的第一个 EA 例程将包含这三个函数。如以下例程所示, 每次市场深度的变化将会出现以下消息: "市场深度 Si-9.15 有变化":

//+------------------------------------------------------------------+
//|                                                       Expert.mq5 |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
//+------------------------------------------------------------------+
//| 初始化函数                                                        |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   MarketBookAdd("Si-9.15");
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| 逆初函数                                                          |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   MarketBookRelease("Si-9.15");
  }
//+------------------------------------------------------------------+
//| BookEvent 函数                                                   |
//+------------------------------------------------------------------+
void OnBookEvent(const string &symbol)
  {
//---
   printf("市场深度 " + symbol +  " 有变化"); 
  }
//+------------------------------------------------------------------+

代码中的关键指令以黄色标记强调。

 

1.3. 利用 MarketBookGet functions 和 MqlBookInfo 结构接收第二级报价

现在, 我们已经了解了接收市场深度变化的通知, 是时候来学习如何使用 MarketBookGet 函数来访问 DOM 的信息。让我们来看看它的原型和用法。

前面已经提到, 市场深度呈现为一个特殊的表格, 由两部分组成, 以显示卖出和买入限价订单。就像其它的所有表格, 显示市场深度最简单的方式就是利用数组, 此时数组的索引就是表格的行号, 而且数组值就是确定一行或一系列的数据, 包括交易量、价格和应用类型。让我们也来设想一下显示各行索引的市场深度表格:

行索引 订单类型 交易量 价格
0 Sell Limit 18 56844
1  Sell Limit  1  56843
2  Sell Limit  21  56842
3  Buy Limit  9  56836
4  Buy Limit  5  56835
5  Buy Limit  15  56834

 表格 1. 以表格呈现的市场深度

为了更容易遍历整张表格, 卖单都标为粉红色, 而买单标为蓝色。市场深度表格基本上是一个二维阵列。第一维表示行号, 第二维 - 三个表格要素之一 (订单类型 - 0, 定单交易量 - 1 以及订单价格 - 2)。然而, 为了避免操纵多维数组, 在 MQL5 中使用了一个特殊的 MqlBookInfo 结构。它包括所有必要的数值。这种方式每条市场深度索引包含一个 MqlBookInfo 结构, 切按顺序携带了有关订单类型, 交易量和价格的信息。让我们来定义这个结构:

struct MqlBookInfo
  {
   ENUM_BOOK_TYPE   type;       // 来自 ENUM_BOOK_TYPE 枚举的订单类型
   double           price;      // 订单价格
   long             volume;     // 订单交易量
  };

现在, 操纵市场深度的方法我们应该很清楚了。MarketBookGet 函数返回 MqlBookInfo 结构的数组。数组索引表示价格表格的行, 切索引结构包含有关交易量、价格和订单类型的信息。认识到这些, 我们将尝试访问第一笔 DOM 订单, 出于此原因, 我们略微修改之前 EA 例程里的 OnBookEvent 函数:

//+------------------------------------------------------------------+
//| BookEvent 函数                                                   |
//+------------------------------------------------------------------+
void OnBookEvent(const string &symbol)
  {
//---
   //printf("市场深度 " + symbol +  " 有变化"); 
   MqlBookInfo book[];
   MarketBookGet(symbol, book);
   if(ArraySize(book) == 0)
   {
      printf("加载市场预订价格失败。原因: " + (string)GetLastError());
      return;
   }
   string line = "价格: " + DoubleToString(book[0].price, Digits()) + "; ";
   line += "交易量: " + (string)book[0].volume + "; ";
   line += "类型: " + EnumToString(book[0].type);
   printf(line);
  }

当在任意图表上运行此 EA 时, 我们将收到有关第一个 DOM 和其参数的报告:

2015.06.05 15:54:17.189 Expert (Si-9.15,H1)     价格: 56464; 交易量: 56; 类型: BOOK_TYPE_SELL
2015.06.05 15:54:17.078 Expert (Si-9.15,H1)     价格: 56464; 交易量: 56; 类型: BOOK_TYPE_SELL
2015.06.05 15:54:17.061 Expert (Si-9.15,H1)     价格: 56464; 交易量: 56; 类型: BOOK_TYPE_SELL
...

观察我们之前的表格, 很容易猜到, DOM 的零索引处的级别对应于最坏的卖出价 (BOOK_TYPE_SELL)。而与此相反, 最低买入价取自所得数组的最后一个索引。最佳卖出和买入价大约在市场深度的中间位置。所获价格的首个缺点, 就是市场深度正常执行计算时最佳价格通常位于表格中间。最低的卖出和买入价则是次要的。进一步, 当分析 CMarketBook 类时, 我们将提供便利的操纵市场深度的特殊索引器来解决这个问题。

 

第 2 章. 简单访问和操作市场深度的 CMarketBook 类


2.1. 设计 CMarketInfoBook 类

在第一章中, 我们见识了操纵市场深度的系统函数, 并发现了访问第二级报价的事件模型的具体特征。在这一章里我们将创建一个特殊的类 CMarketBook 可以便利地使用标准市场深度。根据从第一章获得的知识, 我们能够讨论我们的类在操纵这种数据时应具有的属性。

因此, 在设计这个类时需要考虑的第一件事就是接收数据时的资源密度。市场深度可在每秒钟更新几十次, 除此之外, 它还包含几十个 MqlBookInfo 型的元素。所以, 我们的类最初必须仅操纵一种金融工具。当处理来自若干个不同金融工具的深度市场时, 它会为每个特定的金融工具, 创建多个类的拷贝:

CMarketBook("Si-9.15");            // 市场深度 Si-9.15
CMarketBook("ED-9.15");            // 市场深度 ED-9.15
CMarketBook("SBRF-9.15");          // 市场深度 SBRF-9.15
CMarketBook("GAZP-9.15");          // 市场深度 GAZP-9.15

第二个方面, 我们需要关注的是如何更容易获取数据的组织。由于限价订单会产生高频的价格流, 这不太可能将市场深度拷贝到一个安全的面向对象的表格。所以, 我们的类将提供直接的, 尽管不很安全, 由 MarketBookGet 系统函数来访问 MqlBookInfo 数组。例如, 利用我们的类访问 DOM 零索引, 可如下编写:

CMarketBook BookOnSi("Si-9.15");
...
//+------------------------------------------------------------------+
//| BookEvent 函数                                                   |
//+------------------------------------------------------------------+
void OnBookEvent(const string &symbol)
  {
//---
   MqlBookInfo info = BookOnSi.MarketBook[0];
  }
//+------------------------------------------------------------------+

MarketBook 是通过调用 MarketBookGet 函数直接获取的一个数组。不过, 使用我们的类更便利, 这主要基于这样的事实: 除了直接访问限价订单数组, 我们的类更有针对性的访问市场深度的最常用价格。例如, 为了获取最佳卖出价, 编写如下代码就足够了:

double best_ask = BookOnSi.InfoGetDouble(MBOOK_BEST_ASK_PRICE);

这比在您的 EA 里计算最佳卖出价索引, 并依据这个索引得到价格数值更方便。从以上代码可以很明显看出, CMarketBook, 以及许多其它 MQL5 系统函数, 诸如 SymbolInfoDouble 或 OrderHistoryInteger, 使用它们自己的修饰符集合以及 InfoGetInteger 和 InfoGetDouble 方法来分别访问整数和双精度数值。为了获取所需属性, 我们必须指定此属性的修饰符。让我们彻底地描述这些属性的修饰符:

//+------------------------------------------------------------------+
//| 用于 DOM 整数属性的特别                                            |
//| 修饰符。                                                          |
//+------------------------------------------------------------------+
enum ENUM_MBOOK_INFO_INTEGER
{
   MBOOK_BEST_ASK_INDEX,         // 最佳卖出价索引
   MBOOK_BEST_BID_INDEX,         // 最佳买入价索引
   MBOOK_LAST_ASK_INDEX,         // 最坏卖出价索引
   MBOOK_LAST_BID_INDEX,         // 最坏买入价索引
   MBOOK_DEPTH_ASK,              // 卖出级别数量
   MBOOK_DEPTH_BID,              // 买入级别数量
   MBOOK_DEPTH_TOTAL             // DOM 级别总数
};
//+------------------------------------------------------------------+
//| 用于 DOM 双精度属性的特别                                           |
//| 修饰符。                                                          |
//+------------------------------------------------------------------+
enum ENUM_MBOOK_INFO_DOUBLE
{
   MBOOK_BEST_ASK_PRICE,         // 最佳卖出价
   MBOOK_BEST_BID_PRICE,         // 最佳买入价
   MBOOK_LAST_ASK_PRICE,         // 最坏卖出价 
   MBOOK_LAST_BID_PRICE,         // 最坏买入价
   MBOOK_AVERAGE_SPREAD          // 卖出价与买入价之间的平均点差
};

当然, 除了 InfoGet...方法组, 我们的类将包含 Refresh 方法来触发市场深度的更新。由于事实上我们的类需要调用 Refresh() 方法, 我们将只在需要它时再使用资源密集型的类来更新。

 

2.2. 市场深度级别最常用指数的计算

CMarketBook 类是 MqlBookInfo 数组的外包。其主要目的是提供快速、便捷地访问此数组内需求最频繁的信息。因此, 类中只启用两个基本资源密集型操作:

我们不能提升您的 MarketBookGet 系统函数的速度, 但这也没必要, 因为所有 MQL5 语言的系统函数已经过最大成都的优化。但是我们可以生成所需索引的最快计算。再一次, 我们参考 ENUM_MBOOK_INFO_INTEGER 和 ENUM_MBOOK_INFO_DOUBLE 属性修饰符。如您所见, 几乎所有可用属性均基于四个索引的计算:

还使用了三个整数值属性:

很明显, 最坏卖出价的索引将始终为零, 因为该数组由 MarketBookGet 函数生成, 且起始为最坏卖出价。搜索最坏买入价索引是不足道的 - 在所获取的 MqlInfoBook 数组当中它一直处于最后的索引 (我们要提醒您, 最后一个元素的索引小于元素在此数组的总数):

最坏卖出价索引 = 0

最坏买入价索引 = 市场深度中元素总数 - 1

整数值属性的索引也很容易计算。所以, 市场深度总数一直等于 MqlBookInfo 数组里的元素数量。来自卖出价一侧的市场深度如下:

卖出价深度 = 最佳卖出价索引 - 最坏卖出价索引 + 1

我们总是加一, 因为数组号码从零开始, 并确定元素数量需要加一用于更好的索引。然而, 通过加一得到最好的卖出价, 我们已经获得了最好的买入价索引。例如, 如果在表格 1 我们在二号行加一, 我们将从最佳卖出限价订单 56 842 移动到最佳买入限价订单 56 836。我们还发现, 最坏卖出价索引始终为零。因此, 我们可以减少公式来寻找相应的卖出价深度:

卖出价深度 = 最佳买入价索引

计算买入价深度有些不同。显然买单数量等于订单总数减去卖单总数或卖出价深度。由于在之前的公式里我们已经了解到卖出价深度等于最佳买入价索引, 这不难推导出确定买单数量或买价深度数量的公式:

买入深度 = 市场深度总数 - 最佳卖出价索引

市场深度总数一直等于买入和卖出深度总数, 并因此等于在市场深度中的元素总数:

市场深度总数 = 市场深度中的元素总数

经分析, 我们已经发现了几乎所有的常用索引。利用数学排减, 我们已经用直接索引取代了索引的计算。这在 CMarketBook 类中非常重要, 因为尽可能快地访问市场深度属性和索引是必需的。

除了实际的索引, 通常还需要了解当前金融工具的平均点差。点差是最佳买入价和最佳卖出价之间的差价。CMarketBook 类可以调用带有 MBOOK_AVERAGE_SPREAD 修饰符的 InfoGetDouble 方法来获取这个参数的平均值。CMarketBook 在 Refresh 方法里计算当前点差, 以及通过调用方法时记忆的次数来求出平均值。

不过, 我们尚未发现最佳买入价和卖出价的主要索引, 因此我们进入到下一部分。

 

2.3. 基于这些指数的前值预测最佳卖出价和买入价的指数

计算最佳卖出价和买入价是一个更艰巨的任务, 我们还没有完成。例如, 在表格 1 最佳卖出价的索引应是 2 号, 而最佳买入价的索引应是 3 号。在该表中, 简化的市场深度仅由 6 个级别组成。在现实中, 市场深度可能会更大, 包含多达 64 个买卖级别。这是一个重要的数值, 要考虑到每秒钟 DOM 的更新可以发生很多次。

最简单的解决方案是在此处使用 "一分为二" 的方法。事实上, 如果我们取表格 1 中的级别总数, 其为 6, 若一分为二, 所获的数字 (3) 即是最佳买入价的索引。因此, 其前面的索引就是最佳卖出价的索引 (2)。但是, 这种方法只适用于在 DOM 表中卖出级别数量等于买入级别的数量。这通常发生在流动性的市场, 然而, 在流动性不足的市场, 市场深度也许只有部分充满, 而在另一侧有可能没有任何级别存在。

我们的 CMarketBook 类要求可操纵任何市场以及任何市场深度, 所以一分为二的方法不适合我们。为了描绘一分为二的方法可能无法正常工作的情形, 我们参考以下图示:


图示. 3. 买入价位的数量并非总等同于卖出价位的数量。

图示 3. 显示两张市场深度其中第一个期货合约是两年期联邦国债 (OFZ2-9.15) 的 DOM。第二个是 EUR/USD 期货合约 (ED-9.15)。它表明, 对于 OFZ2-9.15 买入价位的数是四, 而卖出价位的数目是八。在一个更具流动性的 ED-9.15 市场, 买卖双方的买入和卖出级别的数量都是 12。在 ED-9.15 的案例中以一分为二的方法来确定索引可以工作, 但对于 OFZ2 - 就不灵了。

搜索索引的更可靠方式应使用 DOM 迭代, 直到第一笔订单匹配 BOOK_TYPE_BUY 类型。之前的索引将自动变成最佳卖出价的索引。这是 CMarketBook 类所具有的方法。我们将参考以上的描绘:

void CMarketBook::SetBestAskAndBidIndex(void)
{
   if(!FindBestBid())
   {
      //以低速全搜索来查找最佳卖出价
      int bookSize = ArraySize(MarketBook);   
      for(int i = 0; i < bookSize; i++)
      {
         if((MarketBook[i].type == BOOK_TYPE_BUY) || (MarketBook[i].type == BOOK_TYPE_BUY_MARKET))
         {
            m_best_ask_index = i-1;
            FindBestBid();
            break;
         }
      }
   }
}

此方法的主要目的在于利用操作符进行 DOM 的迭代。一旦在市场深度里遇到匹配 BOOK_TYPE_BUY 类型的第一笔订单, 最佳买入和最佳卖出索引即被设置, 且迭代中断。每次更新都进行完整的市场深度迭代是极端资源密集型方案。

取代每次更新都进行迭代, 有一个选项来 记忆早前获取的最佳买入价位和卖出价位索引。事实上, 市场深度通常包含固定数量的买入和卖出级别。因此, 不需要每次进行市场深度迭代来查找新的索引。参考之前发现的索引, 并了解它们是否仍然是最佳买入价和卖出价的索引就足够了。FindBestBid 私有方法用来解决这类问题。让我们来查看其内容:

//+------------------------------------------------------------------+
//| 根据最佳卖出价快速查找最佳买入价                                     |
//+------------------------------------------------------------------+
bool CMarketBook::FindBestBid(void)
{
   m_best_bid_index = -1;
   bool isBestAsk = m_best_ask_index >= 0 && m_best_ask_index < m_depth_total &&
                    (MarketBook[m_best_ask_index].type == BOOK_TYPE_SELL ||
                    MarketBook[m_best_ask_index].type == BOOK_TYPE_SELL_MARKET);
   if(!isBestAsk)return false;
   int bestBid = m_best_ask_index+1;
   bool isBestBid = bestBid >= 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;
      return true;
   }
   return false;
}

它操作很方便。首先, 该方法确认当前最佳卖出价的索引仍符合最佳卖出价索引。之后, 复位最佳买入价的索引, 并尝试再次查找它, 引用的元素在最佳卖出价索引之后:

int bestBid = m_best_ask_index+1;

如果找到的元素确实是最佳买入价的索引, 那么 DOM 以前的状态与当前状态的买入和卖出级别数量相同。正因如此, 可避免 DOM 迭代, 因为在 SetBestAskAndBidIndex 方法里, 迭代之前调用了一个 FindBestBid 方法。因此, DOM 迭代仅在第一次函数调用时, 以及卖出和买入级别数量变化的情况下执行。

虽然产生的源代码比之简单的市场深度更为庞大、复杂, 但事实上, 它运行得更快。性能上的获益对于流动性市场的巨大 DOM 尤其明显。简单指令来检查条件将会极快得到满足, 这些检查的数量也比使用 for 操作符的循环小得多。所以, 旨在查找最佳价格索引的方法, 其性能会比正常迭代更高。

 

2.4. 利用 GetDeviationByVol 方法确定最大滑点

市场深度经常被交易者用来确定当前市场的流动性, 即, 市场深度用作控制风险的附加工具。如果市场流动性较低, 以市价单入场可以导致高滑点。滑点总是意味着可能会有显著代价的额外损失。

为了避免这种情形, 必须使用另外的方法来控制入场。请阅读文章 "在莫斯科证交所进行交易时如何令您的 EA 更安全" 获取更多信息。所以, 我们不会详细描述这些方法, 只会略有提及, 即通过访问市场深度, 我们可以在入场之前评估潜在的滑点值。滑点的大小取决于两个因素:

经过访问市场深度, 可令我们看到我们的订单将以什么交易量和价格被执行。如果我们知道自己的订单交易量, 我们可以计算入场价的加权平均价格。这个价格与最佳买入价或卖出价之间的差价 (依据入场方向) 就是我们的滑点。

手工计算加权平均入场价是不可能的, 因为这需要在很短的时间片里进行巨量计算 (我提醒您, DOM 的状态也许在每秒钟里变化若干次)。因此, 把这个任务委托给 EA 或指标是很自然的。

CMarketBook 类包括一个特殊的方法来计算此特性 - GetDeviationByVol。因为成交量影响滑点大小, 所以要给方法传递预计将在市场上执行的交易量。由于此方法使用交易量的算术整数值, 如同莫斯科证交所期货市场, 此方法取交易量为长整型数值。除此之外, 该方法需要知道哪一侧的流动性计算必须被执行, 因此使用了一个特殊的 ENUM_MBOOK_SIDE 枚举:

//+------------------------------------------------------------------+
//| MarketBook 的方向                                                 |
//+------------------------------------------------------------------+
enum ENUM_MBOOK_SIDE
{
   MBOOK_ASK,                    // 卖出方
   MBOOK_BID                     // 买入方
};

现在让我们来介绍一下 GetDeviationByVol 方法的源代码:

//+------------------------------------------------------------------+
//| 获取交易量的偏离值。返回 -1.0 如果偏离是                              |
//| 无限 (流动性不足)                                                  |
//+------------------------------------------------------------------+
double CMarketBook::GetDeviationByVol(long vol, ENUM_MBOOK_SIDE side)
{
   int best_ask = InfoGetInteger(MBOOK_BEST_ASK_INDEX);
   int last_ask = InfoGetInteger(MBOOK_LAST_ASK_INDEX); 
   int best_bid = InfoGetInteger(MBOOK_BEST_BID_INDEX);
   int last_bid = InfoGetInteger(MBOOK_LAST_BID_INDEX);
   double avrg_price = 0.0;
   long volume_exe = vol;
   if(side == MBOOK_ASK)
   {
      for(int i = best_ask; i >= last_ask; i--)
      {
         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 <= 0)break;
      }
   }
   else
   {
      for(int i = best_bid; i <= last_bid; i++)
      {
         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 <= 0)break;
      }
   }
   if(volume_exe > 0)
      return -1.0;
   avrg_price/= (double)vol;
   double deviation = 0.0;
   if(side == MBOOK_ASK)
      deviation = avrg_price - MarketBook[best_ask].price;
   else
      deviation = MarketBook[best_bid].price - avrg_price;
   return deviation;
}

如您所见, 代码量显著, 但其计算原理其实并不复杂。首先, 市场深度迭代从最佳方向朝最坏价格执行。迭代会针对每个方向的相关一侧执行。在迭代过程中, 当前交易量被加到总交易量里。如果调用总交易量, 且它与所需的交易量匹配, 则从循环中退出。那么给定交易量的平均入场价计算完毕。最后, 在平均入场价和最佳买/卖价之间的差价计算完毕。差价的绝对值就是评估的滑点。

这种方法需要计算 DOM 的直接迭代。虽然只有 DOM 二分之一的部分迭代, 且大多数时候只是部分, 不过, 这种计算比 DOM 的常用索引计算需要更多的时间。所以, 此计算直接在一个单独的方法里实现并在需要时进行, 即, 仅当它需要获取该信息的清晰形式的时候。

 

2.5. 操作 CMarketBook 类的例程

所以, 我们已覆盖 CMarketBook 类的基本方法, 是时候用它来实施一下。我们的测试例子相当简单易懂, 即使是初学编程者。我们来编写一段测试 EA, 基于 DOM 执行一次性的信息输出。当然, 为此目的编写一个脚本更为恰当, 但通过脚本访问市场深度是不可能的, 只能使用 EA 或指标。我们的 EA 源代码如下:

//+------------------------------------------------------------------+
//|                                               TestMarketBook.mq5 |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <Trade\MarketBook.mqh>     //  包含 CMarketBook 类
CMarketBook Book(Symbol());         // 以当前金融工具初始化类

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
int OnInit()
  {
   PrintMbookInfo();
   return INIT_SUCCEEDED;
  }
//+------------------------------------------------------------------+
//| 脚本程序开始函数                                                   |
//+------------------------------------------------------------------+
void OnTimer()
  {
//---
   
  }
//+------------------------------------------------------------------+
//| 打印 MarketBook 信息                                              |
//+------------------------------------------------------------------+
void PrintMbookInfo()
  {
   Book.Refresh();                  // 更新市场深度状态。
   /* 获取基础统计 */
   int total=Book.InfoGetInteger(MBOOK_DEPTH_TOTAL);
   int total_ask = Book.InfoGetInteger(MBOOK_DEPTH_ASK);
   int total_bid = Book.InfoGetInteger(MBOOK_DEPTH_BID);
   int best_ask = Book.InfoGetInteger(MBOOK_BEST_ASK_INDEX);
   int best_bid = Book.InfoGetInteger(MBOOK_BEST_BID_INDEX);

   printf("市场深度总数: "+(string)total);
   printf("卖出价位数量: "+(string)total_ask);
   printf("买入价位数量: "+(string)total_bid);
   printf("最佳卖出价索引: "+(string)best_ask);
   printf("最佳买入价索引: "+(string)best_bid);
   
   double best_ask_price = Book.InfoGetDouble(MBOOK_BEST_ASK_PRICE);
   double best_bid_price = Book.InfoGetDouble(MBOOK_BEST_BID_PRICE);
   double last_ask = Book.InfoGetDouble(MBOOK_LAST_ASK_PRICE);
   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()));
  }
//+------------------------------------------------------------------+

当在 OFZ2 图表上运行这个测试 EA, 我们可以得到以下报告:

2015.06.16 17:13:23.482 TestMarketBook (OFZ2-9.15,D1)   平均点差: 70
2015.06.16 17:13:23.482 TestMarketBook (OFZ2-9.15,D1)   最坏买入价: 9831
2015.06.16 17:13:23.482 TestMarketBook (OFZ2-9.15,D1)   最坏卖出价: 9999
2015.06.16 17:13:23.482 TestMarketBook (OFZ2-9.15,D1)   最佳买入价: 9840
2015.06.16 17:13:23.482 TestMarketBook (OFZ2-9.15,D1)   最佳卖出价: 9910
2015.06.16 17:13:23.482 TestMarketBook (OFZ2-9.15,D1)   最佳买入价索引: 7
2015.06.16 17:13:23.482 TestMarketBook (OFZ2-9.15,D1)   最佳卖出价索引: 6
2015.06.16 17:13:23.482 TestMarketBook (OFZ2-9.15,D1)   买入价位数量: 2
2015.06.16 17:13:23.482 TestMarketBook (OFZ2-9.15,D1)   卖出价位数量: 7
2015.06.16 17:13:23.482 TestMarketBook (OFZ2-9.15,D1)   市场深度总数: 9

让我们将获取的此金融工具的报告与 DOM 截图比较一下:


图示. 4. 当运行测试报告时 OFZ2 的市场深度

我们已经确认, 收到的索引和价格完全符合目前的市场深度。 

 

第 3 章. 编写您自己的市场深度作为面板指标


3.1. 设计市场深度面板的一般原理创建指标

通过访问 CMarketBook 类, 可以相对简单地创建一个特殊的面板, 直接在图表上显示当前的市场深度。我们将在用户指标的基础上创建面板。选择使用指标作为基础是出于这样一个事实, 即每个图表上只能有存在一个 EA, 而指标可以有无限数量。如果我们采用 EA 作为面板的基础, 则在同一图表上将不再可能利用 EA 进行交易, 这会很不方便。

我们将要提供的市场深度具有在图表上显示和隐藏的能力, 这是因为对于每个图表, 它仅作为一个标准的交易面板而实现。我们将使用相同的按钮来显示或隐藏它:

 

图示. 5. 在 MetaTrader 5 中的标准交易面板

我们的市场深度将被放置在图表的左上角, 与交易面板位于同一地方。这是由于事实上, 放置市场深度指标的图表上已经启动了一个交易 EA。为了不遮挡其位于右上角的图标, 我们将我们的面板移动到左侧。

当创建一个指标时, 需要使用二个系统函数之一的 OnCalculate。由于我们的面板不使用自这些函数接收到的信息, 我们将保留这些函数为空。此外, 指标将不使用任何的图形系列, 所以 indicator_plots 属性在这种情况下将等于零。

OnBookEvent 系统函数将是我们的指标使用的主要函数, 所以我们需要签署当前图表品种以便接收有关的市场深度变化信息。我们将使用已熟知的 MarketBookAdd 函数来订阅。

市场深度面板将以特别的 CBookPanel 类形式实现。现在, 无需了解这个类的进一步相关细节, 我们将提供最初的指标文件代码:

//+------------------------------------------------------------------+
//|                                                   MarketBook.mq5 |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property indicator_chart_window
#property indicator_plots 0
#include <Trade\MarketBook.mqh>
#include "MBookPanel.mqh"

CBookPanel Panel;
//+------------------------------------------------------------------+
//| 自定义指标初始化函数                                                |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- 指标缓存区映射
   MarketBookAdd(Symbol());
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| MarketBook 变化事件                                               |
//+------------------------------------------------------------------+
void OnBookEvent(const string &symbol)
  {
   Panel.Refresh();
  }
//+------------------------------------------------------------------+
//| 图表事件                                                          |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,         // 事件表示符
                  const long& lparam,   // 长整形事件参数
                  const double& dparam, // 双精度形事件参数
                  const string& sparam) // 字符串形事件参数
  {
   Panel.Event(id,lparam,dparam,sparam);
   ChartRedraw();
  }
//+------------------------------------------------------------------+
//| 自定义指标迭代函数                                                 |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &price[])
  {
//---

//--- 返回 prev_calculated 值用于下次调用
   return(rates_total);
  }
//+------------------------------------------------------------------+

现在 CBookPanel 类只包含了其操作的基本要素: 您点击市场深度出现的箭头, 和 "MarketBook" 标签来签署我们未来的 DOM。当运行我们的指标时, 图表看上去如下方式:

 

图示. 6. 图表上 MarketBook 面板的位置

这个类中的每个元素均是从基类 CNode 派生出的独立类。这个类包含基本的方法, 如 Show 和 Hide, 可以在子类中重写。CNode 类还为每个实例生成一个唯一名称, 可令其更方便地使用标准函数来创建图形对象并设置它们的属性。

 

3.2. 点击事件处理以及创建市场深度表格

目前, 我们的指标还不能响应点击箭头, 因此我们将继续进行这项工作。我们要为面板所做的第一件事就是进入 OnChartEvent 事件处理器。我们将调用这个方法作为事件。它将采用取自 OnChartEvent 的参数。此外, 我们将扩展 CNode 基类, 为它提供 CArrayObj 数组, 其中将包含其它 CNode 类型的图形元素。以后它将帮助我们创建更多相同类型的元素 - 市场深度单元。

现在, 我们将要提供 CBookPanel 类和其父类 CNode 的源代码: 

//+------------------------------------------------------------------+
//|                                                   MBookPanel.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#include <Trade\MarketBook.mqh>
#include "Node.mqh"
#include "MBookText.mqh"
#include "MBookFon.mqh"
//+------------------------------------------------------------------+
//| CBookPanel 类                                                    |
//+------------------------------------------------------------------+
class CBookPanel : CNode
  {
private:
   CMarketBook       m_book;
   bool              m_showed;
   CBookText         m_text;
public:
   CBookPanel();
   ~CBookPanel();
   void              Refresh();
   virtual void Event(int id, long lparam, double dparam, string sparam);
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CBookPanel::CBookPanel()
{
   m_elements.Add(new CBookFon(GetPointer(m_book)));
   ObjectCreate(ChartID(), m_name, OBJ_LABEL, 0, 0, 0);
   ObjectSetInteger(ChartID(), m_name, OBJPROP_XDISTANCE, 70);
   ObjectSetInteger(ChartID(), m_name, OBJPROP_YDISTANCE, -3);
   ObjectSetInteger(ChartID(), m_name, OBJPROP_COLOR, clrBlack);
   ObjectSetString(ChartID(), m_name, OBJPROP_FONT, "Webdings");
   ObjectSetString(ChartID(), m_name, OBJPROP_TEXT, CharToString(0x36));
}
CBookPanel::~CBookPanel(void)
{
   OnHide();
   m_text.Hide();
   ObjectDelete(ChartID(), m_name);
}

CBookPanel::Refresh(void)
{

}

CBookPanel::Event(int id, long lparam, double dparam, string sparam)
{
   switch(id)
   {
      case CHARTEVENT_OBJECT_CLICK:
      {
         if(sparam != m_name)return;
         if(!m_showed)OnShow();        
         else OnHide();
         m_showed = !m_showed;
      }
   }
}
//+------------------------------------------------------------------+

更新 DOM 状态的 Refresh 方法还不完整。我们将稍后再创建它。目前的功能已经能够作为我们的市场深度的第一个原型进行展示。到目前为止, 点击箭头时, 只显示标准的灰色画布。当再次点击, 它会消失:

 

图示. 7. 市场深度出现

市场深度看起来不太令人信服, 但我们将继续完善它。

 

3.3. 市场深度单元。

Cells 创建市场深度的基础每个单元是包含有关交易量或价格信息的表元素。此外, 单元可以由颜色区分: 对于买入限价单它被涂成蓝色, 对于卖出限价单 - 粉红色。每个市场深度的单元数量会有差别, 所以, 所有单元需要按需 动态 创建并在存储在一个特殊数据容器 CArrayObj。由于所有单元, 不论它们显示什么, 都具有相同的尺寸和类型, 在类中实现的各种单元类型都具有相同。

对于显示交易量的单元以及显示价格的单元, 将使用一个特殊的 CBookCeil 类。当创建这个类的对象时单元类型将被指定, 所以每个类实例都会知道它应显示来自市场深度的哪些必要信息, 以及背景应涂上什么颜色。CBookCeil 将使用两种图元: 文本标签 OBJ_TEXT_LABEL 和长方形标签 OBJ_RECTANBLE_LABEL。第一个将显示文本, 第二个 - 实际的市场深度单元。

这里是 CBookCeil 类的源代码:

//+------------------------------------------------------------------+
//|                                                   MBookPanel.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#include "Node.mqh"
#include <Trade\MarketBook.mqh>
#include "Node.mqh"
#include "MBookText.mqh"

#define BOOK_PRICE 0
#define BOOK_VOLUME 1

class CBookCeil : public CNode
{
private:
   long  m_ydist;
   long  m_xdist;
   int   m_index;
   int m_ceil_type;
   CBookText m_text;
   CMarketBook* m_book;
public:
   CBookCeil(int type, long x_dist, long y_dist, int index_mbook, CMarketBook* book);
   virtual void Show();
   virtual void Hide();
   virtual void Refresh();
   
};

CBookCeil::CBookCeil(int type, long x_dist, long y_dist, int index_mbook, CMarketBook* book)
{
   m_ydist = y_dist;
   m_xdist = x_dist;
   m_index = index_mbook;
   m_book = book;
   m_ceil_type = type;
}

void CBookCeil::Show()
{
   ObjectCreate(ChartID(), m_name, OBJ_RECTANGLE_LABEL, 0, 0, 0);
   ObjectSetInteger(ChartID(), m_name, OBJPROP_XDISTANCE, m_xdist);
   ObjectSetInteger(ChartID(), m_name, OBJPROP_YDISTANCE, m_ydist);
   ObjectSetInteger(ChartID(), m_name, OBJPROP_COLOR, clrBlack);
   ObjectSetInteger(ChartID(), m_name, OBJPROP_FONTSIZE, 9);
   ObjectSetInteger(ChartID(), m_name, OBJPROP_BORDER_TYPE, BORDER_FLAT);
   m_text.Show();
   m_text.SetXDist(m_xdist+10);
   m_text.SetYDist(m_ydist+2);
   Refresh();
}

void CBookCeil::Refresh(void)
{
   ENUM_BOOK_TYPE type = m_book.MarketBook[m_index].type;
   if(type == BOOK_TYPE_BUY || type == BOOK_TYPE_BUY_MARKET)
      ObjectSetInteger(ChartID(), m_name, OBJPROP_BGCOLOR, clrCornflowerBlue);
   else if(type == BOOK_TYPE_SELL || type == BOOK_TYPE_SELL_MARKET)
      ObjectSetInteger(ChartID(), m_name, OBJPROP_BGCOLOR, clrPink);
   else
      ObjectSetInteger(ChartID(), m_name, OBJPROP_BGCOLOR, clrWhite);
   MqlBookInfo info = m_book.MarketBook[m_index];
   if(m_ceil_type == BOOK_PRICE)
      m_text.SetText(DoubleToString(info.price, Digits()));
   else if(m_ceil_type == BOOK_VOLUME)
      m_text.SetText((string)info.volume);
}

void CBookCeil::Hide(void)
{
   OnHide();
   m_text.Hide();
   ObjectDelete(ChartID(),m_name);
}

该类的主要操作是使用 Show 和 Refresh 方法来执行。后者, 取决于所发送的单元类型, 按其相应颜色涂色, 并在其内显示交易量或价格。为了创建单元, 您必须指定其类型, X 轴上的位置, Y 轴上的位置, 此单元对应的 DOM 索引, 以及市场深度来自哪个单元接收的信息。

一个特殊的私有方法 CreateCeils 将在类中创建单元, 实现一个 DOM 基板。这里是源代码:

void CBookFon::CreateCeils()
{
   int total = m_book.InfoGetInteger(MBOOK_DEPTH_TOTAL);
   for(int i = 0; i < total; i++)
   {
      CBookCeil* Ceil = new CBookCeil(0, 12, i*15+20, i, m_book);
      CBookCeil* CeilVol = new CBookCeil(1, 63, i*15+20, i, m_book);
      m_elements.Add(Ceil);
      m_elements.Add(CeilVol);
      Ceil.Show();
      CeilVol.Show();
   }
}

它可通过点击箭头调用, 展开市场深度。

现在一切就绪, 可以创建我们的新版市场深度了。修改并编译项目后, 我们的指标已经获得了新的形式:

 

图式. 8. 市场深度指标的第一个版本

3.4. 显示在市场深度里的交易量直方条

所获得的市场深度已经执行基本功能 - 它显示交易价位, 成交量和买卖限价单的价格。因此, 市场深度值的每一次变化也可以改变相应的单元数值。然而, 通过视觉不容易跟踪所获得表格内的交易量。例如, 在 MetaTrader 5 的标准 DOM 交易量显示为直方条的背景, 即显示当前市场深度最大交易量的相对大小。此外, 它不应妨碍在我们的市场深度里实现类似的功能。

这里有不同的方式来解决这个问题。最简单的解决方案是在 CBookCeil 类里直接进行所有必要的计算。因此它需要在其 Refresh 方法里编写以下内容:

void CBookCeil::Refresh(void)
{
   ...
   MqlBookInfo info = m_book.MarketBook[m_index];
   ...
   //更新市场深度直方条
   int begin = m_book.InfoGetInteger(MBOOK_LAST_ASK_INDEX);
   int end = m_book.InfoGetInteger(MBOOK_BEST_ASK_INDEX);
   long max_volume = 0;
   if(m_ceil_type != BOOK_VOLUME)return;
   for(int i = begin; i < end; i++)
   {
      if(m_book.MarketBook[i].volume > max_volume)
         max_volume = m_book.MarketBook[i].volume;
   }
   double delta = 1.0;
   if(max_volume > 0)
      delta = (info.volume/(double)max_volume);
   long size = (long)(delta * 50.0);
   ObjectSetInteger(ChartID(), m_name, OBJPROP_XSIZE, size);
   ObjectSetInteger(ChartID(), m_name, OBJPROP_YDISTANCE, m_ydist);
}

在完整的 DOM 迭代方法中, 有一个 DOM 的最大交易量, 那么当前交易量由最大切分。所获取的份额乘以交易量表格单元的最大宽度 (它以 50 像素的常量表示)。画布所获得的宽度就是所需的直方条:

 

图示. 9. 带有交易量直方条的市场深度

不过, 该代码中的问题是, 每次调用 Refresh 时, DOM 迭代在每个单元里都要完成。对于拥有 40 个元素的市场深度, 这意味着每次市场深度更新, 将会进行 800 次迭代。每个单元只为自身进行市场迭代, 所以每个单元内的迭代由二十次迭代组成 (市场深度除半)。现代计算机如此处理任务, 是一个非常低效的操作方法, 特别是鉴于有必要用最快捷、最高效的算法操纵市场深度。

 

3.5. 在市场深度里快速计算最大交易量, 迭代的优化

不幸的是, 不太可能剔除市场深度的完整迭代。每次市场深度刷新后, 最大交易量、价位可能会有大幅变化。然而, 我们可以尽量减少迭代的次数。为此目的, 您应该了解在两次 Refresh 调用之间 DOM 迭代不超过一次。第二件您要做的事就是尽量减少完整迭代的调用次数。为此目的, 您必须使用延迟计算, 或者换句话说, 仅在明确需要时才进行这种计算。我们将所有的计算直接转移到 CMarketBook 市场深度类中, 并且编写一个特殊的, 位于 CMarketBook 内部的计算子类 CBookCalculation。请查看以下源码: 

class CMarketBook;

class CBookCalculation
{
private:
   int m_max_ask_index;         // 最大卖出交易量索引
   long m_max_ask_volume;       // 最大卖出交易量
   
   int m_max_bid_index;         // 最大买入交易量索引
   long m_max_bid_volume;       // 最大买入交易量
   
   long m_sum_ask_volume;       // DOM 中卖出交易量总数
   long m_sum_bid_volume;       // DOM 中买入交易量总数
   
   bool m_calculation;          // 所有计算执行完毕的指示标志
   CMarketBook* m_book;         // 市场深度指标
   
   void Calculation(void)
   {
      // 对于卖方
      int begin = (int)m_book.InfoGetInteger(MBOOK_LAST_ASK_INDEX);
      int end = (int)m_book.InfoGetInteger(MBOOK_BEST_ASK_INDEX);
      for(int i = begin; i < end; i++)
      {
         if(m_book.MarketBook[i].volume > m_max_ask_volume)
         {
            m_max_ask_index = i;
            m_max_ask_volume = m_book.MarketBook[i].volume;
         }
         m_sum_ask_volume += m_book.MarketBook[i].volume;
      }
      // 对于买方
      begin = (int)m_book.InfoGetInteger(MBOOK_BEST_BID_INDEX);
      end = (int)m_book.InfoGetInteger(MBOOK_LAST_BID_INDEX);
      for(int i = begin; i < end; i++)
      {
         if(m_book.MarketBook[i].volume > m_max_bid_volume)
         {
            m_max_bid_index = i;
            m_max_bid_volume = m_book.MarketBook[i].volume;
         }
         m_sum_bid_volume += m_book.MarketBook[i].volume;
      }
      m_calculation = true;
   }
   
public:
   CBookCalculation(CMarketBook* book)
   {
      Reset();
      m_book = book;
   }
   
   void Reset()
   {
      m_max_ask_volume = 0.0;
      m_max_bid_volume = 0.0;
      m_max_ask_index = -1;
      m_max_bid_index = -1;
      m_sum_ask_volume = 0;
      m_sum_bid_volume = 0;
      m_calculation = false;
   }
   int GetMaxVolAskIndex()
   {
      if(!m_calculation)
         Calculation();
      return m_max_ask_index;
   }
   
   long GetMaxVolAsk()
   {
      if(!m_calculation)
         Calculation();
      return m_max_ask_volume;
   }
   int GetMaxVolBidIndex()
   {
      if(!m_calculation)
         Calculation();
      return m_max_bid_index;
   }
   
   long GetMaxVolBid()
   {
      if(!m_calculation)
         Calculation();
      return m_max_bid_volume;
   }
   long GetAskVolTotal()
   {
      if(!m_calculation)
         Calculation();
      return m_sum_ask_volume;
   }
   long GetBidVolTotal()
   {
      if(!m_calculation)
         Calculation();
      return m_sum_bid_volume;
   }
};

所有的市场深度迭代以及资源密集型计算均隐藏在 Calculate 私有方法的内部。只有计算标志 m_calculate 重置为 false 条件时它才会被调用。重置标志只会发生在 Reset 方法里。由于这个类是专为操纵 CMarketBook 类而设计, 只有这个类可以访问它。

市场深度的 Refresh 方法刷新之后, CMarketBook 类通过调用它的 Reset 方法复位计算模块的条件。由于在两次刷新之间, 市场深度的完整迭代不会超过一次。还使用了一次预留执行。换言之, 只有当来自六个公共可用方法之一的清楚调用时, CBookCalcultae 类的 Calculate 方法才被调用。

除了查找交易量, 执行市场深度完整迭代的类还添加了一个字段, 含有买/卖限价单的总数。不需要额外的时间来计算这些参数, 因为数组的总周期已被计算。

现在, 用智能按需迭代取代市场深度的常规迭代。这极大降低了资源的使用, 使得市场深度的操作十分快速有效。

 

3.6. 收尾: 交易量直方条和分界线

我们已经几乎完成创建指标的任务。查询最大交易量的实际需要已经帮助我们创建了一套计算经济所需指标的有效方法。如果在将来, 我们要添加新的计算参数到我们的市场深度, 这将很容易做到。为此目的, 扩展我们的 CBookCalculate 类就好了, 添加相关方法来, 并输入相应的修饰符 ENUM_MBOOK_INFO_INTEGER 和ENUM_MBOOK_INFO_DOUBLE 枚举。

现在, 我们必须利用我们已完成的工作, 并为每个单元重写 Refresh 方法:

void CBookCeil::Refresh(void)
{
   ENUM_BOOK_TYPE type = m_book.MarketBook[m_index].type;
   long max_volume = 0;
   if(type == BOOK_TYPE_BUY || type == BOOK_TYPE_BUY_MARKET)
   {
      ObjectSetInteger(ChartID(), m_name, OBJPROP_BGCOLOR, clrCornflowerBlue);
      max_volume = m_book.InfoGetInteger(MBOOK_MAX_BID_VOLUME);
   }
   else if(type == BOOK_TYPE_SELL || type == BOOK_TYPE_SELL_MARKET)
   {
      ObjectSetInteger(ChartID(), m_name, OBJPROP_BGCOLOR, clrPink);
      max_volume = m_book.InfoGetInteger(MBOOK_MAX_ASK_VOLUME); //交易量之前已计算, 不会发生循环重复出现
   }
   else
      ObjectSetInteger(ChartID(), m_name, OBJPROP_BGCOLOR, clrWhite);
   MqlBookInfo info = m_book.MarketBook[m_index];
   if(m_ceil_type == BOOK_PRICE)
      m_text.SetText(DoubleToString(info.price, Digits()));
   else if(m_ceil_type == BOOK_VOLUME)
      m_text.SetText((string)info.volume);
   if(m_ceil_type != BOOK_VOLUME)return;
   double delta = 1.0;
   if(max_volume > 0)
      delta = (info.volume/(double)max_volume);
   long size = (long)(delta * 50.0);
   ObjectSetInteger(ChartID(), m_name, OBJPROP_XSIZE, size);
   ObjectSetInteger(ChartID(), m_name, OBJPROP_YDISTANCE, m_ydist);
}

外观上, 我们的面板指标与之前的版本工作方式相同, 但实际上直方条的计算速度显著增加。什么是编程艺术, 这就是有关 - 创建有效且易于使用的算法, 在相应模块 (类) 的私有方法里隐藏实现它们的复杂逻辑。

随着交易量直方条的出现, 买/卖价交易量之间的分界线变得非常模糊。所以, 我们要在 CBookPanel 类中创建这条线, CBookLine 特殊子类实现了这个功能:

class CBookLine : public CNode
{
private:
   long m_ydist;
public:
   CBookLine(long y){m_ydist = y;}
   virtual void Show()
   {
      ObjectCreate(ChartID(),     m_name, OBJ_RECTANGLE_LABEL, 0, 0, 0);
      ObjectSetInteger(ChartID(), m_name, OBJPROP_YDISTANCE, m_ydist);
      ObjectSetInteger(ChartID(), m_name, OBJPROP_XDISTANCE, 13);
      ObjectSetInteger(ChartID(), m_name, OBJPROP_YSIZE, 3);
      ObjectSetInteger(ChartID(), m_name, OBJPROP_XSIZE, 108);
      ObjectSetInteger(ChartID(), m_name, OBJPROP_COLOR, clrBlack);
      ObjectSetInteger(ChartID(), m_name, OBJPROP_BGCOLOR, clrBlack);
      ObjectSetInteger(ChartID(), m_name, OBJPROP_BORDER_TYPE, BORDER_FLAT);
   }
};

这是一个非常简单的类, 基本上, 只是确定其位置。这条线的 Y 轴位置, 必须在 Show 方法里创建它的时候进行计算。知道最佳卖出价的索引则相对比较容易做到这一点:

long best_bid = m_book.InfoGetInteger(MBOOK_BEST_BID_INDEX);
long y = best_bid*15+19;

在这种情况下, best_bid 单元的索引乘以每个单元的宽度 (15 像素), 再加上 19 像素的附加常数。

我们的市场深度终于拥有了能获得愉快使用体验的最小外观和功能。当然, 还有更多的可以做。如果需要的话, 我们的指标可以做到更接近标准 MetaTrader 5 市场深度的功能。

但是, 本文的主要目的并不在此。创建市场深度面板的唯一目的就是展示 CMarketBook 类的可行性。它有助于令这个类更快、更好、功能更强大, 并因此完全实现其目标。我们将向您展示一个简短的视频, 其中揭示了我们迄今完成的所有工作。下面是我们的 DOM 动态面板:



3.7. 在 DOM 里添加有关所交易金融工具的限价订单总数的属性信息

莫斯科交易所的一个显着特点是传送实时的限价单总数的信息。本文着重市场深度的操纵, 而非定位于任何特定的市场。不过, 尽管这些信息是特定的 (某个特定交易平台特有的), 但在终端的系统级别依然可用。此外, 市场深度还提供扩展数据。这决定于属性修饰符枚举的扩展, 并在市场深度 CMarketBook 类里面直接包含这些属性的支持。

莫斯科股票交易所提供以下实时信息:

未平合约与市场上的限价单数量并未直接相关 (如其当前的流动性), 然而, 这些信息经常需要与有关的限价单信息结合, 所以通过 CMarketBook 类来访问它看起来也合适。要访问这些信息, 您必须使用 SymbolInfoInteger 和 SymbolInfoDouble 函数。然而, 为了从单一位置访问数据, 我们将在 InfoGetInteger 和 InfoGetDouble 函数里引入更多的枚举和变化来拓展我们的市场深度类:

long CMarketBook::InfoGetInteger(ENUM_MBOOK_INFO_INTEGER property)
{
   switch(property)
   {
      ...
      case MBOOK_BUY_ORDERS:
         return SymbolInfoInteger(m_symbol, SYMBOL_SESSION_BUY_ORDERS);
      case MBOOK_SELL_ORDERS:
         return SymbolInfoInteger(m_symbol, SYMBOL_SESSION_SELL_ORDERS);
      ...
   }
   return 0;
}

 

double CMarketBook::InfoGetDouble(ENUM_MBOOK_INFO_DOUBLE property)
{
   switch(property)
   {
      ...
      case MBOOK_BUY_ORDERS_VOLUME:
         return SymbolInfoDouble(m_symbol, SYMBOL_SESSION_BUY_ORDERS_VOLUME);
      case MBOOK_SELL_ORDERS_VOLUME:
         return SymbolInfoDouble(m_symbol, SYMBOL_SESSION_SELL_ORDERS_VOLUME);
      case MBOOK_OPEN_INTEREST:
         return SymbolInfoDouble(m_symbol, SYMBOL_SESSION_INTEREST);
   }
   return 0.0;  
}

如您所见, 代码非常简单。事实上, 它重复 MQL 的标准功能。但是, 将它添加到 CMarketBook 类的要点在于为用户提供便利和 集中式 模块来访问限价单信息以及它们的价位。


第 4 章. CMarketBook 类的文档

我们已经完成了描述, 并创建了操纵市场深度的类 CMarketBook。第四章包含了它的公共方法的文档。使用这些文档, 类的操纵变得简单明了, 即使是刚入门的程序员也一样。此外, 这章也能方便地用作类操纵的小指南。


4.1. 从市场深度里获取基本信息以及操纵它的方法

Refresh() 方法

它刷新市场深度条件。对于每次调用 OnBookEvent 系统事件 (市场深度已有变化), 它也需要调用此方法。

void        Refresh(void);

用法

在第四章的相关章节查找用法例程。

 

InfoGetInteger() 方法

返回 ENUM_MBOOK_INFO_INTEGER 修饰符相对应的市场深度属性。在 ENUM_MBOOK_INFO_INTEGER 列表里可以找到一份完整的支持功能清单。

long        InfoGetInteger(ENUM_MBOOK_INFO_INTEGER property);

返回值

长整形市场深度属性的整数值。失败时返回 -1。

用法

在第四章的相关章节查找用法例程。 

 

InfoGetDouble() 方法

返回 ENUM_MBOOK_INFO_DOUBLE 修饰符相对应的市场深度属性。在 ENUM_MBOOK_INFO_DOUBLE 列表里可以找到一份完整的支持功能清单。

double      InfoGetDouble(ENUM_MBOOK_INFO_DOUBLE property);

返回值

市场深度属性的双精度值。失败时返回 -1.0。

用法

在第四章的相关章节查找用法例程。  

 

IsAvailable() 方法

返回 true, 如果市场深度的信息可用, 否则 false。这个方法必须在操纵市场深度类检查相应类型信息之前调用。

bool        IsAvailable(void);

返回值

True, 如果市场深度可用于进一步操作, 否则 false。

 

SetMarketBookSymbol() 方法

设置品种, 市场深度即将操作的品种。也可以在创建 CMarketBook 类实例时设置市场深度的品种, 在构造函数中明确说明使用的品名。

bool        SetMarketBookSymbol(string symbol);

返回值

True, 如果品种可用于交易, 否则 false。

 

GetMarketBookSymbol() 方法

返回金融工具的品名, 类的当前实例设置的市场深度操作的品种。 

string      GetMarketBookSymbol(void);

返回值

类的当前实例显示的市场深度操作的品种。NULL, 如果金融工具未选择或不可用 

 

GetDeviationByVol() 方法

返回入场时市价单可能的滑点。这个值只是评估, 如果市场深度在入场时已有变化, 其所获取的滑点可能与该函数以前计算的不同。不过, 这个功能提供了相当精确的评估滑点, 且在入场时, 可作为附加信息的来源。

该方法有两个参数: 意向交易的额度, 以及完成交易时使用的流动性类型的迭代指示。例如, 卖出限价单的流动性将用于买入, 且在此种情况下 MBOOK_ASK 类型将作为指定 一方。对于卖出, 反之, MBOOK_BID 将作为指示。请阅读 ENUM_BOOK_SIDE 枚举的说明获取更多信息。

double     GetDeviationByVol(long vol, ENUM_MBOOK_SIDE side);

参数:

返回值

潜在的金融工具的滑点点数。

 

4.2. CMarketBook 类的枚举和修饰符

枚举 ENUM_MBOOK_SIDE

ENUM_BOOK_SIDE 枚举包含流动性类型的指示修饰符。枚举和字段描述列表如下:

字段描述
MBOOK_ASK 指示流动性由卖出限价单提供。
MBOOK_BID 指示流动性由买入限价单提供。

 

每笔市价单可以由限价单执行。依据订单方向, 买入或卖出限价单将被使用。买入交易的下方将是一笔或若干笔卖出限价单。卖出交易的下方将是一笔或若干笔买入限价单。这样的方式修饰符可以指示市场深度两部分之一: 买方或卖方。修饰符由 GetDeviationByVol 函数使用, 在操作时, 您所需要知道的就是期望的市场交易将会用到哪一侧的流动性。

 

枚举 ENUM_MBOOK_INFO_INTEGER

枚举 ENUM_MBOOK_INFO_INTEGER 包含必须用 InfoGetInteger 方法才能获取的属性修饰符。枚举和字段描述列表如下:

字段描述
MBOOK_BEST_ASK_INDEX 最佳卖出价索引
MBOOK_BEST_BID_INDEX 最佳买入价索引
MBOOK_LAST_ASK_INDEX 最后的最坏卖出价索引
MBOOK_LAST_BID_INDEX 最后的最坏买入价索引
MBOOK_DEPTH_ASK 来自卖方或其交易级别总数的市场深度
MBOOK_DEPTH_BID 来自买方或其交易级别总数的市场深度
MBOOK_DEPTH_TOTAL 市场深度总数或买入和卖出级别数量
MBOOK_MAX_ASK_VOLUME 最大卖出交易量
MBOOK_MAX_ASK_VOLUME_INDEX 最大卖出交易量索引
MBOOK_MAX_BID_VOLUME 最大买入交易量
MBOOK_MAX_BID_VOLUME_INDEX 最大买入交易量索引
MBOOK_ASK_VOLUME_TOTAL 在市场深度里当前可用的卖出限价单总数
MBOOK_BID_VOLUME_TOTAL  在市场深度里当前可用的买入限价单总数
MBOOK_BUY_ORDERS 在股票市场里当前可用的买入限价单总数
MBOOK_SELL_ORDERS 在股票市场里当前可用的卖出限价单总数

 

枚举 ENUM_MBOOK_INFO_DOUBLE

枚举 ENUM_MBOOK_INFO_DOUBLE 包含必须用 InfoGetDouble 方法才能获取的属性修饰符。枚举和字段描述列表如下:

字段描述
MBOOK_BEST_ASK_PRICE 最佳卖出价
MBOOK_BEST_BID_PRICE 最佳买入价
MBOOK_LAST_ASK_PRICE 最坏或最后的卖出价
MBOOK_LAST_BID_PRICE 最坏或最后的买入价
MBOOK_AVERAGE_SPREAD 最佳买入价和最佳卖出价之间的平均差价, 或点差。
MBOOK_OPEN_INTEREST  未平合约
MBOOK_BUY_ORDERS_VOLUME 买单数量
MBOOK_SELL_ORDERS_VOLUME  卖单数量

 

4.3. 使用 CMarketBook 类的例程

这个例程包含一段组成 EA 的源代码, 它在起始点显示市场深度的基本信息:

//+------------------------------------------------------------------+
//|                                               TestMarketBook.mq5 |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <Trade\MarketBook.mqh>     //  包含 CMarketBook 类
CMarketBook Book(Symbol());         // 以当前金融工具初始化类

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
int OnInit()
  {
   PrintMbookInfo();
   return INIT_SUCCEEDED;
  }
//+------------------------------------------------------------------+
//| 脚本程序开始函数                                                   |
//+------------------------------------------------------------------+
void OnTimer()
  {
//---
   
  }
//+------------------------------------------------------------------+
//| 打印 MarketBook 信息                                              |
//+------------------------------------------------------------------+
void PrintMbookInfo()
  {
   Book.Refresh();                                                   // 更新市场深度状态。
//--- 获取主要的整数型统计值
   int total=(int)Book.InfoGetInteger(MBOOK_DEPTH_TOTAL);            // 获取市场深度总数
   int total_ask = (int)Book.InfoGetInteger(MBOOK_DEPTH_ASK);        // 获取卖出级别数量
   int total_bid = (int)Book.InfoGetInteger(MBOOK_DEPTH_BID);        // 获取买入级别数量
   int best_ask = (int)Book.InfoGetInteger(MBOOK_BEST_ASK_INDEX);    // 获取最佳卖出价索引
   int best_bid = (int)Book.InfoGetInteger(MBOOK_BEST_BID_INDEX);    // 获取最佳买入价索引

//--- 显示基本统计
   printf("市场深度总数: "+(string)total);
   printf("卖出价位数量: "+(string)total_ask);
   printf("买入价位数量: "+(string)total_bid);
   printf("最佳卖出价索引: "+(string)best_ask);
   printf("最佳买入价索引: "+(string)best_bid);
   
//--- 获取主要的双精度型统计值
   double best_ask_price = Book.InfoGetDouble(MBOOK_BEST_ASK_PRICE); // 获取最佳卖出价
   double best_bid_price = Book.InfoGetDouble(MBOOK_BEST_BID_PRICE); // 获取最佳买入价
   double last_ask = Book.InfoGetDouble(MBOOK_LAST_ASK_PRICE);       // 获取最坏卖出价
   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()));
  }
//+------------------------------------------------------------------+

 

结论

本文的展开相当动态。我们已经从技术角度分析了市场深度, 并提出了一个高性能类容器来操纵它。作为一个例子, 我们已经基于这个类容器创建了一款市场深度指标, 它可以紧凑地显示在金融工具的价格图表上。

我们的市场深度指标是很基本的, 它仍然缺少了一些东西。不过, 主要的目的实现了 - 我们确信, 利用我们已经创建的 CMarketBook 类, 我们能够相对快速地构建复杂 EA 和指标, 来分析金融工具当前的流动性。在设计 CMarketBook 类时, 已经为性能付出了大量注意力, 因为市场深度拥有一个非常动态的表格, 每分钟变化数百次。

在文章中描述的类, 可成为您的剥头皮或高频系统的坚实基础。可随意添加特定功能到您的系统中。为此, 只需从 CMarketBook 衍生创建您的市场深度类, 并编写您需要的扩展方法。我们希望, 市场深度提供的基本属性, 能令您的工作更容易、更可靠。