Skip to content

市场深度

除了以报价形式在终端中接收的几种类型的最新市场价格数据(卖价/买价/最新价)以及最后交易的成交量之外,MetaTrader 5 还支持市场深度(订单簿)功能。市场深度是一个记录数组,包含围绕当前市场价格下达的买入和卖出订单的成交量信息。成交量是在当前价格上下的几个价位层级上进行汇总的,价格变动的最小增量根据交易品种的规格而定。正如我们所见,最大的订单簿规模(价格层级数量)是在 SYMBOL_TICKS_BOOKDEPTH 交易品种属性中设置的。

终端用户了解界面中的市场深度功能及其操作原理。如果您需要进一步的详细信息,请查阅相关文档。

订单簿包含了扩展的市场信息,通常被称为 “市场深度”。了解这些信息使您能够创建更复杂的交易系统。

实际上,关于一笔报价的信息只是订单簿的一小部分。从某种简化的意义上来说,一笔报价是一个两级的订单簿,包含一个最近的卖价(可用报价)和一个最近的买价(可用需求)。此外,报价并不提供这些价格下的订单成交量。

市场深度的变化可能比报价的变化要频繁得多,因为市场深度的变化不仅会受到已成交交易的影响,还会受到挂单限价订单成交量变化的影响。

通常,订单簿和行情数据(报价、交易)的数据提供商是不同的实体,并且报价事件(智能交易系统中的 OnTick 或指标中的 OnCalculate)与市场深度事件并不匹配。这两个线程都是异步且并行到达的,但最终都会进入 MQL 程序的事件队列中。

需要注意的是,一般来说,市场深度数据可用于交易所交易品种,但也存在正反两个方面的例外情况:

  • 由于这样或那样的原因,某个交易所交易品种可能没有市场深度数据。
  • 经纪商可能会根据他们收集到的关于客户订单的信息,为场外交易品种提供市场深度数据。

在 MQL5 中,智能交易系统和指标都可以获取市场深度数据。通过使用特殊函数(MarketBookAdd、MarketBookRelease),程序可以启用或禁用对平台上市场深度变化通知的订阅。为了接收这些通知,程序必须在其代码中定义 OnBookEvent 事件处理函数。在接收到通知后,可以使用 MarketBookGet 函数读取订单簿数据。

终端会保存行情报价和报价数据的历史记录,但不会保存市场深度数据的历史记录。特别是,用户或 MQL 程序可以下载所需追溯期内的历史数据(如果经纪商有这些数据的话),并在这些数据上对智能交易系统和指标进行测试。

相比之下,市场深度数据仅在线广播,在测试器中不可用。经纪商的服务器上没有市场深度数据的存档。要在测试器中模拟订单簿的行为,您应该在线收集市场深度历史数据,然后从在测试器中运行的 MQL 程序中读取这些数据。您可以在 MQL5 市场中找到现成的相关产品。

管理市场深度事件的订阅

终端以订阅方式接收市场深度信息:MQL 程序必须通过调用相应的函数 MarketBookAddMarketBookRelease 来表明其接收市场深度(订单簿)事件的意图,或者相反,终止其订阅。

订阅市场深度信息

MarketBookAdd 函数用于订阅指定交易品种的订单簿变化通知。因此,你可以订阅多个交易品种的订单簿,而不仅仅是当前图表的交易品种。

c
bool MarketBookAdd(const string symbol)

通常,这个函数会在 OnInit 函数中调用,或者在长生命周期对象的类构造函数中调用。订单簿变化的通知会以 OnBookEvent 事件的形式发送给程序,因此,为了处理这些事件,程序必须有一个同名的处理函数。

如果在调用该函数之前,指定的交易品种没有在市场报价中被选中,它会自动添加到该窗口中。

取消市场深度信息订阅

MarketBookRelease 函数用于取消对指定订单簿变化的通知订阅。

c
bool MarketBookRelease(const string symbol)

通常,这个函数应该在 OnDeinit 函数中调用,或者在长生命周期对象的类析构函数中调用。

这两个函数在成功时返回 true,否则返回 false

订阅计数器机制

对于在同一图表上运行的所有应用程序,会按交易品种维护单独的订阅计数器。也就是说,图表上可以有对不同交易品种的多个订阅,并且每个订阅都有自己的计数器。

通过单次调用任何一个函数进行的订阅或取消订阅操作,只会改变特定图表上特定交易品种的订阅计数器。这意味着两个图表可以订阅同一个交易品种的 OnBookEvent 事件,但订阅计数器的值可能不同。

订阅计数器的初始值为零。每次调用 MarketBookAdd 时,给定图表上指定交易品种的订阅计数器会增加 1(图表交易品种和 MarketBookAdd 中的交易品种不必匹配)。调用 MarketBookRelease 时,图表内指定交易品种的订阅计数器会减少 1。

只要图表内某个交易品种的订阅计数器大于零,就会为该交易品种生成 OnBookEvent 事件。因此,每个包含 MarketBookAdd 调用的 MQL 程序在工作结束时,必须使用 MarketBookRelease 正确取消对每个交易品种事件的订阅。为此,你应该确保 MarketBookAddMarketBookRelease 的调用次数相匹配。MQL5 不允许你查询计数器的值。

示例代码分析

下面是一个简单的无缓冲区指标 MarketBookAddRelease.mq5 的代码,它在启动时启用对订单簿的订阅,并在卸载时禁用订阅。在 WorkSymbol 输入参数中,你可以指定要订阅的交易品种。如果留空(默认值),则会为当前图表的交易品种发起订阅。

c
input string WorkSymbol = ""; // WorkSymbol (empty means current chart symbol)

const string _WorkSymbol = StringLen(WorkSymbol) == 0 ? _Symbol : WorkSymbol;
string symbols[];

void OnInit()
{
    const int n = StringSplit(_WorkSymbol, ',', symbols);
    for(int i = 0; i < n; ++i)
    {
        if(!PRTF(MarketBookAdd(symbols[i])))
            PrintFormat("MarketBookAdd(%s) failed", symbols[i]);
    }
}

int OnCalculate(const int rates_total, const int prev_calculated, const int, const double &price[])
{
    return rates_total;
}

void OnDeinit(const int)
{
    for(int i = 0; i < ArraySize(symbols); ++i)
    {
        if(!PRTF(MarketBookRelease(symbols[i])))
            PrintFormat("MarketBookRelease(%s) failed", symbols[i]);
    }
}

作为一个额外的功能,允许指定多个用逗号分隔的交易品种。在这种情况下,会请求对所有交易品种的订阅。

当指标启动时,日志中会显示订阅成功的标志或错误代码。然后,指标会在 OnDeinit 处理函数中尝试取消对事件的订阅。

  • 默认设置下:在具有可用订单簿的交易品种的图表上,日志中会有如下记录:
MarketBookAdd(symbols[i])=true / ok
MarketBookRelease(symbols[i])=true / ok
  • 在没有订单簿的交易品种的图表上:会看到错误代码:
MarketBookAdd(symbols[i])=false / BOOKS_CANNOT_ADD(4901)
MarketBookAdd(XPDUSD) failed
MarketBookRelease(symbols[i])=false / BOOKS_CANNOT_DELETE(4902)
MarketBookRelease(XPDUSD) failed

你可以通过在输入参数 WorkSymbol 中指定存在或不存在的交易品种来进行实验。在下一节中,我们将考虑订阅多个交易品种订单簿的情况。

接收市场深度变化事件

当订单簿状态发生变化时,终端会生成 OnBookEvent 事件。该事件由在源代码中定义的 OnBookEvent 函数进行处理。为了让终端开始向 MQL 程序发送特定品种的 OnBookEvent 通知,必须首先使用 MarketBookAdd 函数订阅接收这些通知。

要取消对某个品种接收 OnBookEvent 事件,可调用 MarketBookRelease 函数。

OnBookEvent 事件是广播式的,这意味着图表上只要有一个 MQL 程序订阅了 OnBookEvent 事件,同一图表上的所有其他程序也将开始接收这些事件,前提是它们的代码中有 OnBookEvent 处理程序。因此,有必要分析作为参数传递给处理程序的品种名称。

OnBookEvent 处理程序的原型如下:

c
void OnBookEvent(const string &symbol)

即使前一个 OnBookEvent 事件的处理尚未完成,OnBookEvent 事件也会被排队。

重要的是,OnBookEvent 事件只是通知,并不提供订单簿的状态。要获取市场深度数据,需调用 MarketBookGet 函数。

然而,需要注意的是,即使直接从 OnBookEvent 处理程序中调用 MarketBookGet,它也只会获取调用 MarketBookGet 时订单簿的当前状态,这不一定与触发发送 OnBookEvent 事件时的订单簿状态一致。当一系列非常快速的订单簿变化到达终端时,就可能发生这种情况。

鉴于此,为了获得最完整的市场深度变化时间顺序,我们需要编写 OnBookEvent 的实现,并优先考虑执行速度的优化。

同时,在 MQL5 中没有一种绝对可靠的方法来获取所有唯一的市场深度状态。

如果你的程序开始成功接收通知,但在市场开盘时通知消失了(而报价仍在继续传来),这可能表明订阅存在问题。特别是,另一个设计不良的 MQL 程序可能取消订阅的次数过多。在这种情况下,建议在预定义的超时时间(例如,几十秒或一分钟)后,使用新的 MarketBookAdd 调用重新订阅。

无缓冲指标 MarketBookEvent.mq5 的一个示例允许你跟踪 OnBookEvent 事件的到达,并在注释中打印品种名称和当前时间(毫秒系统计数器)。为了清晰起见,我们使用 Comments.mqh 文件中的多行注释函数(在“在图表窗口中显示消息”部分)。

有趣的是,如果将输入参数 WorkSymbol 留空(默认值),该指标本身不会发起对订单簿的订阅,但能够拦截同一图表上其他 MQL 程序请求的消息。让我们来验证一下。

c
#include <MQL5Book/Comments.mqh>
   
input string WorkSymbol = ""; // 工作品种(如果为空,拦截其他程序发起的事件)
   
void OnInit()
{
   if(StringLen(WorkSymbol))
   {
      PRTF(MarketBookAdd(WorkSymbol));
   }
   else
   {
      Print("Start listening to OnBookEvent initiated by other programs");
   }
}
   
void OnBookEvent(const string &symbol)
{
   ChronoComment(symbol + " " + (string)GetTickCount());
}
 
void OnDeinit(const int)
{
   Comment("");
   if(StringLen(WorkSymbol))
   {
      PRTF(MarketBookRelease(WorkSymbol));
   }
}

让我们使用默认设置(不进行自身订阅)运行 MarketBookEvent,然后添加上一节中的 MarketBookAddRelease 指标,并为其指定几个具有可用订单簿的品种列表(在下面的示例中,是 “XAUUSD,BTCUSD,USDCNH”)。在哪个图表上运行这些指标并不重要:可以是完全不同的品种,比如 EURUSD。

在启动 MarketBookEvent 后,图表将为空(没有注释),因为还没有进行订阅。一旦 MarketBookAddRelease 启动(日志中应该会出现三行,其中成功订阅的状态等于 true),随着这些品种的订单簿更新,它们的名称将交替出现在注释中(我们还没有学习如何读取订单簿;这将在下一节讨论)。

以下是屏幕上的显示情况:

plaintext
Notifications about changing the order books of "foreign" symbols
Notifications about changes in the order books of "third-party" symbols

如果现在移除 MarketBookAddRelease 指标,它将取消其订阅,注释也将停止更新。随后移除 MarketBookEvent 指标将清除注释。

请注意,在取消订阅的请求发出到市场深度事件实际停止更新注释之间,会经过一些时间(一两秒)。

你可以在图表上单独运行 MarketBookEvent 指标,在其 WorkSymbol 参数中指定某个品种,以确保在同一个应用程序内通知能够正常工作。之前使用 MarketBookAddRelease 只是为了演示通知的广播性质。换句话说,在一个程序中启用对订单簿变化的订阅确实会影响另一个程序接收通知。

读取当前市场深度数据

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

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

对于接收数组,你可以预先为足够数量的记录分配内存。如果动态数组的大小为零或不足,终端会自行为此数组分配内存。

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

MarketBookGet 通常直接在 OnBookEvent 处理程序代码中,或在从该处理程序调用的函数中使用。

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

c
struct MqlBookInfo 
{ 
   ENUM_BOOK_TYPE type;            // 请求类型 
   double         price;           // 价格 
   long           volume;          // 交易量 
   double         volume_real;     // 高精度交易量 
};

枚举类型 ENUM_BOOK_TYPE 包含以下成员:

标识符描述
BOOK_TYPE_SELL卖出请求
BOOK_TYPE_BUY买入请求
BOOK_TYPE_SELL_MARKET以市场价格卖出请求
BOOK_TYPE_BUY_MARKET以市场价格买入请求

在订单簿中,卖出订单位于上半部分,买入订单位于下半部分。通常情况下,这会导致元素按从高价格到低价格的顺序排列。换句话说,在索引 0 下方是最高价格,最后一条记录是最低价格,并且它们之间的价格逐渐降低。在这种情况下,价格水平之间的最小价格步长是 SYMBOL_TRADE_TICK_SIZE,不过,交易量为零的水平不会被传输,也就是说,相邻元素之间的价格差可能会很大。

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

实际上,订单簿的内容有时可能与所公布的规则相矛盾。特别是,一些买入或卖出请求可能会落入订单簿的相反部分(可能是有人以不利的高价买入或以不利的低价卖出,但数据提供商也可能存在数据聚合错误)。结果,由于遵循 “所有卖出订单在上,所有买入订单在下” 的优先级,订单簿中的价格顺序将会被打乱(见下面的示例)。此外,在订单簿的同一半部分或相反部分中都可能会发现重复的价格(水平)。

从理论上讲,在订单簿中间买入和卖出价格一致是正确的,这意味着点差为零。然而,不幸的是,在订单簿的更深处也会出现重复的价格水平。

当我们说订单簿的 “一半” 时,不应从字面上理解。根据流动性的不同,供需水平的数量可能不匹配。一般来说,订单簿不是对称的。

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    // 重复
17 S 138.11 200
18 S 134.62 420    // 低
19 S 133.55 10627  // 低
 
20 B 143.23 9564   // 高
21 B 138.86 533    // 高
22 B 138.39 739    // 重复
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 中指定交易品种的市场深度事件(如果在那里留空行,则假定为当前图表的交易品种)。

c++
input string WorkSymbol = ""; // 工作交易品种(如果为空,使用当前图表交易品种)
   
const string _WorkSymbol = StringLen(WorkSymbol) == 0? _Symbol : WorkSymbol;
int digits;
   
void OnInit()
{
   PRTF(MarketBookAdd(_WorkSymbol));
   digits = (int)SymbolInfoInteger(_WorkSymbol, SYMBOL_DIGITS);
   ...
}
   
void OnDeinit(const int)
{
   Comment("");
   PRTF(MarketBookRelease(_WorkSymbol));
}

在处理事件的代码中定义了 OnBookEvent 处理程序,在其中调用 MarketBookGet 函数,并将得到的 MqlBookInfo 数组的所有元素作为多行注释输出。

c++
void OnBookEvent(const string &symbol)
{
   if(symbol == _WorkSymbol) // 只处理请求的交易品种的订单簿
   {
      MqlBookInfo mbi[];
      if(MarketBookGet(symbol, mbi)) // 获取当前订单簿
      {
         ...
         int half = ArraySize(mbi) / 2; // 估计订单簿的中间位置
         bool correct = true;
         // 将水平和交易量的信息收集到一行中(用连字符分隔)
         string s = "";
         for(int i = 0; i < 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].price, digits),
               mbi[i].volume, mbi[i].volume_real);
               
            if(i > 0) // 当请求类型改变时,寻找订单簿的中间位置
            {
               if(mbi[i - 1].type == BOOK_TYPE_SELL
                  && mbi[i].type == BOOK_TYPE_BUY)
               {
                  half = i; // 这就是中间位置,因为类型发生了变化
               }
               
               if(mbi[i - 1].price <= mbi[i].price)
               {
                  correct = false; // 顺序相反 = 数据有问题
               }
            }
         }
         Comment(s + (!correct? "\nINCORRECT BOOK" : ""));
         ...
      }
   }
}

由于订单簿变化相当快,通过注释来跟踪不太方便。因此,我们将为该指标添加几个缓冲区,在其中我们将以直方图的形式分别显示订单簿两半部分(卖出和买入)的内容:零柱线将对应形成点差的中心水平。随着柱线编号的增加,“市场深度” 也会增加,也就是说,在那里会显示越来越远的价格水平:在上部直方图中,这意味着较低价格的买入订单,在下部直方图中则是较高价格的卖出订单。

c++
#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[];

让我们提供在标准模式和扩展模式下可视化订单簿的功能(即跳过或显示交易量为零的水平),以及以手数或单位的分数形式显示交易量本身。这两个选项在内置的市场深度窗口中都有对应的功能。

c++
input bool AdvancedMode = false;
input bool ShowVolumeInLots = false;

OnInit 中设置缓冲区并获取一些我们稍后需要的交易品种属性。

c++
int depth, digits;
double tick, contract;
   
void OnInit()
{
   ...
   // 设置指标缓冲区
   SetIndexBuffer(0, buys);
   SetIndexBuffer(1, sells);
   ArraySetAsSeries(buys, true);
   ArraySetAsSeries(sells, true);
   // 获取必要的交易品种属性
   depth = (int)PRTF(SymbolInfoInteger(_WorkSymbol, SYMBOL_TICKS_BOOKDEPTH));
   tick = SymbolInfoDouble(_WorkSymbol, SYMBOL_TRADE_TICK_SIZE);
   contract = SymbolInfoDouble(_WorkSymbol, SYMBOL_TRADE_CONTRACT_SIZE);
}

OnBookEvent 处理程序中添加缓冲区填充操作。

c++
#define VOL(V) (ShowVolumeInLots? V / contract : V)
   
void OnBookEvent(const string &symbol)
{
   if(symbol == _WorkSymbol) // 只处理请求的交易品种的订单簿
   {
      MqlBookInfo mbi[];
      if(MarketBookGet(symbol, mbi)) // 获取当前订单簿
      {
         // 清空缓冲区到深度的 10 倍最大深度余量,
         // 因为扩展模式可能有很多空元素
         for(int i = 0; i <= depth * 10; ++i)
         {
            buys[i] = EMPTY_VALUE;
            sells[i] = EMPTY_VALUE;
         }
         ...// 继续像之前一样形成并显示注释
         if(!correct) return;
         
         // 用数据填充缓冲区
         if(AdvancedMode) // 启用显示跳过的水平
         {
            for(int i = 0; i < 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 // 标准模式:只显示重要元素
         {
            for(int i = 0; i < 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 设置下的工作情况。

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

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

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

需要注意的是,该指标可能没有足够快地重绘以与内置订单簿保持同步。这并不意味着 MQL程序没有及时收到事件,而只是异步图表渲染的一个副作用。实际工作的算法通常会对订单簿进行分析处理和下单操作,而不是进行可视化。

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

在应用算法中使用市场深度数据

市场深度被认为是开发高级交易系统的一项非常有用的技术。特别是,通过分析接近当前市场价格层级上的市场深度成交量分布,您可以提前了解特定成交量的平均订单执行价格:只需将那些能够确保订单成交的层级成交量(在相反方向上)相加即可。在流动性较差的市场中,如果成交量不足,该算法可能会避免开仓交易,以防止出现显著的价格滑点。

基于市场深度数据,还可以构建其他交易策略。例如,了解那些存在大量成交量的价格层级可能是很重要的。

MarketBookVolumeAlert.mq5

在下一个测试指标 MarketBookVolumeAlert.mq5 中,我们实现了一个简单的算法,用于跟踪超过给定值的成交量或其变化情况。

c
#property indicator_chart_window
#property indicator_plots 0

input string WorkSymbol = ""; // 工作交易品种(如果为空,则使用当前图表的交易品种)
input bool CountVolumeInLots = false;
input double VolumeLimit = 0;

const string _WorkSymbol = StringLen(WorkSymbol) == 0? _Symbol : WorkSymbol;

该指标中没有图形显示。在 WorkSymbol 参数中输入要监控的交易品种(如果留空,则表示使用图表的当前工作交易品种)。VolumeLimit 参数指定了被跟踪对象的最小阈值,也就是该算法的敏感度。根据 CountVolumeInLots 参数的值,成交量将以手数(为 true 时)或单位数(为 false 时)进行分析并显示给用户。这也会影响 VolumeLimit 值的输入方式。从单位数到手数的分数转换由 VOL 宏提供:其中使用的合约大小 contract 在 OnInit 函数中进行初始化(见下文)。

c
#define VOL(V) (CountVolumeInLots? V / contract : V)

如果发现成交量大于阈值,程序将在注释中显示关于相应价格层级的消息。为了保存最近的警告历史记录,我们使用已经熟悉的多行注释类(Comments.mqh)。

c
#define N_LINES 25                // 注释缓冲区中的行数
#include <MQL5Book/Comments.mqh>

在 OnInit 处理函数中,我们准备好必要的设置并订阅市场深度(DOM)事件。

c
double contract;
int digits;

void OnInit()
{
   MarketBookAdd(_WorkSymbol);
   contract = SymbolInfoDouble(_WorkSymbol, SYMBOL_TRADE_CONTRACT_SIZE);
   digits = (int)MathRound(MathLog10(contract));
   Print(SymbolInfoDouble(_WorkSymbol, SYMBOL_SESSION_BUY_ORDERS_VOLUME));
   Print(SymbolInfoDouble(_WorkSymbol, SYMBOL_SESSION_SELL_ORDERS_VOLUME));
}

如果您的经纪商为所选交易品种填充了 SYMBOL_SESSION_BUY_ORDERS_VOLUME 和 SYMBOL_SESSION_SELL_ORDERS_VOLUME 属性,这些属性将有助于您确定选择哪个阈值是合理的。默认情况下,VolumeLimit 为 0,这意味着订单簿中的所有变化都会生成警告。为了过滤掉不重要的波动,建议将 VolumeLimit 设置为一个超过所有层级平均成交量大小的值(可以提前查看内置的订单簿或 MarketBookDisplay.mq5 指标)。

我们以通常的方式实现收尾工作。

c
void OnDeinit(const int)
{
   MarketBookRelease(_WorkSymbol);
   Comment("");
}

主要工作由 OnBookEvent 处理器完成。它描述了一个静态数组 MqlBookInfo mbp,用于存储上一个版本的订单簿(自上次函数调用以来)。

c
void OnBookEvent(const string &symbol)
{
   if(symbol != _WorkSymbol) return; // 仅处理请求的交易品种

   static MqlBookInfo mbp[];      // 之前的表格/订单簿
   MqlBookInfo mbi[];
   if(MarketBookGet(symbol, mbi)) // 读取当前订单簿
   {
      if(ArraySize(mbp) == 0) // 第一次调用时,我们只是保存,因为没有可比较的内容
      {
         ArrayCopy(mbp, mbi);
         return;
      }
     ...

如果存在旧的和新的订单簿,我们通过嵌套循环(使用 i 和 j)将它们层级上的成交量相互进行比较。请记住,索引增加意味着价格降低。

c
      int j = 0;
      for(int i = 0; i < ArraySize(mbi); ++i)
      {
         bool found = false;
         for( ; j < ArraySize(mbp); ++j)
         {
            if(MathAbs(mbp[j].price - mbi[i].price) < DBL_EPSILON * mbi[i].price)
            {       // mbp[j].price == mbi[i].price
               if(VOL(mbi[i].volume_real - mbp[j].volume_real) >= VolumeLimit)
               {
                  NotifyVolumeChange("Enlarged", mbp[j].price,
                     VOL(mbp[j].volume_real), VOL(mbi[i].volume_real));
               }
               else
               if(VOL(mbp[j].volume_real - mbi[i].volume_real) >= VolumeLimit)
               {
                  NotifyVolumeChange("Reduced", mbp[j].price,
                     VOL(mbp[j].volume_real), VOL(mbi[i].volume_real));
               }
               found = true;
               ++j;
               break;
            }
            else if(mbp[j].price > mbi[i].price)
            {
               if(VOL(mbp[j].volume_real) >= VolumeLimit)
               {
                  NotifyVolumeChange("Removed", mbp[j].price,
                     VOL(mbp[j].volume_real), 0.0);
               }
               // 继续循环,增加 ++j 以处理更低的价格
            }
            else // mbp[j].price < mbi[i].price
            {
               break;
            }
         }
         if(!found) // 唯一(新)的价格
         {
            if(VOL(mbi[i].volume_real) >= VolumeLimit)
            {
               NotifyVolumeChange("Added", mbi[i].price, 0.0, VOL(mbi[i].volume_real));
            }
         }
      }
     ...

这里,重点不在于层级的类型,而仅在于成交量值。不过,如果您愿意,您可以根据发生重要变化的层级的类型字段,轻松地在通知中添加买入或卖出的标识。

最后,我们将 mbi 的新副本保存到静态数组 mbp 中,以便在下次函数调用时与之进行比较。

c
      if(ArrayCopy(mbp, mbi) <= 0)
      {
         Print("ArrayCopy failed:", _LastError);
      }
      if(ArrayResize(mbp, ArraySize(mbi)) <= 0) // 根据需要缩小数组
      {
         Print("ArrayResize failed:", _LastError);
      }
   }
}

如果动态目标数组碰巧比源数组大,ArrayCopy 不会自动缩小该数组,因此我们使用 ArrayResize 显式设置其确切大小。

辅助函数 NotifyVolumeChange 只是将关于找到的变化的信息添加到注释中。

c
void NotifyVolumeChange(const string action, const double price,
   const double previous, const double volume)
{
   const string message = StringFormat("%s: %s %s -> %s",
      action,
      DoubleToString(price, (int)SymbolInfoInteger(_WorkSymbol, SYMBOL_DIGITS)),
      DoubleToString(previous, digits),
      DoubleToString(volume, digits));
   ChronoComment(message);
}

以下图片显示了指标在 CountVolumeInLots=false、VolumeLimit=20 设置下的结果。

订单簿中成交量变化的通知

订单簿中成交量变化的通知

MarketBookQuasiTicks.mq5

作为使用订单簿的另一个可能示例,我们来看一下获取多货币报价的问题。我们在 “自定义事件的生成” 部分已经涉及到了这个问题,在那里我们看到了一种可能的解决方案以及 EventTickSpy.mq5 指标。现在,在熟悉了市场深度 API 之后,我们可以实现一种替代方案。

让我们创建一个指标 MarketBookQuasiTicks.mq5,它将订阅给定交易品种列表的订单簿,并在其中找到最佳卖价和买价,也就是围绕点差的价格对,即卖价(Ask)和买价(Bid)。

当然,这些信息并不完全等同于标准报价(请记住,交易/报价流和订单簿流可能来自完全不同的提供商),但它提供了对市场足够准确和及时的视图。

交易品种的新价格值将显示在多行注释中。

工作交易品种列表在 SymbolList 输入参数中指定,是一个以逗号分隔的列表。在 OnInit 和 OnDeinit 处理函数中启用和禁用对市场深度事件的订阅。

c
#define N_LINES 25                // 注释缓冲区中的行数
#include <MQL5Book/Comments.mqh>

input string SymbolList = "EURUSD,GBPUSD,XAUUSD,USDJPY"; // 交易品种列表(逗号分隔的列表)

const string WorkSymbols = StringLen(SymbolList) == 0? _Symbol : SymbolList;
string symbols[];

void OnInit()
{
   const int n = StringSplit(WorkSymbols, ',', symbols);
   for(int i = 0; i < n; ++i)
   {
      if(!MarketBookAdd(symbols[i]))
      {
         PrintFormat("MarketBookAdd(%s) failed with code %d", symbols[i], _LastError);
      }
   }
}

void OnDeinit(const int)
{
   for(int i = 0; i < ArraySize(symbols); ++i)
   {
      if(!MarketBookRelease(symbols[i]))
      {
         PrintFormat("MarketBookRelease(%s) failed with code %d", symbols[i], _LastError);
      }
   }
   Comment("");
}

对每个新订单簿的分析在 OnBookEvent 中进行。

c
void OnBookEvent(const string &symbol)
{
   MqlBookInfo mbi[];
   if(MarketBookGet(symbol, mbi)) // 获取当前订单簿
   {
      int half = ArraySize(mbi) / 2; // 估计订单簿的中间位置
      bool correct = true;
      for(int i = 0; i < ArraySize(mbi); ++i)
      {
         if(i > 0)
         {
            if(mbi[i - 1].type == BOOK_TYPE_SELL
               && mbi[i].type == BOOK_TYPE_BUY)
            {
               half = i; // 指定订单簿的中间位置
            }
            
            if(mbi[i - 1].price <= mbi[i].price)
            {
               correct = false;
            }
         }
      }
      
      if(correct) // 从正确的订单簿中检索最佳买价/卖价
      {
         // mbi[half - 1].price // 卖价
         // mbi[half].price     // 买价
         OnSymbolTick(symbol, mbi[half].price);
      }
   }
}

找到的市场卖价/买价将传递给辅助函数 OnSymbolTick,以便在注释中显示。

c
void OnSymbolTick(const string &symbol, const double price)
{
   const string message = StringFormat("%s %s",
      symbol, DoubleToString(price, (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS)));
   ChronoComment(message);
}

如果您愿意,您可以确保我们合成的报价与标准报价没有太大差异。

这就是图表上显示的关于传入准报价的信息的样子。

基于订单簿事件的多交易品种准报价

基于订单簿事件的多交易品种准报价

同时,应该再次指出,订单簿事件仅在平台上在线可用,而在测试器中不可用。如果交易系统完全基于订单簿的准报价构建,那么对其进行测试将需要使用第三方解决方案,以确保在测试器中收集和回放订单簿数据。