Рецепты MQL5 - Пишем свой стакан цен

Vasiliy Sokolov | 3 июля, 2015

Оглавление


Введение

Язык MQL5 постоянно развивается и с каждым годом предоставляет все больше возможностей для работы с биржевой информацией. Одним из таких биржевых типов данных является информация о биржевом стакане цен. Это специальная таблица, показывающая ценовые уровни и объемы лимитных заявок. MetaTrader 5 имеет встроенный стакан цен для отображения лимитных заявок, однако этого не всегда бывает достаточно. В первую очередь необходимо дать простой и удобный доступ к стакану цен своему торговому эксперту. Конечно, в языке MQL5 есть несколько специальных функций для работы с этим типом информации, но все эти функции достаточно низкоуровневые и требуют дополнительных математических расчетов.

Тем не менее, всех промежуточных расчетов можно избежать. Все что для этого нужно, это один раз написать специальный класс для работы со стаканом цен. Все сложные расчеты будут производиться внутри него, а сам класс будет предоставлять удобные методы для работы с ценами и уровнями стакана. Благодаря этому классу будет достаточно просто создать эффектную панель в виде индикатора, которая будет быстро отображать текущее состояние цен в стакане:


Рис. 1. Биржевой стакан цен в виде панели

Эта статья научит читателей программно работать со стаканом цен, а также подробно опишет принципы работы класса CMarketBook, который органично расширит стандартную библиотеку классов MQL5 и предоставит удобные методы для работы со стаканом.

Из первой главы данной статьи будет ясно, что штатный стакан цен, предоставляемый MetaTrader 5, обладает внушительными возможностями. Мы не будем пытаться дублировать все эти многочисленные возможности в своем индикаторе, наша задача будет иной. На практическом примере создания удобной торговой панели — стакана цен — мы покажем, что принципы объектно-ориентированного программирования позволяют достаточно легко оперировать сложными структурами данными. Мы убедимся в том, что с помощью MQL5 не представляет трудностей получить доступ к стакану цен прямо из своего эксперта и, как следствие, визуализировать его представление так, как будет удобно нам.

 

Глава 1. Стандартный стакан цен в MetaTrader 5 и методы работы с ним


1.1. Стандартный стакан цен в MetaTrader 5

MetaTrader 5 поддерживает работу на централизованных биржевых площадках и предоставляет штатные средства для работы со стаканом цен. В первую очередь это, конечно же, сама таблица лимитных заявок, с недавнего времени обладающая также расширенным режимом представления. Для того чтобы открыть стакан цен, необходимо прежде всего подключиться к одной из бирж, поддерживающей MetaTrader 5, и выбрать в контекстном меню "Вид" --> "Стакан цен" --> "Название инструмента". После этого появится отдельное окно, сочетающее тиковый график цены и таблицу лимитных заявок:

 

Рис. 2. Стандартный стакан цен в MetaTrader 5

Штатный стакан цен в MetaTrader 5 обладает богатой функциональностью. В частности, он позволяет отобразить:

Из приведенного списка видно, что возможности стандартного стакана цен более чем внушительные. Но давайте разберемся с тем, как работать с данными, получая к ним доступ программным путем. Прежде всего необходимо получить представление о том, как устроен биржевой стакан цен, и об общей организации данных в нем. Подробно об этом написано в статье "Основы биржевого ценообразования на примере срочной секции Московской биржи" в разделе "1.3. Сведение продавцов и покупателей, биржевой стакан цен". Мы же не будем подробно останавливаться на описании этой таблицы, подразумевая, что читатель уже имеет достаточные представления об этом.

 

1.2. Событийная модель для работы со стаканом цен

Стакан цен — крайне динамичная таблица данных. На быстрых, динамичных рынках за одну секунду таблица лимитных заявок может изменяться десятки раз. Поэтому необходимо стараться обрабатывать только ту информацию, которая действительно необходима для обработки, иначе количество переданных данных и загрузка центрального процессора по обработке этих данных может превысить все разумные пределы. Именно поэтому в MetaTrader 5 необходима специальная событийная модель, которая препятствует получению и обработке данных, которые в реальности не планируется использовать. Разберем эту модель подробно. 

Каждое событие на рынке, такое как приход нового тика или совершение торговой операции, можно обрабатывать, вызывая соответствующую ассоциированную с ним функцию. Например, при приходе нового тика в MQL5 вызывается специальная функция-обработчик OnTick(). Изменение размера графика или его положения вызывает свою функцию — OnChartEvent(). Эта событийная модель также распространяется и на изменение стакана цен. Например, если кто-то выставит лимитную заявку на продажу или покупку в стакане цен, то его состояние изменится, что вызовет специальную функцию OnBookEvent().

Так как в терминале доступны десятки и даже сотни разных символов, каждый из которых обладает своим стаканом цен, количество вызовов функции OnBookEvent может быть огромным и крайне ресурсоемким. Чтобы этого не происходило, терминал необходимо заранее уведомить в момент запуска своего индикатора или эксперта, с каких именно инструментов необходимо получать информацию о котировках второго уровня (так еще называют информацию, предоставляемую стаканом цен). Для этих целей служит специальная системная функция MarketBookAdd. Например, если мы хотим получать информацию о стакане цен по инструменту Si-9.15 (фьючерсный контракт на доллар-рубль с экспирацией в сентябре 2015 года), нам необходимо в своем эксперте или индикаторе в функции OnInit написать следующий код:

void OnInit()
{
   MarketBookAdd("Si-9.15");
}

С помощью этой функции мы сделали так называемую "подписку" — уведомили терминал, что эксперт или индикатор необходимо информировать в случае изменения стакана по инструменту Si-9.15. При этом изменения стаканов других инструментов будут нам недоступны, что существенно снизит потребляемые программой ресурсы.

Противоположной функцией MarketBookAdd является функция MarketBookRelease. Она, напротив, "отписывает" нас от уведомлений по изменению стакана. Считается хорошим тоном программиста делать такую отписку в секции OnDeinit, закрывая, таким образом, получение данных при закрытии эксперта или индикатора:

void OnDeinit(const int reason)
{
   MarketBookRelease("Si-9.15");
}

Вызов функции MarketBookAdd по сути означает, что в момент изменения стакана цен по требуемому инструменту будет вызываться специальная функция-обработчик OnBookEvent(). Таким образом, простейший эксперт или индикатор, работающий со стаканом цен, будет содержать три системные функции:

Наш первый простейший эксперт будет содержать эти три функции. Например, в примере ниже эксперт при каждом изменении стакана цен будет печатать соответствующее сообщение: "Стакан цен по Si-9.15 изменен":

//+------------------------------------------------------------------+
//|                                                       Expert.mq5 |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   MarketBookAdd("Si-9.15");
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   MarketBookRelease("Si-9.15");
  }
//+------------------------------------------------------------------+
//| BookEvent function                                               |
//+------------------------------------------------------------------+
void OnBookEvent(const string &symbol)
  {
//---
   printf("Стакан цен по " + symbol +  " изменен"); 
  }
//+------------------------------------------------------------------+

Ключевые инструкции в коде выделены желтым маркером.

 

1.3. Получение котировок второго уровня с помощью функций MarketBookGet и структуры MqlBookInfo

Теперь, когда мы научились получать уведомления об изменении состояния стакана цен, настало время научиться получать доступ к информации стакана. Для доступа к этой информации необходимо использовать функцию MarketBookGet. Давайте изучим ее прототип и использование.

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

Индекс строки Тип заявки Объем Цена
0 Sell Limit 18 56844
1  Sell Limit  1  56843
2  Sell Limit  21  56842
3  Buy Limit  9  56836
4  Buy Limit  5  56835
5  Buy Limit  15  56834

 Таблица 1. Представление стакана цен в виде таблицы

Для того чтобы было проще ориентироваться в таблице, заявки на продажу окрашены в розовый цвет, а заявки на покупку — в голубой. Приведенная таблица стакана цен по своей сути является двумерным массивом. В первом измерении указывается номер строки, а во втором измерении — один из трех факторов таблицы (тип заявки — 0, объем заявки — 1 и цена заявки — 2). Однако для того чтобы не работать с многомерными массивами, в MQL5 используется специальная структура MqlBookInfo. Она включает в себя все необходимые значения. Таким образом, каждый индекс стакана цен содержит структуру MqlBookInfo, которая, в свою очередь, содержит информацию о типе заявки, ее объеме и цене. Давайте приведем определение этой структуры:

struct MqlBookInfo
  {
   ENUM_BOOK_TYPE   type;       // тип заявки из перечисления ENUM_BOOK_TYPE
   double           price;      // цена
   long             volume;     // объем
  };

Теперь нам должен быть ясен метод работы со стаканом цен. Функция MarketBookGet возвращает массив структур MqlBookInfo. Индекс массива указывает на строку таблицы цен, а сама структура по индексу содержит информацию об объеме, цене и типе заявки. Зная это, попробуем получить доступ к самой первой заявке стакана цен, для чего немного модифицируем функцию OnBookEvent нашего эксперта из предыдущего примера:

//+------------------------------------------------------------------+
//| BookEvent function                                               |
//+------------------------------------------------------------------+
void OnBookEvent(const string &symbol)
  {
//---
   //printf("Стакан цен по " + symbol +  " изменен"); 
   MqlBookInfo book[];
   MarketBookGet(symbol, book);
   if(ArraySize(book) == 0)
   {
      printf("Failed load market book price. Reason: " + (string)GetLastError());
      return;
   }
   string line = "Price: " + DoubleToString(book[0].price, Digits()) + "; ";
   line += "Volume: " + (string)book[0].volume + "; ";
   line += "Type: " + EnumToString(book[0].type);
   printf(line);
  }

Запустив эксперт на любом из графиков, мы будем получать сообщения о самой первой цене в стакане и ее параметрах:

2015.06.05 15:54:17.189 Expert (Si-9.15,H1)     Price: 56464; Volume: 56; Type: BOOK_TYPE_SELL
2015.06.05 15:54:17.078 Expert (Si-9.15,H1)     Price: 56464; Volume: 56; Type: BOOK_TYPE_SELL
2015.06.05 15:54:17.061 Expert (Si-9.15,H1)     Price: 56464; Volume: 56; Type: BOOK_TYPE_SELL
...

Смотря на нашу предыдущую таблицу, нетрудно догадаться, что уровень, доступный по нулевому индексу стакана, соответствует самой худшей цене предложения (BOOK_TYPE_SELL). Напротив, уровень самого худшего спроса занимает последний индекс в полученном массиве. Лучшие цены на продажу (Ask) и покупку (Bid) находятся примерно в середине стакана цен. Первое неудобство получаемых цен состоит в том, что в стакане цен отсчет принято производить с лучших цен, которые находятся, как правило, в середине таблицы. Цены же худшего спроса и предложения имеют второстепенное значение. В дальнейшем, рассматривая класс CMarketBook, мы решим эту проблему, предоставляя специфичные индексаторы, удобные для работы со стаканом цен.

 

Глава 2. Класс CMarketBook для простого доступа к стакану и работы с ним


2.1. Проектирование класса CMarketInfoBook

В первой главе мы ознакомились с системными функциями для работы со стаканом цен и выяснили особенности событийной модели для организации доступа к котировкам второго уровня. В этой главе мы создадим специальный класс CMarketBook для удобной работы со стандартным стаканом цен. На основе знаний, полученных из первой главы, мы можем порассуждать о том, какими свойствами должен обладать наш класс для работы с этим типом данных.

Итак, первое, что необходимо учитывать в проектировании этого класса — это ресурсоемкость получаемых данных. Стакан может обновляться десятки раз в секунду, к тому же, он содержит десятки элементов типа MqlBookInfo. Следовательно, наш класс должен изначально работать лишь с одним из инструментов. В случае, когда потребуется обработка сразу нескольких стаканов цен с разных инструментов, будет достаточно создать несколько экземпляров нашего класса с указанием конкретного инструмента:

CMarketBook("Si-9.15");            // Стакан цен для Si-9.15
CMarketBook("ED-9.15");            // Стакан цен для ED-9.15
CMarketBook("SBRF-9.15");          // Стакан цен для SBRF-9.15
CMarketBook("GAZP-9.15");          // Стакан цен для GAZP-9.15

Второе, о чем придется позаботиться — это организация быстрого доступа к данным. Так как лимитные заявки генерируют высокочастотный поток ценовых уровней, копировать стакан цен в безопасную объектно-ориентированную таблицу не представляется возможным. Поэтому наш класс будет предоставлять прямой, пусть и не столь безопасный, доступ к массиву MqlBookInfo, предоставляемому системной функцией MarketBookGet. Например, для доступа к нулевому индексу стакана цен с помощью нашего класса, необходимо будет написать следующее:

CMarketBook BookOnSi("Si-9.15");
...
//+------------------------------------------------------------------+
//| BookEvent function                                               |
//+------------------------------------------------------------------+
void OnBookEvent(const string &symbol)
  {
//---
   MqlBookInfo info = BookOnSi.MarketBook[0];
  }
//+------------------------------------------------------------------+

MarketBook при этом является массивом, непосредственно получаемым с помощью функции MarketBookGet. Однако удобство использования нашего класса будет заключаться прежде всего в том, что помимо прямого доступа к массиву лимитных заявок, наш класс позволит адресно обращаться к наиболее часто используемым ценам в стакане. Например, для того чтобы получить лучшую цену предложения, достаточно будет написать следующее:

double best_ask = BookOnSi.InfoGetDouble(MBOOK_BEST_ASK_PRICE);

Это гораздо удобнее, чем рассчитывать индекс лучшего предложения в своем эксперте, а затем получать значение цены по этому индексу. Из написанного выше кода становится понятным, что CMarketBook, так же как и многие системные функции MQL5 вроде SymbolInfoDouble или OrderHistoryInteger, использует свой набор модификаторов и методов InfoGetInteger и InfoGetDouble для доступа к целочисленной и вещественной информации соответственно. Для получения необходимого свойства необходимо указать определенный модификатор этого свойства. Давайте опишем подробно модификаторы этих свойств:

//+------------------------------------------------------------------+
//| Определяет модификаторы для получения целочисленных свойств      |
//| стакана.                                                         |
//+------------------------------------------------------------------+
enum ENUM_MBOOK_INFO_INTEGER
{
   MBOOK_BEST_ASK_INDEX,         // Индекс лучшей цены предложения (Ask)
   MBOOK_BEST_BID_INDEX,         // Индекс лучшей цены спроса (Bid) 
   MBOOK_LAST_ASK_INDEX,         // Индекс худшей цены предложения
   MBOOK_LAST_BID_INDEX,         // Индекс худшей цены спроса
   MBOOK_DEPTH_ASK,              // Количество уровней на продажу
   MBOOK_DEPTH_BID,              // Количество уровней на покупку
   MBOOK_DEPTH_TOTAL             // Общее количество уровней стакана 
};
//+------------------------------------------------------------------+
//| Определяет модификаторы для получения вещественных свойств       |
//| стакана.                                                         |
//+------------------------------------------------------------------+
enum ENUM_MBOOK_INFO_DOUBLE
{
   MBOOK_BEST_ASK_PRICE,         // Лучшая цена предложения (Ask)
   MBOOK_BEST_BID_PRICE,         // Лучшая цена спроса (Bid)
   MBOOK_LAST_ASK_PRICE,         // Худшая цена предложения 
   MBOOK_LAST_BID_PRICE,         // Худшая цена спроса
   MBOOK_AVERAGE_SPREAD          // Средний спред между Ask и Bid
};

Конечно же, помимо методов группы InfoGet..., наш класс будет содержать метод Refresh, вызывающий обновление стакана цен. Благодаря тому, что наш класс требует явного обновления информации вызовом метода Refresh(), мы будем задействовать ресурсоемкое обновление класса лишь тогда, когда это будет необходимо.

 

2.2. Расчет индексов наиболее часто используемых уровней стакана цен

Класс CMarketBook по сути является своеобразной обложкой для массива MqlBookInfo. Его основная работа заключается в быстром и удобном предоставлении наиболее часто востребованной информации из этого массива. Следовательно, основных ресурсоемких операций, задействованных классом, всего две:

Ускорить работу системной функции MarketBookGet мы не можем, но этого и не нужно, так как все системные функции языка MQL5 максимально оптимизированы. Но мы можем произвести максимально быстрый расчет требуемых индексов. Еще раз обратимся к модификаторам свойств ENUM_MBOOK_INFO_INTEGER и ENUM_MBOOK_INFO_DOUBLE. Как видно, практически все доступные свойства базируются на расчете четырех индексов:

Также используется три целочисленных свойства:

Очевидно, что индекс худшего предложения всегда будет равен нулю, так как именно с худшей цены предложения начинается массив, получаемый с помощью функции MarketBookGet. Также тривиальным является определение индекса худшего спроса — он всегда будет занимать самый последний индекс в получаемом массиве MqlInfoBook (напомним, что индекс последнего элемента в массиве меньше общего количества элементов этого массива на единицу) :

индекс худшего предложения = 0

индекс худшего спроса = общее количество элементов в стакане - 1

Индексы целочисленных свойств также просты для вычисления. Так, общая глубина стакана всегда равна количеству элементов в массиве MqlBookInfo. Глубина стакана со стороны предложения равна:

глубина предложения = индекс лучшего предложения - индекс худшего предложения + 1

Мы всегда прибавляем единицу, так как нумерация в массиве начинается с нуля, и для определения количества элементов требуется прибавлять единицу к лучшему индексу. Однако, прибавляя единицу к индексу лучшего предложения, мы уже получаем индекс лучшего спроса. Например, если мы в таблице 1 прибавим к строке с вторым номером единицу, то перейдем от лучшей Sell Limit заявки по цене 56 842 к лучшей Buy Limit заявке по цене 56 836. Также мы уже выяснили, что индекс худшего предложения всегда равен нулю. Следовательно, мы можем сократить формулу определения глубины предложения до следующей:

глубина предложения = индекс лучшего спроса

Расчет глубины спроса несколько иной. Очевидно, что количество заявок на покупку равно общему количеству заявок за вычетом количества заявок на продажу или глубины предложения. Поскольку из предыдущей формулы мы выяснили, что глубина предложения равна индексу лучшего спроса, нетрудно рассчитать формулу для определения количества заявок на покупку или глубины спроса:

глубина спроса = общая глубина стакана - индекс лучшего предложения

Общая глубина стакана всегда равна сумме глубины спроса и предложения и, следовательно, равна общему количеству элементов в стакане:

общая глубина стакана = общее количество элементов в стакане

Аналитически мы нашли практически все наиболее часто используемые индексы. С помощью математических сокращений мы заменили расчет индексов на прямую индексацию. В классе CMarketBook это очень важно, так как требуется максимально быстрый доступ к свойствам и индексам стакана.

Помимо самих индексов, зачастую бывает необходимо знать средний уровень спреда для текущего инструмента. Спред — это разница между ценами лучшего спроса и предложения. Класс CMarketBook позволяет получить среднее значение этого показателя с помощью метода InfoGetDouble, для чего его необходимо вызывать с модификатором MBOOK_AVERAGE_SPREAD. CMarketBook производит расчет текущего спреда в методе Refresh и рассчитывает его среднее значение запоминая количество вызовов этого метода.

Однако задачу по нахождению основных индексов лучшего спроса и предложения мы пока не решили, поэтому перейдем к следующему разделу.

 

2.3. Предсказывание индексов лучшего спроса и предложения на основе предыдущих значений этих индексов

Более сложной задачей, которую нам осталось выполнить, является расчет индексов лучшего предложения и лучшего спроса. Например, в таблице 1 индексом лучшего предложения будет индекс с номером 2, а индексом лучшего спроса будет индекс 3. В этой таблице представлен упрощенный стакан, состоящий всего из 6 уровней. В реальности стакан может быть гораздо больше и содержать до 64 уровней на продажу и покупку. Это существенная величина, учитывая, что обновление стакана может происходит несколько раз в секунду.

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

Наш класс CMarketBook должен работать на любых рынках и с любыми стаканами, поэтому простой метод деления на два нам не подходит. Для иллюстрации ситуации, когда метод деления на два может не сработать, обратимся к следующему рисунку:

 

Рис. 3. Количество уровней спроса не всегда равно количеству уровней предложения

На рисунке 3 изображены два стакана. Первый из них — стакан фьючерсного контракта на двухлетние облигации федерального займа (OFZ2-9.15). Второй — фьючерсный контракт евро/доллар (ED-9.15). Видно, что для OFZ2-9.15 количество ценовых уровней покупателей равно четырем, в то время как количество ценовых уровней продавцов равно восьми. На более ликвидном рынке ED-9.15 количество уровней как продавцов так и покупателей равны и составляют 12 уровней для каждой из сторон. Видно, что в случае с ED-9.15 способ определения индексов делением на два сработал бы, а в случае с OFZ2 — нет.

Куда более надежным способом определения индекса был бы перебор стакана до момента первого вхождения заявки с типом BOOK_TYPE_BUY. Предыдущий индекс при этом автоматически становился бы индексом лучшего предложения. Именно таким методом обладает класс CMarketBook. Обратимся к нему для иллюстрации сказанного:

void CMarketBook::SetBestAskAndBidIndex(void)
{
   if(!FindBestBid())
   {
      //Find best ask by slow full search
      int bookSize = ArraySize(MarketBook);   
      for(int i = 0; i < bookSize; i++)
      {
         if((MarketBook[i].type == BOOK_TYPE_BUY) || (MarketBook[i].type == BOOK_TYPE_BUY_MARKET))
         {
            m_best_ask_index = i-1;
            FindBestBid();
            break;
         }
      }
   }
}

Основная работа этого метода заключена в переборе стакана оператором for. Как только в стакане встречается первая заявка с типом BOOK_TYPE_BUY, устанавливаются индексы лучшего спроса и предложения, а перебор прерывается. Но полный перебор стакана при каждом его обновлении был бы слишком ресурсозатратным решением.

Вместо того чтобы осуществлять перебор при каждом обновлении, можно запомнить ранее найденные индексы лучшего спроса и предложения! В самом деле, стакан цен, как правило, содержит не меняющееся количество уровней на продажу и покупку. Поэтому каждый раз перебирать стакан в поисках нужных нам индексов не нужно. Достаточно обратиться по ранее найденным индексам и понять, являются ли они по-прежнему индексами лучшего спроса и предложения. Именно этим и занимается приватный метод FindBestBid. Настало время посмотреть его содержимое:

//+------------------------------------------------------------------+
//| Fast find best bid by best ask                                   |
//+------------------------------------------------------------------+
bool CMarketBook::FindBestBid(void)
{
   m_best_bid_index = -1;
   bool isBestAsk = m_best_ask_index >= 0 && m_best_ask_index < m_depth_total &&
                    (MarketBook[m_best_ask_index].type == BOOK_TYPE_SELL ||
                    MarketBook[m_best_ask_index].type == BOOK_TYPE_SELL_MARKET);
   if(!isBestAsk)return false;
   int bestBid = m_best_ask_index+1;
   bool isBestBid = bestBid >= 0 && bestBid < m_depth_total &&
                    (MarketBook[bestBid].type == BOOK_TYPE_BUY ||
                    MarketBook[bestBid].type == BOOK_TYPE_BUY_MARKET);
   if(isBestBid)
   {
      m_best_bid_index = bestBid;
      return true;
   }
   return false;
}

Его работа проста. Прежде всего метод удостоверяется, что текущий индекс лучшего предложения по-прежнему соответствует индексу предложения. Затем он сбрасывает индекс лучшего спроса и заново пытается его найти, обращаясь к элементу, следующему за индексом лучшего предложения:

int bestBid = m_best_ask_index+1;

Если найденный элемент действительно является лучшим индексом спроса, значит, предыдущее состояние стакана имело то же количество уровней продавцов и покупателей, что и текущее. Благодаря этому перебора стакана удается избежать, ведь в методе SetBestAskAndBidIndex перед перебором делается вызов метода FindBestBid. Таким образом, перебор стакана производится лишь в момент первого вызова функции, а также в случае изменения количества уровней на продажу и/или на покупку.

Хотя результирующий исходный код получился больше и сложнее простого перебора стакана, в действительности он будет работать быстрее. Особенно выигрыш в производительности будет заметен на больших стаканах ликвидных рынков. Простые инструкции на проверку условий выполняются очень быстро, а само количество этих проверок значительно меньше циклов оператора for. Поэтому производительность этого метода поиска индексов лучших цен будет выше обычного перебора.

 

2.4. Определение предельного проскальзывания с помощью метода GetDeviationByVol

Зачастую стакан цен используется многими трейдерами для определения текущей ликвидности на рынке, то есть стакан цен используется ими как дополнительный инструмент для контроля своего риска. Ведь если ликвидность на рыкне низкая, вход в рынок с помощью рыночных приказов может вызвать высокое проскальзывание. Проскальзывание — это всегда дополнительные убытки, порою оно может достигать существенных величин.

Чтобы не происходило подобных ситуаций, необходимо использовать дополнительные приемы, контролирующие вход в рынок. Более подробно об этих приемах написано в специальной статье "Как обезопасить себя и своего эксперта при торговле на Московской бирже". Поэтому мы не будем подробно описывать эти методы, а лишь скажем, что получая доступ к стакану, мы можем оценить величину своего потенциального проскальзывания еще до входа в рынок. Величина проскальзывания зависит от двух факторов:

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

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

Класс CMarketBook содержит специальный метод для вычисления этой характеристики: GetDeviationByVol. Так как на величину проскальзывания влияет объем сделки, методу необходимо передать объем, который предполагается исполнить на рынке. Поскольку в методе используется целочисленная арифметика объемов, принятая на срочном рынке Московской биржи, метод принимает объем в виде целочисленного типа long. Помимо этого, методу необходимо знать, для какой стороны ликвидности требуется выполнить расчет, для чего используется специальное перечисление ENUM_MBOOK_SIDE:

//+------------------------------------------------------------------+
//| Side of MarketBook.                                              |
//+------------------------------------------------------------------+
enum ENUM_MBOOK_SIDE
{
   MBOOK_ASK,                    // Ask side
   MBOOK_BID                     // Bid side
};

Теперь давайте приведем исходный код метода GetDeviationByVol:

//+------------------------------------------------------------------+
//| Get deviation value by volume. Return -1.0 if deviation is       |
//| infinity (insufficient liquidity)                                |
//+------------------------------------------------------------------+
double CMarketBook::GetDeviationByVol(long vol, ENUM_MBOOK_SIDE side)
{
   int best_ask = InfoGetInteger(MBOOK_BEST_ASK_INDEX);
   int last_ask = InfoGetInteger(MBOOK_LAST_ASK_INDEX); 
   int best_bid = InfoGetInteger(MBOOK_BEST_BID_INDEX);
   int last_bid = InfoGetInteger(MBOOK_LAST_BID_INDEX);
   double avrg_price = 0.0;
   long volume_exe = vol;
   if(side == MBOOK_ASK)
   {
      for(int i = best_ask; i >= last_ask; i--)
      {
         long currVol = MarketBook[i].volume < volume_exe ?
                        MarketBook[i].volume : volume_exe ;   
         avrg_price += currVol * MarketBook[i].price;
         volume_exe -= MarketBook[i].volume;
         if(volume_exe <= 0)break;
      }
   }
   else
   {
      for(int i = best_bid; i <= last_bid; i++)
      {
         long currVol = MarketBook[i].volume < volume_exe ?
                        MarketBook[i].volume : volume_exe ;   
         avrg_price += currVol * MarketBook[i].price;
         volume_exe -= MarketBook[i].volume;
         if(volume_exe <= 0)break;
      }
   }
   if(volume_exe > 0)
      return -1.0;
   avrg_price/= (double)vol;
   double deviation = 0.0;
   if(side == MBOOK_ASK)
      deviation = avrg_price - MarketBook[best_ask].price;
   else
      deviation = MarketBook[best_bid].price - avrg_price;
   return deviation;
}

Как видно, код занимает существенный объем, но его принцип расчета несложен. Во первых, делается перебор стакана от лучшей к худшей цене. Для каждого направления производится перебор в свою сторону. Во время перебора текущий объем добавляется к суммарному объему. Если суммарный объем набран и соответствует заявленному объему, происходит выход из цикла. Затем рассчитывается средняя цена входа для заданного объема. И наконец, рассчитывается разница между средней ценой входа и лучшей ценой спроса/предложения. Абсолютная разница и будет составлять оценочное отклонение.

Данный метод требует для расчета прямой перебор стакана. И хотя перебирается лишь одна из двух частей стакана цен и, как правило, не полностью, все равно данный расчет требует больше времени, чем расчет часто используемых индексов в стакане. Поэтому этот расчет реализован непосредственно в отдельном методе и производится по требованию, то есть только лишь в тех случаях, когда требуется получить эту информацию в явном виде.

 

2.5. Примеры работы с классом CMarketBook

Итак, мы рассмотрели основные методы класса CMarketBook, и настало время немного попрактиковаться в его использовании. Наш тестовый пример будет достаточно простым и понятным даже для начинающих программистов. Напишем тестовый советник, выполняющий однократный вывод текущей информации по стакану. Конечно, для этих целей скрипт подошел бы лучше, однако доступ к стакану через скрипт невозможен, и необходимо использовать либо советник, либо индикатор. Исходный код нашего советника представлен ниже:

//+------------------------------------------------------------------+
//|                                               TestMarketBook.mq5 |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <Trade\MarketBook.mqh>     // Включаем класс CMarketBook
CMarketBook Book(Symbol());         // Инициализируем класс текущим инструментом

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
int OnInit()
  {
   PrintMbookInfo();
   return INIT_SUCCEEDED;
  }
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnTimer()
  {
//---
   
  }
//+------------------------------------------------------------------+
//| Print MarketBook Info                                            |
//+------------------------------------------------------------------+
void PrintMbookInfo()
  {
   Book.Refresh();                  // Обновляем состояние стакана.
   /* Получаем основную статистику */
   int total=Book.InfoGetInteger(MBOOK_DEPTH_TOTAL);
   int total_ask = Book.InfoGetInteger(MBOOK_DEPTH_ASK);
   int total_bid = Book.InfoGetInteger(MBOOK_DEPTH_BID);
   int best_ask = Book.InfoGetInteger(MBOOK_BEST_ASK_INDEX);
   int best_bid = Book.InfoGetInteger(MBOOK_BEST_BID_INDEX);

   printf("ОБЩАЯ ГЛУБИНА СТАКАНА: "+(string)total);
   printf("КОЛИЧЕСТВО ЦЕНОВЫХ УРОВНЕЙ НА ПРОДАЖУ: "+(string)total_ask);
   printf("КОЛИЧЕСТВО ЦЕНОВЫХ УРОВНЕЙ НА ПОКУПКУ: "+(string)total_bid);
   printf("ИНДЕКС ЛУЧШЕГО ПРЕДЛОЖЕНИЯ: "+(string)best_ask);
   printf("ИНДЕКС ЛУЧШЕГО СПРОСА: "+(string)best_bid);
   
   double best_ask_price = Book.InfoGetDouble(MBOOK_BEST_ASK_PRICE);
   double best_bid_price = Book.InfoGetDouble(MBOOK_BEST_BID_PRICE);
   double last_ask = Book.InfoGetDouble(MBOOK_LAST_ASK_PRICE);
   double last_bid = Book.InfoGetDouble(MBOOK_LAST_BID_PRICE);
   double avrg_spread = Book.InfoGetDouble(MBOOK_AVERAGE_SPREAD);
   
   printf("ЛУЧШАЯ ЦЕНА ПРЕДЛОЖЕНИЯ: " + DoubleToString(best_ask_price, Digits()));
   printf("ЛУЧШАЯ ЦЕНА СПРОСА: " + DoubleToString(best_bid_price, Digits()));
   printf("ХУДШАЯ ЦЕНА ПРЕДЛОЖЕНИЯ: " + DoubleToString(last_ask, Digits()));
   printf("ХУДШАЯ ЦЕНА СПРОСА: " + DoubleToString(last_bid, Digits()));
   printf("СРЕДНИЙ СПРЕД: " + DoubleToString(avrg_spread, Digits()));
  }
//+------------------------------------------------------------------+

Запустив этот тестовый эксперт на графике OFZ2, мы получим следующий отчет:

2015.06.16 17:13:23.482 TestMarketBook (OFZ2-9.15,D1)   СРЕДНИЙ СПРЕД: 70
2015.06.16 17:13:23.482 TestMarketBook (OFZ2-9.15,D1)   ХУДШАЯ ЦЕНА СПРОСА: 9831
2015.06.16 17:13:23.482 TestMarketBook (OFZ2-9.15,D1)   ХУДШАЯ ЦЕНА ПРЕДЛОЖЕНИЯ: 9999
2015.06.16 17:13:23.482 TestMarketBook (OFZ2-9.15,D1)   ЛУЧШАЯ ЦЕНА СПРОСА: 9840
2015.06.16 17:13:23.482 TestMarketBook (OFZ2-9.15,D1)   ЛУЧШАЯ ЦЕНА ПРЕДЛОЖЕНИЯ: 9910
2015.06.16 17:13:23.482 TestMarketBook (OFZ2-9.15,D1)   ИНДЕКС ЛУЧШЕГО СПРОСА: 7
2015.06.16 17:13:23.482 TestMarketBook (OFZ2-9.15,D1)   ИНДЕКС ЛУЧШЕГО ПРЕДЛОЖЕНИЯ: 6
2015.06.16 17:13:23.482 TestMarketBook (OFZ2-9.15,D1)   КОЛИЧЕСТВО ЦЕНОВЫХ УРОВНЕЙ НА ПОКУПКУ: 2
2015.06.16 17:13:23.482 TestMarketBook (OFZ2-9.15,D1)   КОЛИЧЕСТВО ЦЕНОВЫХ УРОВНЕЙ НА ПРОДАЖУ: 7
2015.06.16 17:13:23.482 TestMarketBook (OFZ2-9.15,D1)   ОБЩАЯ ГЛУБИНА СТАКАНА: 9

Сравним получившийся отчет со скриншотом стакана по этому инструменту:

 

Рис. 4. Стакан цен по OFZ2 в момент запуска тестового отчета

Как видно, полученные индексы и цены полностью соответствуют текущему стакану цен, в чем нам и требовалось убедиться. 

 

Глава 3. Пишем свой стакан цен в виде панели-индикатора


3.1. Общие принципы проектирования панели стакана цен. Создание индикатора

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

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

 

Рис. 5. Стандартная торговая панель в MetaTrader 5

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

При создании индикатора необходимо использовать одну из двух системных функций OnCalculate. Так как наша панель не использует информацию, получаемую в этих функциях, мы оставим эти методы пустыми. Индикатор также не будет использовать ни одной графической серии, поэтому свойство indicator_plots в нашем случае будет равно нулю.

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

Панель стакана цен будет реализована в виде специального класса CBookPanel. Сейчас, не вдаваясь в подробности этого класса, приведем код самого стартового файла индикатора:

//+------------------------------------------------------------------+
//|                                                   MarketBook.mq5 |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property indicator_chart_window
#property indicator_plots 0
#include <Trade\MarketBook.mqh>
#include "MBookPanel.mqh"

CBookPanel Panel;
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   MarketBookAdd(Symbol());
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| MarketBook change event                                          |
//+------------------------------------------------------------------+
void OnBookEvent(const string &symbol)
  {
   Panel.Refresh();
  }
//+------------------------------------------------------------------+
//| Chart events                                                     |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,         // идентификатор события  
                  const long& lparam,   // параметр события типа long
                  const double& dparam, // параметр события типа double
                  const string& sparam) // параметр события типа string
  {
   Panel.Event(id,lparam,dparam,sparam);
   ChartRedraw();
  }
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &price[])
  {
//---

//--- return value of prev_calculated for next call
   return(rates_total);
  }
//+------------------------------------------------------------------+

Сейчас класс CBookPanel содержит только базовые элементы для своей работы: стрелку, при нажатии на которую в будущем будет появляться наш стакан цен, и надпись "MarketBook", подписывающую наш будущий стакан. При запуске нашего индикатора, график будет выглядеть следующим образом:

 

Рис. 6. Расположение будущей панели MarketBook на графике

Каждый элемент этого класса — также независимый класс, производный от базового класса CNode. Этот класс содержит базовые методы, вроде Show и Hide, которые можно переопределить в классах-потомках. Также класс CNode генерирует уникальное имя для каждого экземпляра, что делает более удобным использование стандартных функций для создания графических объектов и задания их свойств.

 

3.2. Обработка события нажатия и создание формы стакана

Сейчас наш индикатор не реагирует на нажатие стрелки, поэтому продолжим его дорабатывать. Первое, что необходимо сделать для нашей панели — это ввести обработчик события OnChartEvent. Назовем этот метод Event. Он будет принимать параметры, получаемые из OnChartEvent. Также расширим базовый класс CNode, снабдив его массивом CArrayObj, в котором будут храниться другие графические элементы типа CNode. В будущем это поможет нам создавать множество однотипных элементов — ячеек стакана.

Теперь приведем исходный код класса CBookPanel и его класса-родителя CNode: 

//+------------------------------------------------------------------+
//|                                                   MBookPanel.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#include <Trade\MarketBook.mqh>
#include "Node.mqh"
#include "MBookText.mqh"
#include "MBookFon.mqh"
//+------------------------------------------------------------------+
//| CBookPanel class                                                 |
//+------------------------------------------------------------------+
class CBookPanel : CNode
  {
private:
   CMarketBook       m_book;
   bool              m_showed;
   CBookText         m_text;
public:
   CBookPanel();
   ~CBookPanel();
   void              Refresh();
   virtual void Event(int id, long lparam, double dparam, string sparam);
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CBookPanel::CBookPanel()
{
   m_elements.Add(new CBookFon(GetPointer(m_book)));
   ObjectCreate(ChartID(), m_name, OBJ_LABEL, 0, 0, 0);
   ObjectSetInteger(ChartID(), m_name, OBJPROP_XDISTANCE, 70);
   ObjectSetInteger(ChartID(), m_name, OBJPROP_YDISTANCE, -3);
   ObjectSetInteger(ChartID(), m_name, OBJPROP_COLOR, clrBlack);
   ObjectSetString(ChartID(), m_name, OBJPROP_FONT, "Webdings");
   ObjectSetString(ChartID(), m_name, OBJPROP_TEXT, CharToString(0x36));
}
CBookPanel::~CBookPanel(void)
{
   OnHide();
   m_text.Hide();
   ObjectDelete(ChartID(), m_name);
}

CBookPanel::Refresh(void)
{

}


CBookPanel::Event(int id, long lparam, double dparam, string sparam)
{
   switch(id)
   {
      case CHARTEVENT_OBJECT_CLICK:
      {
         if(sparam != m_name)return;
         if(!m_showed)OnShow();        
         else OnHide();
         m_showed = !m_showed;
      }
   }
}
//+------------------------------------------------------------------+

Метод Refresh, обновляющий состояние стакана, еще не заполнен. Его мы создадим чуть позже. Текущий функционал уже позволяет показать нам первый прототип нашего стакана. Пока при нажатии стрелки отображается только стандартная серая подложка. При повторном нажатии она исчезает:

 

Рис. 7. Внешний вид будущего стакан цен

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

 

3.3. Ячейки стакана.

Основу стакана составляют его ячейки. Каждая ячейка — это элемент таблицы, содержащий информацию об объеме или цене. Также ячейка различается по цвету: для лимитных ордеров на покупку она окрашивается в синий цвет, для лимитных ордеров на продажу — в розовый цвет. Для каждого стакана цен количество ячеек может быть разным, следовательно, все ячейки необходимо создавать динамически, по требованию, и хранить их в специальном контейнере данных CArrayObj. Так как все ячейки, независимо от того, что они показывают, имеют одинаковый размер и тип, то класс, реализующий разные типы ячеек, будет одним для всех типов ячеек.

И для ячеек, показывающих объем, и для ячеек, показывающих цену, будет использоваться специальный класс CBookCeil. Тип ячейки будет указываться во время создания объекта этого класса, таким образом, каждый экземпляр класса будет знать, какую именно информацию из стакана цен ему будет необходимо отобразить, и каким цветом окрасить свой фон. CBookCeil будет использовать два графических примитива: текстовую метку OBJ_TEXT_LABEL и прямоугольную метку OBJ_RECTANBLE_LABEL. Первая будет отображать текст, вторая — саму ячейку стакана.

Приведем исходный код класса CBookCeil:

//+------------------------------------------------------------------+
//|                                                   MBookPanel.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#include "Node.mqh"
#include <Trade\MarketBook.mqh>
#include "Node.mqh"
#include "MBookText.mqh"

#define BOOK_PRICE 0
#define BOOK_VOLUME 1

class CBookCeil : public CNode
{
private:
   long  m_ydist;
   long  m_xdist;
   int   m_index;
   int m_ceil_type;
   CBookText m_text;
   CMarketBook* m_book;
public:
   CBookCeil(int type, long x_dist, long y_dist, int index_mbook, CMarketBook* book);
   virtual void Show();
   virtual void Hide();
   virtual void Refresh();
   
};

CBookCeil::CBookCeil(int type, long x_dist, long y_dist, int index_mbook, CMarketBook* book)
{
   m_ydist = y_dist;
   m_xdist = x_dist;
   m_index = index_mbook;
   m_book = book;
   m_ceil_type = type;
}

void CBookCeil::Show()
{
   ObjectCreate(ChartID(), m_name, OBJ_RECTANGLE_LABEL, 0, 0, 0);
   ObjectSetInteger(ChartID(), m_name, OBJPROP_XDISTANCE, m_xdist);
   ObjectSetInteger(ChartID(), m_name, OBJPROP_YDISTANCE, m_ydist);
   ObjectSetInteger(ChartID(), m_name, OBJPROP_COLOR, clrBlack);
   ObjectSetInteger(ChartID(), m_name, OBJPROP_FONTSIZE, 9);
   ObjectSetInteger(ChartID(), m_name, OBJPROP_BORDER_TYPE, BORDER_FLAT);
   m_text.Show();
   m_text.SetXDist(m_xdist+10);
   m_text.SetYDist(m_ydist+2);
   Refresh();
}

void CBookCeil::Refresh(void)
{
   ENUM_BOOK_TYPE type = m_book.MarketBook[m_index].type;
   if(type == BOOK_TYPE_BUY || type == BOOK_TYPE_BUY_MARKET)
      ObjectSetInteger(ChartID(), m_name, OBJPROP_BGCOLOR, clrCornflowerBlue);
   else if(type == BOOK_TYPE_SELL || type == BOOK_TYPE_SELL_MARKET)
      ObjectSetInteger(ChartID(), m_name, OBJPROP_BGCOLOR, clrPink);
   else
      ObjectSetInteger(ChartID(), m_name, OBJPROP_BGCOLOR, clrWhite);
   MqlBookInfo info = m_book.MarketBook[m_index];
   if(m_ceil_type == BOOK_PRICE)
      m_text.SetText(DoubleToString(info.price, Digits()));
   else if(m_ceil_type == BOOK_VOLUME)
      m_text.SetText((string)info.volume);
}

void CBookCeil::Hide(void)
{
   OnHide();
   m_text.Hide();
   ObjectDelete(ChartID(),m_name);
}

Основную работу этого класса выполняют методы Show и Refresh. Последний, в зависимости от переданного типа ячейки, окрашивает ее в тот или иной цвет и отображает в ней объем либо цену. Чтобы создать ячейку, необходимо указать ее тип, местоположение по оси X, местоположение по оси Y, индекс стакана цен, которому соответствует данная ячейка, и собственно стакан цен, из которого ячейка будет получать информацию.

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

void CBookFon::CreateCeils()
{
   int total = m_book.InfoGetInteger(MBOOK_DEPTH_TOTAL);
   for(int i = 0; i < total; i++)
   {
      CBookCeil* Ceil = new CBookCeil(0, 12, i*15+20, i, m_book);
      CBookCeil* CeilVol = new CBookCeil(1, 63, i*15+20, i, m_book);
      m_elements.Add(Ceil);
      m_elements.Add(CeilVol);
      Ceil.Show();
      CeilVol.Show();
   }
}

Он будет вызываться при нажатии на стрелку, раскрывающую стакан цен.

Теперь все готово для создания нашей новой версии стакана. После внесения изменений и компиляции проекта наш индикатор приобрел новый вид:

 

Рис. 8. Первая версия стакана цен, в виде индикатора

3.4. Отображаем гистограмму объемов в стакане цен

Получившийся стакан цен уже выполняет основную функцию — он показывает торговые уровни, объем и цены лимитных заявок на продажу и покупку. При этом каждое изменение значений в стакане цен также меняет значения в соответствующих ячейках. Однако визуально непросто отслеживать величину объемов в получившейся таблице. Например, в стандартном стакане MetaTrader 5 объем отображается на фоне гистограммы, которая показывает относительную величину текущего объема по отношению к максимальному объему в стакане. Нам также не мешало бы внедрить подобный функционал в наш стакан.

Мы можем решить эту задачу разными путями. Самым простым решением было бы сделать все нужные расчеты прямо в классе CBookCeil. Для этого в его методе Refresh необходимо было бы написать следующее:

void CBookCeil::Refresh(void)
{
   ...
   MqlBookInfo info = m_book.MarketBook[m_index];
   ...
   //Обновляем гистограмму стакана цен
   int begin = m_book.InfoGetInteger(MBOOK_LAST_ASK_INDEX);
   int end = m_book.InfoGetInteger(MBOOK_BEST_ASK_INDEX);
   long max_volume = 0;
   if(m_ceil_type != BOOK_VOLUME)return;
   for(int i = begin; i < end; i++)
   {
      if(m_book.MarketBook[i].volume > max_volume)
         max_volume = m_book.MarketBook[i].volume;
   }
   double delta = 1.0;
   if(max_volume > 0)
      delta = (info.volume/(double)max_volume);
   long size = (long)(delta * 50.0);
   ObjectSetInteger(ChartID(), m_name, OBJPROP_XSIZE, size);
   ObjectSetInteger(ChartID(), m_name, OBJPROP_YDISTANCE, m_ydist);
}

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

 

Рис. 9. Стакан цен с гистограммой объемов

Однако проблема данного кода в том, что полный перебор стакана делается в каждой ячейке при каждом вызове Refresh. Для стакана с глубиной сорок элементов это означает 800 итераций цикла for за одно обновление стакана. Каждая ячейка делает перебор стакана только для своей стороны, поэтому перебор внутри каждой ячейки будет состоять из двадцати итераций (глубина стакана, разделенная на два). И хотя современные компьютеры справляются с этой задачей, это слишком неэффективный прием работы, особенно учитывая то, что работать со стаканом необходимо максимально быстрыми и эффективными алгоритмами.

 

3.5. Быстрый расчет максимальных объемов в стакане, оптимизация перебора

К сожалению, от полного перебора в стакане цен нельзя избавиться. После каждого обновления стакана цен, максимальный объем и его ценовой уровень может кардинальным образом измениться. Однако мы можем минимизировать количество переборов. Для этого перебор стакана нужно научиться делать не более одного раза между двумя вызовами Refresh. Второе, что необходимо сделать — это минимизировать само количество вызовов полного перебора. Для этого необходимо воспользоваться отложенным расчетом или, иными словами, выполнять этот расчет только при явном требовании. Все расчеты также непосредственно перенесем в класс стакана цен CMarketBook, для чего напишем специальный расчетный подкласс CBookCalculation, находящийся внутри CMarketBook. Ниже приведем его исходный код: 

class CMarketBook;

class CBookCalculation
{
private:
   int m_max_ask_index;         // Индекс максимального объема предложения
   long m_max_ask_volume;       // Объем максимального предложения
   
   int m_max_bid_index;         // Индекс максимального объема спроса
   long m_max_bid_volume;       // Объем максимального спроса
   
   long m_sum_ask_volume;       // Суммарный объем предложения в стакане
   long m_sum_bid_volume;       // Суммарный объем спроса в стакане.
   
   bool m_calculation;          // Флаг, указывающий, что все необходимые расчеты произведены
   CMarketBook* m_book;         // Указатель на стакан цен
   
   void Calculation(void)
   {
      // FOR ASK SIDE
      int begin = (int)m_book.InfoGetInteger(MBOOK_LAST_ASK_INDEX);
      int end = (int)m_book.InfoGetInteger(MBOOK_BEST_ASK_INDEX);
      for(int i = begin; i < end; i++)
      {
         if(m_book.MarketBook[i].volume > m_max_ask_volume)
         {
            m_max_ask_index = i;
            m_max_ask_volume = m_book.MarketBook[i].volume;
         }
         m_sum_ask_volume += m_book.MarketBook[i].volume;
      }
      // FOR BID SIDE
      begin = (int)m_book.InfoGetInteger(MBOOK_BEST_BID_INDEX);
      end = (int)m_book.InfoGetInteger(MBOOK_LAST_BID_INDEX);
      for(int i = begin; i < end; i++)
      {
         if(m_book.MarketBook[i].volume > m_max_bid_volume)
         {
            m_max_bid_index = i;
            m_max_bid_volume = m_book.MarketBook[i].volume;
         }
         m_sum_bid_volume += m_book.MarketBook[i].volume;
      }
      m_calculation = true;
   }
   
public:
   CBookCalculation(CMarketBook* book)
   {
      Reset();
      m_book = book;
   }
   
   void Reset()
   {
      m_max_ask_volume = 0.0;
      m_max_bid_volume = 0.0;
      m_max_ask_index = -1;
      m_max_bid_index = -1;
      m_sum_ask_volume = 0;
      m_sum_bid_volume = 0;
      m_calculation = false;
   }
   int GetMaxVolAskIndex()
   {
      if(!m_calculation)
         Calculation();
      return m_max_ask_index;
   }
   
   long GetMaxVolAsk()
   {
      if(!m_calculation)
         Calculation();
      return m_max_ask_volume;
   }
   int GetMaxVolBidIndex()
   {
      if(!m_calculation)
         Calculation();
      return m_max_bid_index;
   }
   
   long GetMaxVolBid()
   {
      if(!m_calculation)
         Calculation();
      return m_max_bid_volume;
   }
   long GetAskVolTotal()
   {
      if(!m_calculation)
         Calculation();
      return m_sum_ask_volume;
   }
   long GetBidVolTotal()
   {
      if(!m_calculation)
         Calculation();
      return m_sum_bid_volume;
   }
};

Весь перебор стакана и ресурсоемкие вычисления скрыты внутри приватного метода Calculate. Он вызывается только в том случае, если флаг расчета m_calculate сброшен в состояние false. Сброс этого флага происходит только в одном-единственном месте — в методе Reset. Так как этот класс создан исключительно для работы внутри класса CMarketBook, доступ к нему имеет только этот класс.

После обновления стакана метод Refresh класса CMarketBook сбрасывает состояние расчетного модуля, вызывая его метод Reset. Благодаря этому полный перебор стакана происходит не более одного раза между двумя его обновлениями. Также используется отложенное выполнение. Иными словами, метод Calculate класса CBookCalcultae вызывается только в том случае, когда происходит явный вызов одного из шести его общедоступных методов.

Помимо нахождения объема, в класс, осуществляющий полный перебор стакана, были добавлены поля, содержащие суммарный объем лимитных ордеров на продажу и покупку. Дополнительного времени для расчета эти параметры не потребуют, так как рассчитываются в общем цикле массива.

Теперь вместо постоянного перебора стакана используется умный перебор по требованию. Это существенно сокращает используемые ресурсы и делает работу со стаканом максимально эффективной и быстрой.

 

3.6. Последние штрихи: гистограмма объемов и разделительная черта

Мы практически полностью завершили работу над созданием нашего индикатора. Практическая необходимость в нахождении максимального объема помогла нам создать эффективный метод экономного расчета необходимых показателей. Если в дальнейшем мы захотим добавить новые расчетные параметры в наш стакан, сделать это будет достаточно просто — для этого достаточно будет расширить наш класс CBookCalculate соответствующими методами и ввести соответствующие модификаторы в перечисления ENUM_MBOOK_INFO_INTEGER и ENUM_MBOOK_INFO_DOUBLE.

Теперь нам осталось воспользоваться проделанной работой и переписать метод Refresh для каждой ячейки:

void CBookCeil::Refresh(void)
{
   ENUM_BOOK_TYPE type = m_book.MarketBook[m_index].type;
   long max_volume = 0;
   if(type == BOOK_TYPE_BUY || type == BOOK_TYPE_BUY_MARKET)
   {
      ObjectSetInteger(ChartID(), m_name, OBJPROP_BGCOLOR, clrCornflowerBlue);
      max_volume = m_book.InfoGetInteger(MBOOK_MAX_BID_VOLUME);
   }
   else if(type == BOOK_TYPE_SELL || type == BOOK_TYPE_SELL_MARKET)
   {
      ObjectSetInteger(ChartID(), m_name, OBJPROP_BGCOLOR, clrPink);
      max_volume = m_book.InfoGetInteger(MBOOK_MAX_ASK_VOLUME); //Объем уже рассчитан ранее, повторного перебора не происходит
   }
   else
      ObjectSetInteger(ChartID(), m_name, OBJPROP_BGCOLOR, clrWhite);
   MqlBookInfo info = m_book.MarketBook[m_index];
   if(m_ceil_type == BOOK_PRICE)
      m_text.SetText(DoubleToString(info.price, Digits()));
   else if(m_ceil_type == BOOK_VOLUME)
      m_text.SetText((string)info.volume);
   if(m_ceil_type != BOOK_VOLUME)return;
   double delta = 1.0;
   if(max_volume > 0)
      delta = (info.volume/(double)max_volume);
   long size = (long)(delta * 50.0);
   ObjectSetInteger(ChartID(), m_name, OBJPROP_XSIZE, size);
   ObjectSetInteger(ChartID(), m_name, OBJPROP_YDISTANCE, m_ydist);
}

Визуально наша панель-индикатор работает так же, как и в предыдущей версии, однако в реальности скорость расчета гистограммы существенно увеличилась. Именно в этом и состоит искусство программирования — создавать эффективные и простые в использовании алгоритмы, скрывая сложность их реализации внутри приватных методов соответствующих модулей (классов).

С появлением гистограммы объемов стало не совсем понятно, где проходит разделительная черта между объемами спроса и предложения. Поэтому мы создадим такую черту, дополнив класс CBookPanel специальным подклассом CBookLine, реализующим эту черту:

class CBookLine : public CNode
{
private:
   long m_ydist;
public:
   CBookLine(long y){m_ydist = y;}
   virtual void Show()
   {
      ObjectCreate(ChartID(),     m_name, OBJ_RECTANGLE_LABEL, 0, 0, 0);
      ObjectSetInteger(ChartID(), m_name, OBJPROP_YDISTANCE, m_ydist);
      ObjectSetInteger(ChartID(), m_name, OBJPROP_XDISTANCE, 13);
      ObjectSetInteger(ChartID(), m_name, OBJPROP_YSIZE, 3);
      ObjectSetInteger(ChartID(), m_name, OBJPROP_XSIZE, 108);
      ObjectSetInteger(ChartID(), m_name, OBJPROP_COLOR, clrBlack);
      ObjectSetInteger(ChartID(), m_name, OBJPROP_BGCOLOR, clrBlack);
      ObjectSetInteger(ChartID(), m_name, OBJPROP_BORDER_TYPE, BORDER_FLAT);
   }
};

Это очень простой класс, фактически определяющий лишь свое положение. Положение черты по оси Y нужно рассчитать в момент ее создания в методе Show. Сделать это достаточно просто, зная индекс лучшего предложения:

long best_bid = m_book.InfoGetInteger(MBOOK_BEST_BID_INDEX);
long y = best_bid*15+19;

В данном случае индекс ячейки best_bid умножается на ширину каждой ячейки (15 пикселей), и к ней прибавляется дополнительная константа в 19 пикселей.

Наш новый стакан, наконец, приобрел тот минимальный внешний вид и функционал, при котором его использование становится приятным. Конечно, еще многое можно было бы сделать. При желании наш индикатор можно было бы максимально приблизить по своей функциональности к стандартному стакану MetaTrader 5.

Но основная цель данной статьи не в этом. Панель стакана цен создавалась лишь с целью показать возможности класса CMarketBook. Она помогла сделать этот класс быстрее, лучше и функциональнее, и поэтому полностью выполнила свою задачу. Мы же покажем небольшое видео, на котором будет видна проделанная нами работа. Внизу представлена наша панель стакана цен в динамике:

 


3.7. Дополняем свойства стакана информацией об общем количестве лимитных ордеров по инструменту

Одной отличительной особенностью Московской биржи является трансляция информации об общем количестве лимитных ордеров в режиме реального времени. Данная статья освещает работу стакана цен как такового, безотносительно к конкретному рынку. Тем не менее, данная информация, хотя и является специфической (свойственной конкретной торговой площадке), доступна на системном уровне терминала. К тому же, она расширяет данные, предоставленные стаканом цен. Поэтому было решено расширить перечисления модификаторов свойств и включить поддержку этих свойств прямо в класс стакана цен CMarketBook.

Итак, Московская биржа предоставляет следующую информацию в режиме реального времени:

Хотя открытый интерес непосредственно не связан с количеством лимитных ордеров на рынке (т.е. с его текущей ликвидностью), тем не менее, данная информация зачастую требуется совместно с информацией о лимитных ордерах, поэтому доступ к ней через класс CMarketBook также выглядит уместным. Для доступа к этой информации необходимо использовать функции SymbolInfoInteger и SymbolInfoDouble. Однако для того чтобы эти данные были доступны из одного места, наш класс стакана цен мы расширим, введя дополнительные перечисления и изменения в его функции InfoGetInteger и InfoGetDouble:

long CMarketBook::InfoGetInteger(ENUM_MBOOK_INFO_INTEGER property)
{
   switch(property)
   {
      ...
      case MBOOK_BUY_ORDERS:
         return SymbolInfoInteger(m_symbol, SYMBOL_SESSION_BUY_ORDERS);
      case MBOOK_SELL_ORDERS:
         return SymbolInfoInteger(m_symbol, SYMBOL_SESSION_SELL_ORDERS);
      ...
   }
   return 0;
}

 

double CMarketBook::InfoGetDouble(ENUM_MBOOK_INFO_DOUBLE property)
{
   switch(property)
   {
      ...
      case MBOOK_BUY_ORDERS_VOLUME:
         return SymbolInfoDouble(m_symbol, SYMBOL_SESSION_BUY_ORDERS_VOLUME);
      case MBOOK_SELL_ORDERS_VOLUME:
         return SymbolInfoDouble(m_symbol, SYMBOL_SESSION_SELL_ORDERS_VOLUME);
      case MBOOK_OPEN_INTEREST:
         return SymbolInfoDouble(m_symbol, SYMBOL_SESSION_INTEREST);
   }
   return 0.0;  
}

Как видно, код достаточно прост. По сути он дублирует стандартный функционал MQL. Но смысл его добавления в класс CMarketBook именно в том, чтобы предоставить пользователям удобный и централизованный модуль для доступа к информации о лимитных ордерах и их ценовых уровнях.


Глава 4. Документация к классу CMarketBook

Мы завершили описание и создание класса для работы со стаканом цен CMarketBook. Четвертая глава содержит документацию по его открытым методам. Благодаря этой документации использование класса станет простым и понятным, даже для начинающих программистов. Также данную главу удобно будет использовать в виде небольшого справочника по работе с классом.


4.1. Методы для получения основной информации из стакана и работы с ним

Метод Refresh()

Обновляет состояние стакана. При каждом вызове системного события OnBookEvent (состояние стакана изменилось) необходимо также вызывать данный метод.

void        Refresh(void);

Использование

Пример использования смотрите в соответствующем разделе четвертой главы.

 

Метод InfoGetInteger()

Возвращает одно из свойств стакана, соответствующее модификатору ENUM_MBOOK_INFO_INTEGER. Полный перечень поддерживаемых свойств смотрите в описании перечисления  ENUM_MBOOK_INFO_INTEGER.

long        InfoGetInteger(ENUM_MBOOK_INFO_INTEGER property);

Возвращаемое значение

Целочисленное свойство стакана типа long. В случае неудачи возвращает -1.

Использование

Пример использования смотрите в соответствующем разделе четвертой главы. 

 

Метод InfoGetDouble()

Возвращает одно из свойств стакана, соответствующее модификатору ENUM_MBOOK_INFO_DOUBLE. Полный перечень поддерживаемых свойств смотрите в описании перечисления  ENUM_MBOOK_INFO_DOUBLE.

double      InfoGetDouble(ENUM_MBOOK_INFO_DOUBLE property);

Возвращаемое значение

Свойство стакана типа double. В случае неудачи, возвращает -1.0.

Использование

Пример использования смотрите в соответствующем разделе четвертой главы.  

 

Метод IsAvailable()

Возвращает true, если информация по стакану доступна, и false в противном случае. Данный метод необходимо вызывать перед началом работы с классом стакана для проверки возможности работы с данным видом информации.

bool        IsAvailable(void);

Возвращаемое значение

True, если стакан доступен для дальнейшей работы, и false в противном случае.

 

Метод SetMarketBookSymbol()

Устанавливает символ, со стаканом цен которого необходимо работать. Также символ стакана цен можно установить во время создания экземпляра класса CMarketBook, явно указав в конструкторе название используемого символа.

bool        SetMarketBookSymbol(string symbol);

Возвращаемое значение

True, если символ 'symbol' доступен для торговли, false в противном случае.

 

Метод GetMarketBookSymbol()

Возвращает символ инструмента, для работы со стаканом цен которого настроен текущий экземпляр класса. 

string      GetMarketBookSymbol(void);

Возвращаемое значение

Название инструмента, чей стакан отображает текущий экземпляр класса. NULL, если инструмент не выбран либо не доступен. 

 

Метод GetDeviationByVol()

Возвращает величину потенциального проскальзывания при входе в рынок рыночным ордером. Эта величина носит оценочный характер и фактическое получаемое проскальзывание может отличаться от рассчитанного этой функцией, если состояние стакана в момент входа изменится. Тем не менее, данная функция дает достаточно точную оценку проскальзывания, которое произойдет при входе в рынок, и может служит источником дополнительной информации.

Метод принимает два параметра: объем предполагаемой сделки и перечисление, указывающее на тип используемой ликвидности при заключении сделки. Например, для покупки будет использована ликвидность лимитных ордеров, выставленных на продажу, и в этом случае в качестве стороны 'side' необходимо будет указать тип MBOOK_ASK. Для продажи, наоборот, потребуется указать MBOOK_BID. Более подробную информацию о стороне стакана смотрите в описании перечисления ENUM_BOOK_SIDE.

double     GetDeviationByVol(long vol, ENUM_MBOOK_SIDE side);

Параметры:

Возвращаемое значение

Величина потенциального проскальзывания в пунктах инструмента.

 

4.2. Перечисления и модификаторы класса CMarketBook

Перечисление ENUM_MBOOK_SIDE

Перечисление ENUM_BOOK_SIDE содержит модификаторы, указывающие на тип используемой ликвидности. Поля перечисления и их описания указаны ниже:

ПолеОписание
MBOOK_ASK Указывает на ликвидность предоставленную лимитными ордерами на продажу.
MBOOK_BID Указывает на ликвидность предоставленную лимитными ордерами на покупку.

Примечание 

Каждый рыночный ордер может быть исполнен лимитными заявками. В зависимости от направления заявки используются лимитные ордера на покупку или на продажу. Обратной стороной сделки на покупку будет один или несколько лимитных ордеров на продажу. Обратной стороной сделки на продажу будет один или несколько лимитных ордеров на покупку. Данный модификатор, таким образом, указывает на одну из двух частей стакана: сторону покупки или сторону продажи. Модификатор используется функцией GetDeviationByVol, для работы которой необходимо знать, какую сторону ликвидности будет задействовать предполагаемая рыночная сделка.

 

Перечисление ENUM_MBOOK_INFO_INTEGER

Перечисление ENUM_MBOOK_INFO_INTEGER содержит модификаторы свойств, которые требуется получить с помощью метода InfoGetInteger. Поля перечисления и их описания указаны ниже:

ПолеОписание
MBOOK_BEST_ASK_INDEX Индекс лучшей цены предложения
MBOOK_BEST_BID_INDEX Индекс лучшей цены спроса
MBOOK_LAST_ASK_INDEX Индекс худшей или последней цены предложения
MBOOK_LAST_BID_INDEX Индекс худшей или последней цены спроса
MBOOK_DEPTH_ASK Глубина стакана цен со стороны предложения или его общее количество торговых уровней
MBOOK_DEPTH_BID Глубина стакана цен со стороны спроса или его общее количество торговых уровней
MBOOK_DEPTH_TOTAL Общая глубина стакана цен или количество торговых уровней на покупку и продажу
MBOOK_MAX_ASK_VOLUME Максимальный объем предложения
MBOOK_MAX_ASK_VOLUME_INDEX Индекс уровня максимального объема предложения
MBOOK_MAX_BID_VOLUME Максимальный объем спроса
MBOOK_MAX_BID_VOLUME_INDEX Индекс уровня максимального спроса
MBOOK_ASK_VOLUME_TOTAL Суммарный объем лимитных ордеров на продажу, доступный в текущем стакане
MBOOK_BID_VOLUME_TOTAL  Суммарный объем лимитных ордеров на покупку, доступный в текущем стакане
MBOOK_BUY_ORDERS Весь объем лимитных ордеров на покупку, выставленных в текущий момент на бирже
MBOOK_SELL_ORDERS Весь объем лимитных ордеров на продажу, выставленных в текущий момент на бирже

 

Перечисление ENUM_MBOOK_INFO_DOUBLE

Перечисление ENUM_MBOOK_INFO_DOUBLE содержит модификаторы свойств, которые требуется получить с помощью метода InfoGetDouble. Поля перечисления и их описания указаны ниже:

ПолеОписание
MBOOK_BEST_ASK_PRICE Цена лучшего предложения
MBOOK_BEST_BID_PRICE Цена лучшего спроса
MBOOK_LAST_ASK_PRICE Цена худшего или последнего предложения
MBOOK_LAST_BID_PRICE Цена худшего или последнего спроса
MBOOK_AVERAGE_SPREAD Средняя разница между лучшим спросом и предложением или спред
MBOOK_OPEN_INTEREST  Открытый интерес
MBOOK_BUY_ORDERS_VOLUME Количество ордеров на покупку
MBOOK_SELL_ORDERS_VOLUME  Количество ордеров на продажу

 

4.3. Пример использования класса CMarketBook

Данный пример содержит исходный код в виде эксперта, выводящего основную информацию по стакану цен в момент его запуска:

//+------------------------------------------------------------------+
//|                                               TestMarketBook.mq5 |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <Trade\MarketBook.mqh>     // Включаем класс CMarketBook
CMarketBook Book(Symbol());         // Инициализируем класс текущим инструментом

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
int OnInit()
  {
   PrintMbookInfo();
   return INIT_SUCCEEDED;
  }
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnTimer()
  {
//---
   
  }
//+------------------------------------------------------------------+
//| Print MarketBook Info                                            |
//+------------------------------------------------------------------+
void PrintMbookInfo()
  {
   Book.Refresh();                                                   // Обновляем состояние стакана.
//--- Получаем основную целочисленную статистику
   int total=(int)Book.InfoGetInteger(MBOOK_DEPTH_TOTAL);            // Получаем общую глубину стакана
   int total_ask = (int)Book.InfoGetInteger(MBOOK_DEPTH_ASK);        // Получаем количество ценовых уровней на продажу
   int total_bid = (int)Book.InfoGetInteger(MBOOK_DEPTH_BID);        // Получаем количество ценовых уровней на покупку
   int best_ask = (int)Book.InfoGetInteger(MBOOK_BEST_ASK_INDEX);    // Получаем лучший индекс предложения
   int best_bid = (int)Book.InfoGetInteger(MBOOK_BEST_BID_INDEX);    // Получаем лучший индекс спроса

//--- Выводим основную статистику
   printf("ОБЩАЯ ГЛУБИНА СТАКАНА: "+(string)total);
   printf("КОЛИЧЕСТВО ЦЕНОВЫХ УРОВНЕЙ НА ПРОДАЖУ: "+(string)total_ask);
   printf("КОЛИЧЕСТВО ЦЕНОВЫХ УРОВНЕЙ НА ПОКУПКУ: "+(string)total_bid);
   printf("ИНДЕКС ЛУЧШЕГО ПРЕДЛОЖЕНИЯ: "+(string)best_ask);
   printf("ИНДЕКС ЛУЧШЕГО СПРОСА: "+(string)best_bid);
   
//--- Получаем основную статистику double
   double best_ask_price = Book.InfoGetDouble(MBOOK_BEST_ASK_PRICE); // Получаем лучшию цену предложения
   double best_bid_price = Book.InfoGetDouble(MBOOK_BEST_BID_PRICE); // Получаем лучшую цену спроса
   double last_ask = Book.InfoGetDouble(MBOOK_LAST_ASK_PRICE);       // Получаем худшую цену предложения
   double last_bid = Book.InfoGetDouble(MBOOK_LAST_BID_PRICE);       // Получаем худшую цену спроса
   double avrg_spread = Book.InfoGetDouble(MBOOK_AVERAGE_SPREAD);    // Получаем средний спред за время работы стакана цен
   
//--- Выводим цены и спред
   printf("ЛУЧШАЯ ЦЕНА ПРЕДЛОЖЕНИЯ: " + DoubleToString(best_ask_price, Digits()));
   printf("ЛУЧШАЯ ЦЕНА СПРОСА: " + DoubleToString(best_bid_price, Digits()));
   printf("ХУДШАЯ ЦЕНА ПРЕДЛОЖЕНИЯ: " + DoubleToString(last_ask, Digits()));
   printf("ХУДШАЯ ЦЕНА СПРОСА: " + DoubleToString(last_bid, Digits()));
   printf("СРЕДНИЙ СПРЕД: " + DoubleToString(avrg_spread, Digits()));
  }
//+------------------------------------------------------------------+

 

Заключение

Статья получилась достаточно динамичной. Мы рассмотрели стакан цен с технической точки зрения и предложили высокопроизводительный класс-контейнер для работы с ним. В качестве примера мы создали на базе этого класса-контейнера индикатор стакана цен, который можно компактно отобразить на ценовом графике инструмента.

Наш индикатор стакана цен получился очень простым, в нем еще многого не хватает. Однако главная цель была достигнута — мы убедились, что с помощью созданного нами класса CMarketBook можно относительно быстро создавать сложные эксперты и индикаторы, анализирующие текущую ликвидность по инструменту. Большое внимание при проектировании класса CMarketBook уделялось производительности, ведь стакан цен — очень динамичная таблица, меняющаяся сотни раз за минуту.

Класс, описанный в статье, станет хорошей основой для вашей скальперской или высокочастотной системы. Вы сможете дополнить его специфическим для вашей системы функционалом. Для этого просто создайте свой класс стакана, производный от CMarketBook, и напишите методы расширения, требующиеся вам. Однако мы надеемся, что даже те базовые свойства и методы, что предоставлены классом стакана, сделают вашу работу гораздо легче и надежнее.