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

市场深度被认为是开发高级交易系统的一项非常实用的技术。特别是,在贴近市场的层级分析市场交易量的深度分布,可以让你提前发现特定交易量的平均订单执行价格:只需反向累加确保成交所需的各价位成交量即可。在交易量不足的清淡市场中,该算法可能会禁止开仓交易,以避免价格大幅下滑。

基于市场深度数据,也可以构建其他策略。例如,了解大宗交易量所处的价格层级很重要。

MarketBookVolumeAlert.mq5

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

#property indicator_chart_window
#property indicator_plots 0
   
input string WorkSymbol = ""// WorkSymbol (if empty, use current chart symbol)
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 中初始化的(见下文)。

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

如果发现大宗交易量超过该阈值,程序将在注释中显示相应层级的消息。为了保存最新的警告历史,我们使用已知的多行注释类 (Comments.mqh)。

#define N_LINES 25                // number of lines in the comment buffer
#include <MQL5Book/Comments.mqh>

OnInit 处理程序中,让我们准备必要的设置并订阅 DOM 事件。

double contract;
int digits;
   
void OnInit()
{
   MarketBookAdd(_WorkSymbol);
   contract = SymbolInfoDouble(_WorkSymbolSYMBOL_TRADE_CONTRACT_SIZE);
   digits = (int)MathRound(MathLog10(contract));
   Print(SymbolInfoDouble(_WorkSymbolSYMBOL_SESSION_BUY_ORDERS_VOLUME));
   Print(SymbolInfoDouble(_WorkSymbolSYMBOL_SESSION_SELL_ORDERS_VOLUME));
}

如果 SYMBOL_SESSION_BUY_ORDERS_VOLUME 和 SYMBOL_SESSION_SELL_ORDERS_VOLUME 特性是由你的经纪商为所选交易品种填写的,则这些特性可帮助你确定选择哪个阈值是有意义的。默认情况下,VolumeLimit 为 0,这就是为什么订单簿中的任何更改都一定会生成警告的原因。为了筛选掉不显著的波动,建议将 VolumeLimit 设置为超过所有层次的交易量的平均规模值(提前查看内置订单簿或 MarketBookDisplay.mq5 指标)。

按常规方式完成最终实现。

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

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

void OnBookEvent(const string &symbol)
{
   if(symbol != _WorkSymbolreturn// process only the requested symbol 
   
   static MqlBookInfo mbp[];      // previous table/book
   MqlBookInfo mbi[];
   if(MarketBookGet(symbolmbi)) // read the current book
   {
      if(ArraySize(mbp) == 0// first time we just save, because nothing to compare
      {
         ArrayCopy(mbpmbi);
         return;
      }
      ...

如果有一个旧的和一个新的订单簿,我们在 ij 的嵌套循环中比较其各个层级上的交易量。请注意,该指数增加意味着价格降低。

      int j = 0;
      for(int i = 0i < 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);
               }
               // continue the loop increasing ++j to lower prices
            }
            else // mbp[j].price < mbi[i].price
            {
               break;
            }
         }
         if(!found// unique (new) price
         {
            if(VOL(mbi[i].volume_real) >= VolumeLimit)
            {
               NotifyVolumeChange("Added"mbi[i].price0.0VOL(mbi[i].volume_real));
            }
         }
      }
      ...

此处,重点不在于水平的类型,而仅在于交易量值。但是,如果你愿意,你可以根据发生重要变更的层级的 type 字段,轻松地将指定的买入或卖出添加到通知中。

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

      if(ArrayCopy(mbpmbi) <= 0)
      {
         Print("ArrayCopy failed:"_LastError);
      }
      if(ArrayResize(mbpArraySize(mbi)) <= 0// shrink if needed
      {
         Print("ArrayResize failed:"_LastError);
      }
   }
}

ArrayCopy 不会自动缩小动态目标数组(即使其大小大于源数组),因此我们需使用 ArrayResize 来设置确切大小。

辅助函数 NotifyVolumeChange 仅用于将关于所发现的变化信息添加到注释中。

void NotifyVolumeChange(const string actionconst double price,
   const double previousconst double volume)
{
   const string message = StringFormat("%s: %s %s -> %s",
      action,
      DoubleToString(price, (int)SymbolInfoInteger(_WorkSymbolSYMBOL_DIGITS)),
      DoubleToString(previousdigits),
      DoubleToString(volumedigits));
   ChronoComment(message);
}

下图显示了设置 CountVolumeInLots=falseVolumeLimit=20 时该指标的结果。

关于订单簿中交易量变化的通知
关于订单簿中交易量变化的通知

MarketBookQuasiTicks.mq5

作为订单簿可能用途的第二个示例,让我们来看获取多货币分时报价的问题。我们已经在 自定义事件的生成一节中提到了该问题,并且在其中看到了一种可能的解决方案和指标 EventTickSpy.mq5。现在,在熟悉了市场深度 API 之后,我们可以实现一个替代方案。

我们来创建一个指标 MarketBookQuasiTicks.mq5,其将订阅给定金融工具列表的订单簿,并在其中找到最佳报价和需求价格,即点差周围的价格对,这些价格仅为 AskBid 价格。

当然,这些信息并不完全等同于标准分时报价(注意,交易/分时报价和订单簿流量可能来自完全不同的提供商),但提供了对市场充分和及时的了解。

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

工作交易品种列表在 SymbolList 输入参数中指定为逗号分隔列表。启用和禁用对市场深度事件的订阅是在 OnInitOnDeinit 处理程序中完成的。

#define N_LINES 25                // number of lines in the comment buffer
#include <MQL5Book/Comments.mqh>
   
input string SymbolList = "EURUSD,GBPUSD,XAUUSD,USDJPY"// SymbolList (comma,separated,list)
   
const string WorkSymbols = StringLen(SymbolList) == 0 ? _Symbol : SymbolList;
string symbols[];
   
void OnInit()
{
   const int n = StringSplit(WorkSymbols, ',', symbols);
   for(int i = 0i < n; ++i)
   {
      if(!MarketBookAdd(symbols[i]))
      {
         PrintFormat("MarketBookAdd(%s) failed with code %d"symbols[i], _LastError);
      }
   }
}
   
void OnDeinit(const int)
{
   for(int i = 0i < ArraySize(symbols); ++i)
   {
      if(!MarketBookRelease(symbols[i]))
      {
         PrintFormat("MarketBookRelease(%s) failed with code %d"symbols[i], _LastError);
      }
   }
   Comment("");
}

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

void OnBookEvent(const string &symbol)
{
   MqlBookInfo mbi[];
   if(MarketBookGet(symbolmbi)) // getting the current order book
   {
      int half = ArraySize(mbi) / 2// estimate the middle of the order book
      bool correct = true;
      for(int i = 0i < ArraySize(mbi); ++i)
      {
         if(i > 0)
         {
            if(mbi[i - 1].type == BOOK_TYPE_SELL
               && mbi[i].type == BOOK_TYPE_BUY)
            {
               half = i// specify the middle of the order book
            }
            
            if(mbi[i - 1].price <= mbi[i].price)
            {
               correct = false;
            }
         }
      }
      
      if(correct// retrieve the best Bid/Ask prices from the correct order book 
      {
         // mbi[half - 1].price // Ask
         // mbi[half].price     // Bid
         OnSymbolTick(symbolmbi[half].price);
      }
   }
}

找到的市场 Ask/Bid 价格传递给辅助函数 OnSymbolTick,以在注释中显示。

void OnSymbolTick(const string &symbolconst double price)
{
   const string message = StringFormat("%s %s",
      symbolDoubleToString(price, (int)SymbolInfoInteger(symbolSYMBOL_DIGITS)));
   ChronoComment(message);
}

如果需要,你可以验证我们的合成分时报价与标准分时报价没有太大差异。

以下为关于收到的模拟分时报价信息在图表上的呈现效果。

基于订单簿事件的多交易品种模拟分时报价
基于订单簿事件的多交易品种模拟分时报价

同时,需要再次注意的是,订单簿事件仅在平台上在线可用,在 测试程序中不可用。如果交易系统完全建立在订单簿的模拟分时报价上,其测试将需要使用第三方解决方案,以确保在测试程序中收集并回放订单簿。