转换订单簿变更

如有必要,MQL 程序可以使用 CustomBookAdd 函数为自定义交易品种生成订单簿。这尤其适用于来自外部交易所的金融工具,例如加密货币。

int CustomBookAdd(const string symbol, const MqlBookInfo &books[], uint count = WHOLE_ARRAY)

该函数使用 books 数组中的数据, 将订单簿的状态广播到使用自定义 symbol 的已签名 MQL 程序。该数组描述了订单簿的完整状态,即所有买入和卖出订单。转换后的状态完全取代先前的状态,并可通过 MarketBookGet 函数获取。

使用 count 参数,可以指定要传递给函数的 books 数组的元素数量。默认情况下使用整个数组。

该函数返回操作状态标识:成功 (true) 或错误 (false)。

要获取由 CustomBookAdd 函数生成的订单簿,需要这些订单簿的 MQL 程序必须像往常一样使用 MarketBookAdd来订阅事件。

订单簿的更新不会更新金融工具的BidAsk。要更新所需的价格,请使用 CustomTicksAdd添加分时报价。

系统会检查传输数据的正确性:价格和交易量必须大于零,并且对于每个元素,都必须指定其类型、价格和交易量(字段 volume 和/或 volume_real)。如果只要订单簿中有一个元素描述不正确,该函数就会返回错误。

系统也会检查自定义金融工具的订单簿深度参数 (SYMBOL_TICKS_BOOKDEPTH)。如果转换后的订单簿中的卖出或买入级别数量超过此值,则多余的级别将被丢弃。

具有更高精度的交易量 volume_real 优先于普通 volume。如果为订单簿元素同时指定了这两个值,则将使用 volume_real

注意!在当前实现中,CustomBookAdd 会自动锁定自定义交易品种,就好像它是由 MarketBookAdd 订阅的一样,但同时,OnBookEvent 事件不会到达(理论上,生成订单簿的程序可以通过显式调用 MarketBookAdd 来订阅它们,并控制其他程序接收的内容)。你可以通过调用 MarketBookRelease 来移除此锁定。
 
这可能是必需的,因为对于已订阅订单簿的交易品种,无论如何都无法将其从Market Watch中隐藏(直到所有显式或隐式订阅从程序中取消,并且订单簿窗口关闭)。因此,此类交易品种无法删除。

例如,我们创建一个非交易性 EA 交易 PseudoMarketBook.mq5,它将从最近的分时报价历史中生成订单簿的伪状态。这对于那些不转换订单簿的交易品种(尤其是外汇)可能很有用。如果你愿意,可以使用此类自定义交易品种对你自己的使用订单簿的交易算法进行形式化调试。

在输入参数中,我们指示订单簿的最大深度。

input uint CustomBookDepth = 20;

自定义交易品种的名称将通过在当前图表交易品种的名称后添加后缀 ".Pseudo" 来形成。

string CustomSymbol = _Symbol + ".Pseudo";

OnInit 处理程序中,我们创建一个自定义交易品种,并将其公式设置为原始交易品种的名称。因此,我们将获得一个由终端自动更新的原始交易品种的副本,并且我们无需费心复制报价或分时报价。

int OnInit()
{
   bool custom = false;
   if(!PRTF(SymbolExist(CustomSymbolcustom)))
   {
      if(PRTF(CustomSymbolCreate(CustomSymbolCustomPath_Symbol)))
      {
         CustomSymbolSetString(CustomSymbolSYMBOL_DESCRIPTION"Pseudo book generator");
         CustomSymbolSetString(CustomSymbolSYMBOL_FORMULA"\"" + _Symbol + "\"");
      }
   }
   ...

如果自定义交易品种已存在,EA 交易可以向用户提议删除它并在那里完成工作(用户应首先关闭所有带有此交易品种的图表)。

   else
   {
      if(IDYES == MessageBox(StringFormat("Delete existing custom symbol '%s'?",
         CustomSymbol), "Please, confirm"MB_YESNO))
      {
         PRTF(MarketBookRelease(CustomSymbol));
         PRTF(SymbolSelect(CustomSymbolfalse));
         PRTF(CustomRatesDelete(CustomSymbol0LONG_MAX));
         PRTF(CustomTicksDelete(CustomSymbol0LONG_MAX));
         if(!PRTF(CustomSymbolDelete(CustomSymbol)))
         {
            Alert("Can't delete "CustomSymbol", please, check up and delete manually");
         }
         return INIT_PARAMETERS_INCORRECT;
      }
   }
   ...

此交易品种的一个特殊功能是设置 SYMBOL_TICKS_BOOKDEPTH 特性,以及读取合约大小 SYMBOL_TRADE_CONTRACT_SIZE,这在生成交易量时是必需的。

   if(SymbolInfoInteger(_SymbolSYMBOL_TICKS_BOOKDEPTH) != CustomBookDepth
   && SymbolInfoInteger(CustomSymbolSYMBOL_TICKS_BOOKDEPTH) != CustomBookDepth)
   {
      Print("Adjusting custom market book depth");
      CustomSymbolSetInteger(CustomSymbolSYMBOL_TICKS_BOOKDEPTHCustomBookDepth);
   }
   
   depth = (int)PRTF(SymbolInfoInteger(CustomSymbolSYMBOL_TICKS_BOOKDEPTH));
   contract = PRTF(SymbolInfoDouble(CustomSymbolSYMBOL_TRADE_CONTRACT_SIZE));
   
   return INIT_SUCCEEDED;
}

该算法在 OnTick 处理程序中启动。在这里,我们调用了 GenerateMarketBook 函数,但这个函数尚未编写。它将填充通过引用传递的 MqlBookInfo 结构体数组,然后我们将使用 CustomBookAdd 将其发送到自定义交易品种。

void OnTick()
{
   MqlBookInfo book[];
   if(GenerateMarketBook(2000book))
   {
      ResetLastError();
      if(!CustomBookAdd(CustomSymbolbook))
      {
         Print("Can't add market books, "E2S(_LastError));
         ExpertRemove();
      }
   }
}

GenerateMarketBook 函数会分析最新的 count 个分时报价,并基于它们模拟订单簿的可能状态,遵循以下假设:

  • 已买入的可能会被卖出
  • 已卖出的可能会被买入

在一般情况下(在没有交易所标志的情况下),将分时报价划分为对应于买入和卖出的部分,可以通过价格本身的变动来估计:

  • Ask向上变动被视为买入
  • Bid向下变动被视为卖出

结果,我们得到以下算法。

bool GenerateMarketBook(const int countMqlBookInfo &book[])
{
   MqlTick tick// order book centre
   if(!SymbolInfoTick(_Symboltick)) return false;
   
   double buys[];  // buy volumes by price levels
   double sells[]; // sell volumes by price levels
   
   MqlTick ticks[];
   CopyTicks(_SymbolticksCOPY_TICKS_ALL0count); // request tick history
   for(int i = 1i < ArraySize(ticks); ++i)
   {
      // we believe that ask was pushed up by buys
      int k = (int)MathRound((tick.ask - ticks[i].ask) / _Point);
      if(ticks[i].ask > ticks[i - 1].ask)
      {
         // already bought, probably will take profit by selling
         if(k <= 0)
         {
            Place(sells, -kcontract / sqrt(sqrt(ArraySize(ticks) - i)));
         }
      }
      
      // believe that the bid was pushed down by sells
      k = (int)MathRound((tick.bid - ticks[i].bid) / _Point);
      if(ticks[i].bid < ticks[i - 1].bid)
      {
         // already sold, probably will take profit by buying
         if(k >= 0)
         {
            Place(buyskcontract / sqrt(sqrt(ArraySize(ticks) - i)));
         }
      }
   }
   ...

辅助函数 Place 会填充 buyssells 数组,按价格水平在其中累积交易量。我们将在下面展示这一点。数组中的索引定义为与当前最优价格(BidAsk)的点数距离。交易量的大小与分时报价的“时效性”成反比,即越久远的的分时报价,影响力就较小。

填充数组后,系统基于它们形成一个 MqlBookInfo 结构体数组。

   for(int i = 0k = 0i < ArraySize(sells) && k < depth; ++i// top half of the order book
   {
      if(sells[i] > 0)
      {
         MqlBookInfo info = {};
         info.type = BOOK_TYPE_SELL;
         info.price = tick.ask + i * _Point;
         info.volume = (long)sells[i];
         info.volume_real = (double)(long)sells[i];
         PUSH(bookinfo);
         ++k;
      }
   }
   
   for(int i = 0k = 0i < ArraySize(buys) && k < depth; ++i// bottom half of the order book
   {
      if(buys[i] > 0)
      {
         MqlBookInfo info = {};
         info.type = BOOK_TYPE_BUY;
         info.price = tick.bid - i * _Point;
         info.volume = (long)buys[i];
         info.volume_real = (double)(long)buys[i];
         PUSH(bookinfo);
         ++k;
      }
   }
   
   return ArraySize(book) > 0;
}

Place 函数很简单。

void Place(double &array[], const int indexconst double value = 1)
{
   const int size = ArraySize(array);
   if(index >= size)
   {
      ArrayResize(arrayindex + 1);
      for(int i = sizei <= index; ++i)
      {
         array[i] = 0;
      }
   }
   array[index] += value;
}

以下屏幕截图显示了运行 PseudoMarketBook.mq5 EA 交易的 EURUSD 图表,以及生成的订单簿版本。

基于 EURUSD 的自定义交易品种的综合订单簿

基于 EURUSD 的自定义交易品种的综合订单簿