Трансляция изменений стакана заявок

При необходимости, MQL-программа может генерировать для пользовательского символа стакан заявок с помощью функции CustomBookAdd. Это, в частности, может быть полезно для инструментов с внешних бирж, таких как криптовалютные.

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

Функция транслирует подписавшимся MQL-программам состояние стакана цен для пользовательского инструмента symbol, используя данные из массива books. Массив описывает полное состояние стакана, то есть все заявки на покупку и продажу. Переданное состояние полностью заменяет предыдущее и становится доступно через функцию MarketBookGet.

Параметр count позволяет задать количество элементов массива books, которое должно быть передано в функцию. По умолчанию используется весь массив.

Функция возвращает признак успеха (true) или ошибки (false).

Чтобы получить генерируемые функцией CustomBookAdd стаканы, заинтересованная в них MQL-программа должна, как обычно, подписаться на них с помощью MarketBookAdd.

При вбросе стакана цены Bid и Ask инструмента не обновляются: для этой цели следует отдельно вбрасывать тики при помощи CustomTicksAdd.

Передаваемые данные проверяются на корректность: цены и объемы должны быть больше нуля, для каждого элемента должны быть указаны тип, цена и объем (поля volume и/или volume_real). Если хотя бы один элемент стакана описан неверно, функция вернет ошибку.

Также проверяется параметр "Глубина стакана" (SYMBOL_TICKS_BOOKDEPTH) пользовательского инструмента. Если количество уровней на продажу или покупку в передаваемом стакане превышает это значение, лишние уровни отбрасываются.

Объем с повышенной точностью volume_real имеет больший приоритет по сравнению с обычным volume. Если для элемента стакана указаны оба значения, будет использовано volume_real.

Внимание! В текущей реализации CustomBookAdd автоматически блокирует пользовательский символ, как будто на него имеется подписка, выполненная с помощью MarketBookAdd, но события OnBookEvent при этом не поступают (в принципе, генерирующая стаканы программа может на них подписаться, вызвав MarketBookAdd явным образом и контролировать то, что будут получать другие программы). Удалить эту блокировку можно с помощью вызова MarketBookRelease.
 
Это может потребоваться в связи с тем, что символы, для которых имеются подписки на стакан, нельзя скрыть из Обзора рынка никакими средствами (пока все явные или неявные подписки не будут отменены из программ, а также не будет закрыто окно стакана). И как следствие, такие символы нельзя удалить.

В качестве примера создадим неторгующий эксперт PseudoMarketBook.mq5, который будет генерировать из ближайшей истории тиков псевдо-состояния стакана. Это может быть полезно для символов, для которых стакан не транслируется, в частности, для Forex. При желании можно использовать такие пользовательские символы для формальной отладки собственных торговых алгоритмов, использующих стакан.

Среди входных параметров укажем максимальную глубину стакана.

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 + "\"");
      }
   }
   ...

Если пользовательский символ уже существует, эксперт может предложить пользователю удалить его, и на этом завершить работу (предварительно пользователю следует закрыть все графики с этим символом).

   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// центр стакана
   if(!SymbolInfoTick(_Symboltick)) return false;
   
   double buys[];  // объемы покупок по ценовым уровням
   double sells[]; // объемы продажи по ценовым уровням
   
   MqlTick ticks[];
   CopyTicks(_SymbolticksCOPY_TICKS_ALL0count); // запрашиваем историю тиков
   for(int i = 1i < ArraySize(ticks); ++i)
   {
      // считаем, что ask вверх подтолкнули покупки
      int k = (int)MathRound((tick.ask - ticks[i].ask) / _Point);
      if(ticks[i].ask > ticks[i - 1].ask)
      {
         // уже купили, вероятно будут фиксировать прибыль продажей
         if(k <= 0)
         {
            Place(sells, -kcontract / sqrt(sqrt(ArraySize(ticks) - i)));
         }
      }
      
      // считаем, что bid вниз сдвинули продажи
      k = (int)MathRound((tick.bid - ticks[i].bid) / _Point);
      if(ticks[i].bid < ticks[i - 1].bid)
      {
         // уже продали, вероятно будут фиксировать прибыль покупкой
         if(k >= 0)
         {
            Place(buyskcontract / sqrt(sqrt(ArraySize(ticks) - i)));
         }
      }
   }
   ...

Вспомогательная функция Place заполняет массивы buys и sells, аккумулируя в них объемы по ценовым уровням. Мы покажем её ниже. Индексы в массивах определяются как расстояние в пунктах от текущих лучших цен (Bid или Ask). Размер объема обратно пропорционален возрасту тика, то есть более отдаленные в прошлое тики оказывают меньшее влияние.

После того как массивы заполнены, на их основе формируется массив структур MqlBookInfo.

   for(int i = 0k = 0i < ArraySize(sells) && k < depth; ++i// верхняя половина стакана
   {
      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// нижняя половина стакана
   {
      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;
}

На следующем скриншоте показан график EURUSD с работающим на нем экспертом PseudoMarketBook.mq5 и получившийся вариант стакана.

Синтетический стакан заявок пользовательского символа на основе EURUSD

Синтетический стакан заявок пользовательского символа на основе EURUSD