Использование данных стакана в прикладных алгоритмах

Стакан цен считается весьма полезной технологией для разработки продвинутых торговых систем. В частности, анализ распределения объемов стакана на уровнях, близких к рынку, позволяет заранее узнать среднюю цену исполнения ордера конкретного объема: достаточно просуммировать объемы уровней (противоположного направления), которые обеспечат его заливку. На тонком рынке, при недостаточности объемов, алгоритм может воздержаться от открытия сделки, чтобы избежать существенного проскальзывания цены.

На основе данных стакана можно конструировать и другие стратегии. Например, бывает важно знать ценовые уровни, на которых расположены крупные объемы.

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                // количество строк в буфере комментариев
#include <MQL5Book/Comments.mqh>

В обработчике OnInit подготовим необходимые настройки и подпишемся на стакан.

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// обрабатываем только запрошенный символ
   
   static MqlBookInfo mbp[];      // предыдущая таблица/книга
   MqlBookInfo mbi[];
   if(MarketBookGet(symbolmbi)) // читаем текущую книгу
   {
      if(ArraySize(mbp) == 0// первый раз просто сохраняем, т.к. не с чем сравнивать
      {
         ArrayCopy(mbpmbi);
         return;
      }
      ...

При наличии старого и нового стакана производим сравнение объемов на их уровнях друг с другом во вложенных циклах по i и j. Напомним, что увеличение индекса означает уменьшение цены.

      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);
               }
               // продолжаем цикл увеличивая ++j к меньшим ценам
            }
            else // mbp[j].price < mbi[i].price
            {
               break;
            }
         }
         if(!found// уникальная (новая) цена
         {
            if(VOL(mbi[i].volume_real) >= VolumeLimit)
            {
               NotifyVolumeChange("Added"mbi[i].price0.0VOL(mbi[i].volume_real));
            }
         }
      }
      ...

Здесь акцент делается не на типе уровня, а только на величине объема, но при желании легко добавить в уведомления обозначение покупок или продаж, в зависимости от поля type того уровня, где произошло важное изменение.

В завершение сохраняем новую копию mbi в статическом массиве mbp для сравнения относительно него на следующем вызове функции.

      if(ArrayCopy(mbpmbi) <= 0)
      {
         Print("ArrayCopy failed:"_LastError);
      }
      if(ArrayResize(mbpArraySize(mbi)) <= 0// ужимаем если нужно
      {
         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=false, VolumeLimit=20.

Уведомления об изменениях объема в стакане
Уведомления об изменениях объема в стакане

MarketBookQuasiTicks.mq5

В качестве второго примера возможного применения стакана обратимся к проблеме получения мультивалютных тиков. Мы уже касались её в разделе Генерация пользовательских событий, где было предоставлено одно из возможных решений и индикатор EventTickSpy.mq5. Теперь, после знакомства с API стакана цен, мы может реализовать альтернативный вариант.

Создадим индикатор MarketBookQuasiTicks.mq5, который будет подписываться на стаканы заданного списка инструментов и находить в них цены лучшего предложения и спроса, то есть пары цен вокруг спреда, а это — ни что иное как цены Ask и Bid.

Разумеется, данная информация не является полным эквивалентом стандартных тиков (напомним, что потоки сделок/тиков и стакана могут поступать от совершенно разных поставщиков), но обеспечивает адекватное и оперативное представление о рынке.

Новые значения цен по символам будем выводить в многострочный комментарий.

Перечень рабочих символов задается во входном параметре SymbolList, как список, через запятую. Включение и отключение подписки на стаканы производится в обработчиках OnInit и OnDeinit.

#define N_LINES 25                // количество строк в буфере комментариев
#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)) // получаем текущий стакан
   {
      int half = ArraySize(mbi) / 2// оценка середины стакана
      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// уточняем середину стакана
            }
            
            if(mbi[i - 1].price <= mbi[i].price)
            {
               correct = false;
            }
         }
      }
      
      if(correct// извлекаем лучшие цены Bid/Ask из корректного стакана 
      {
         // 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);
}

При желании вы можете убедиться, что наши синтезированные тики мало отличаются от стандартных тиков.

Вот как информация о поступающих квази-тиках выглядит на графике.

Мультисимвольные квази-тики, полученные на основе событий стаканов
Мультисимвольные квази-тики, полученные на основе событий стаканов

Вместе с тем, следует еще раз отметить, что события стакана доступны на платформе только онлайн, но не в тестере. Если торговая система будет построена исключительно на квази-тиках из стакана, для её тестирования потребуется применение сторонних решений, обеспечивающих сбор и воспроизведение стаканов в тестере.