English 中文 Español Deutsch 日本語 Português
preview
Разработка торговой системы на основе стакана цен (Часть I): индикатор

Разработка торговой системы на основе стакана цен (Часть I): индикатор

MetaTrader 5Примеры |
806 4
Daniel Santos
Daniel Santos

Введение

Давайте вспомним, что такое Depth of Market- это ряд отложенных лимитных ордеров. Эти ордера представляют собой торговые «намерения» участников рынка и часто не превращаются в настоящую сделку. Это связано с тем, что у участников есть возможность отменить ранее установленные ордеры по различным причинам. К ним относятся изменения рыночных условий и, как следствие, отсутствие заинтересованности в выполнении ордера в объеме и по цене, указанной выше.

Значение, возвращаемое функцией `SymbolInfoInteger(_Symbol, SYMBOL_TICKS_BOOKDEPTH)`, как раз и является глубиной стакана цен и представляет собой половину массива, который заполнится ценовыми уровнями, с которыми предстоит работать. Одна половина данного массива отводится под количество лимитных ордеров на продажу, а другая - под количество установленных лимитных ордеров на покупку. Согласно документации, для символов без строки заявок значение данного свойства равно нулю. Пример этого мы видим на рисунке ниже, где показан стакан цен, глубина которого равна 10 и где видны все доступные ценовые уровни.

Пример: Стакан цен с глубиной 10

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

Возможно, вы зададитесь вопросом: зачем использовать этот индикатор, а не просто воспользоваться стандартным стаканом цен MetaTrader 5? Вот несколько причин:

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


Создание пользовательского символа

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

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

#define   CUSTOM_SYMBOL_NAME     Symbol()+".C"     
#define   CUSTOM_SYMBOL_PATH     "Forex"           
#define   CUSTOM_SYMBOL_ORIGIN   Symbol()          

#define   DATATICKS_TO_COPY      UINT_MAX 
#define   DAYS_TO_COPY           5
#include <Tools\DateTime.mqh>

В следующем фрагменте, вставленном в функцию OnStart() того же скрипта, мы видим, как создается объект даты "timemaster". Он используется для расчета периода времени, в котором будут собраны тики и бары для клонирования. Согласно определенной нами константе DAYS_TO_COPY, функция Bars будет копировать последние пять дней исходного символа. Это же время начального диапазона преобразуется в миллисекунды и используется в функции CopyTicks, завершая тем самым «клонирование» символа.

   CDateTime timemaster;
   datetime now = TimeTradeServer();
   timemaster.Date(now);
   timemaster.DayDec(DAYS_TO_COPY);
   long DaysAgoMsc = 1000 * timemaster.DateTime();
   int bars_origin = Bars(CUSTOM_SYMBOL_ORIGIN, PERIOD_M1, timemaster.DateTime(), now);
   int create = CreateCustomSymbol(CUSTOM_SYMBOL_NAME, CUSTOM_SYMBOL_PATH, CUSTOM_SYMBOL_ORIGIN);
   if(create != 0 && create != 5304)
      return;
   MqlTick array[] = {};
   MqlRates rates[] = {};
   int attempts = 0;
   while(attempts < 3)
     {
      int received = CopyTicks(CUSTOM_SYMBOL_ORIGIN, array, COPY_TICKS_ALL, DaysAgoMsc, DATATICKS_TO_COPY);
      if(received != -1)
        {
         if(GetLastError() == 0)
            break;
        }
      attempts++;
      Sleep(1000);
     }

По завершении процесса новый символ должен появиться в списке символов «Обзора рынка» с названием <AtivodeOrigem>.C. Затем мы должны открыть новый график с этим синтетическим символом и перейти к следующему этапу.

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


Скрипт генератора событий BookEvent для тестов

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

//+------------------------------------------------------------------+
//|                                            GenerateBookEvent.mq5 |
//|                                               Daniel Santos      |
//+------------------------------------------------------------------+
#property copyright "Daniel Santos"
#property version   "1.00"
#define SYNTH_SYMBOL_MARKET_DEPTH      32
#define SYNTH_SYMBOL_BOOK_ITERATIONS   20
#include <Random.mqh>
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
double BidValue, tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
MqlBookInfo    books[];
int marketDepth = SYNTH_SYMBOL_MARKET_DEPTH;
CRandom rdn;
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
   if(!SymbolInfoInteger(_Symbol, SYMBOL_CUSTOM)) // if the symbol exists
     {
      Print("Custom symbol ", _Symbol, " does not exist");
      return;
     }
   else
      BookGenerationLoop();
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void BookGenerationLoop()
  {
   MqlRates BarRates_D1[];
   CopyRates(_Symbol, PERIOD_D1, 0, 1, BarRates_D1);
   if(ArraySize(BarRates_D1) == 0)
      return;
   BidValue = BarRates_D1[0].close;
   ArrayResize(books, 2 * marketDepth);
   for(int j = 0; j < SYNTH_SYMBOL_BOOK_ITERATIONS; j++)
     {
      for(int i = 0, j = 0; i < marketDepth; i++)
        {
         books[i].type = BOOK_TYPE_SELL;
         books[i].price = BidValue + ((marketDepth - i) * tickSize);
         books[i].volume_real = rdn.RandomInteger(10, 500);
         books[i].volume_real = round((books[i].volume_real + books[j].volume_real) / 2);
         books[i].volume = (int)books[i].volume_real;
         //----
         books[marketDepth + i].type = BOOK_TYPE_BUY;
         books[marketDepth + i].price = BidValue - (i * tickSize);
         books[marketDepth + i].volume_real = rdn.RandomInteger(10, 500);
         books[marketDepth + i].volume_real = round((books[marketDepth + i].volume_real
                                              + books[marketDepth + j].volume_real) / 2);
         books[marketDepth + i].volume = (int)books[marketDepth + i].volume_real;
         if(j != i)
            j++;
        }
      CustomBookAdd(_Symbol, books);
      Sleep(rdn.RandomInteger(400, 1000));
     }
  }
//+------------------------------------------------------------------+

Вместо стандартной функции MathRand() использовали другую реализацию 32-битных случайных чисел. На это есть несколько причин, в том числе простота генерации целочисленных чисел в заданном диапазоне - преимущество, которым мы воспользовались в этом скрипте, используя функцию RandomInteger(min, max).

Для глубины стакана цен выбрали относительно большое значение - 32, то есть в каждой итерации будет сгенерировано 64 ценовых уровня. При желании можно установить значение поменьше.

Алгоритм проверяет, является ли символ пользовательским. Если да, то он переходит к генерации каждого из элементов стакана цен и повторяет это в другом цикле в соответствии с заданным числом итераций. В этой реализации выполняется 20 повторений с паузами между ними произвольно выбранной длительности в диапазоне от 400 миллисекунд до 1000 миллисекунд (эквивалентно 1 секунде). Такая динамика делает визуализацию тиков более реалистичной и приятной.

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

Разница в цене между одним уровнем и следующим устанавливается в соответствии с размером тика символа, получаемым через свойство SYMBOL_TRADE_TICK_SIZE.


Индикатор для отображения изменений Market Depth

Исходный код библиотеки

Индикатор был разработан с использованием объектно-ориентированного программирования. Класс BookEventHistogram был создан для управления гистограммой стакана цен, начиная с его создания, обновления и заканчивая удалением баров при уничтожении объекта этого класса.

Ниже приведены объявления переменных и функций класса BookEventHistogram:

class BookEventHistogram
  {
protected:
   color                histogramColors[]; //Extreme / Mid-high / Mid-low
   int                  bookSize;
   int                  currElements;
   int                  elementMaxPixelsWidth;
   bool                 showMessages;
   ENUM_ALIGN_MODE      corner;
   string               bookEventElementPrefix;
public:
   MqlBookInfo          lastBook[];
   datetime             lastDate;
   void                 SetAlignLeft(void);
   void                 SetCustomHistogramColors(color &colors[]);
   void                 SetBookSize(int value) {bookSize = value;}
   void                 SetElementMaxPixelsWidth(int m);
   int                  GetBookSize(void) {return bookSize;}
   void                 DrawBookElements(MqlBookInfo& book[], datetime now);
   void                 CleanBookElements(void);
   void                 CreateBookElements(MqlBookInfo& book[], datetime now);
   void                 CreateOrRefreshElement(int buttonHeigh, int buttonWidth, int i, color clr, int ydistance);
   //--- Default constructor
                     BookEventHistogram(void);
                    ~BookEventHistogram(void);
  };

Обратите внимание, что не все функции определены в данном сегменте, а дополнены в других строках файла BookEventHistogram.mqh.

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


Исходный код индикатора:

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

Каждый из графиков имеет два цвета: один для ордеров на покупку и другой для ордеров на продажу. Данный набор из шести цветов используется для выбора цвета каждого сегмента в соответствии с заданными критериями. Невооруженным взглядом, большие сегменты гистограммы классифицируются как "предельные", сегменты размером выше среднего - как "mid-high", а остальные - как "mid-low".

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

#define NUMBER_OF_PLOTS 3
#property indicator_chart_window
#property indicator_buffers NUMBER_OF_PLOTS
#property indicator_plots   NUMBER_OF_PLOTS
//--- Invisible plots
#property indicator_label1  "Extreme volume elements colors"
#property indicator_type1   DRAW_NONE
#property indicator_color1  C'212,135,114', C'155,208,226'
//---
#property indicator_label2  "Mid-high volume elements colors"
#property indicator_type2   DRAW_NONE
#property indicator_color2  C'217,111,86', C'124,195,216'
//---
#property indicator_label3  "Mid-low volume elements color"
#property indicator_type3   DRAW_NONE
#property indicator_color3  C'208,101,74', C'114,190,214'
#include "BookEventHistogram.mqh"
enum HistogramPosition
  {
   LEFT,      //<<<< Histogram on the left
   RIGHT,     //Histogram on the right >>>>
  };
enum HistogramProportion
  {
   A_QUARTER,   // A quarter of the chart
   A_THIRD,     // A third of the chart
   HALF,        // Half of the chart
  };
input  HistogramPosition position = RIGHT; // Indicator position
input  HistogramProportion proportion = A_QUARTER; // Histogram ratio (compared to chart width)
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
double volumes[];
color histogramColors[];
BookEventHistogram bookEvent;

Затем определяются два перечислителя для предоставления пользователю определенных опций во время загрузки индикатора.  Задача состоит в том, чтобы определить, где должны построить гистограмму, справа или слева от графика. Пользователю также предлагается определить соотношение, которое будет занимать гистограмма по отношению к ширине графика: четверть, треть или половину. Например, если ширина графика составляет 500 пикселей, а соотношение определено как половина, размер столбцов гистограммы может колебаться от 0 до 250 пикселей.

Наконец, в исходном коде BookEvents.mq5 отметим, что функции OnBookEvent и OnChartEvent вызывают почти все запросы на обновление гистограммы. OnCalculate, с другой стороны, не участвует в алгоритме, а сохраняется только из соображений синтаксиса MQL.


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

Правильная последовательность включения скриптов и индикатора следующая (в соответствии с разработанными на данный момент функциональными возможностями):

  • Скрипт CloneSymbolTicksAndRates (на графике реального символа, который будем клонировать)
  • -> Индикатор BookEvents (график сгенерированного пользовательского символа)
  • -> Скрипт GenerateBookEvent (график сгенерированного пользовательского символа)

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

Анимация далее иллюстрирует данную последовательность, а также работу индикатора. Надеюсь, вам понравится!

Демо - индикатор BookEvents


Заключение

Стакан цен Depth of Market, несомненно, является очень важным элементом для выполнения быстрых сделок, особенно в алгоритмах высокочастотного трейдинга (HFT). Данный тип событий можно получить от рынка через брокера по многим торгуемым символам. Со временем брокеры могут заинтересоваться в расширении охвата и доступности этой информации для других символов.

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

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


Перевод с португальского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/pt/articles/15748

Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (4)
Stanislav Korotky
Stanislav Korotky | 11 апр. 2025 в 13:33
Я не вижу причин, по которым вам нужен пользовательский символ. Вполне можно сохранять и воспроизводить события книги на самом стандартном символе - как на истории обычного графика (для отображения индикатора), так и в тестере (для тестирования советника).
Samuel Manoel De Souza
Samuel Manoel De Souza | 11 апр. 2025 в 14:57

Это очень короткая статья с очень небольшим количеством кода. Посмотрим в следующей части, имеет ли смысл иметь часть 1, 2 и так далее.

SYMBOL_TICKS_BOOKDEPTH дает максимальное количество запросов, отображаемых в Depth of Market. Неверно, что это свойство дает тот же результат, что и подсчет количества уровней в DOM. Оно дает максимальное, а не точное число.

Вы можете сделать это с помощью данного скрипта:

//+------------------------------------------------------------------+
//|TestOnderBook.mq5 |
//|Copyright 2025, Samuel Manoel De Souza |
//| https://www.mql5.com/en/users/samuelmnl |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, Samuel Manoel De Souza"
#property link      "https://www.mql5.com/en/users/samuelmnl"
#property version   "1.00"
//+------------------------------------------------------------------+
//| Функция запуска программы сценария|
//+------------------------------------------------------------------+
int OnInit(void)
  {
   MarketBookAdd(_Symbol);

   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//||
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {

   MarketBookRelease(_Symbol);
  }
//+------------------------------------------------------------------+
//||
//+------------------------------------------------------------------+
void OnTick(void)
  {

  }
//+------------------------------------------------------------------+
//||
//+------------------------------------------------------------------+
void OnBookEvent(const string& symbol)
  {
   double tick_size = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
   MqlBookInfo book[];
   MarketBookGet(_Symbol, book);
   int total = ArraySize(book);
   if(total == 0)
     {
      Print("there is no order available on the book");
      ExpertRemove();
      return;
     }

   int buy_levels = 0, sell_levels = 0;
   int buy_gaps = 0, sell_gaps = 0, gaps = 0;
   for(int i = 0; i < total; i++)
     {
      Print("price: ", book[i].price, ", volume: ", book[i].volume, ", type: ", EnumToString(book[i].type));
      buy_levels += book[i].type == BOOK_TYPE_BUY ? 1 : 0;
      sell_levels += book[i].type == BOOK_TYPE_SELL ? 1 : 0;
      if(i > 0)
        {
         bool is_gap = fabs(book[i].price - book[i - 1].price) >= 2 * tick_size;
         gaps += is_gap ? 1 : 0;
         buy_gaps += is_gap && book[i].type == book[i - 1].type && book[i].type == BOOK_TYPE_BUY ? 1 : 0;
         sell_gaps += is_gap && book[i].type == book[i - 1].type && book[i].type == BOOK_TYPE_SELL ? 1 : 0;
        }
     }

   Print("max levels: ", SymbolInfoInteger(_Symbol, SYMBOL_TICKS_BOOKDEPTH));
   Print("levels: ", total);
   Print("buy levels: ", buy_levels);
   Print("sell levels: ", sell_levels);
   Print("gaps: ", gaps);
   Print("buy gaps: ", buy_gaps);
   Print("sell gap: ", sell_gaps);
   ExpertRemove();
  }
//+------------------------------------------------------------------+
Thomas Gardling
Thomas Gardling | 18 мая 2025 в 13:26
Что случилось с торговой системой, которая должна была быть разработана на основе этого индикатора?
Roman Shiredchenko
Roman Shiredchenko | 19 мая 2025 в 08:11
Samuel Manoel De Souza #:

Это очень короткая статья с очень небольшим количеством кода. Посмотрим в следующей части, имеет ли смысл иметь часть 1, 2 и так далее.

SYMBOL_TICKS_BOOKDEPTH дает максимальное количество запросов, отображаемых в Depth of Market. Неверно, что это свойство дает тот же результат, что и подсчет количества уровней в DOM. Оно дает максимальное, а не точное число.

Вы можете сделать это с помощью данного скрипта:

статья суппер! тоже хотел в планах по стакану написать ТС - точнее с использованием стакана котировок!

Индикатор прогноза волатильности при помощи Python Индикатор прогноза волатильности при помощи Python
Прогнозируем будущую экстремальную волатильность при помощи бинарной классификации. Создаем индикатор прогноза экстремальной волатильности с использованием машинного обучения.
Разработка системы репликации (Часть 63): Нажатие кнопки воспроизведения в сервисе (IV) Разработка системы репликации (Часть 63): Нажатие кнопки воспроизведения в сервисе (IV)
В этой статье мы наконец решим проблемы моделирования тиков на одноминутном баре, чтобы те могли сосуществовать с реальными тиками. Таким образом, мы избежим возникновения проблем в будущем. Представленный здесь контент предназначен только для образовательных целей. Ни в коем случае его не следует рассматривать как приложение, предназначенное для чего-то иного, кроме изучения и освоения представленных концепций.
Разработка системы репликации (Часть 64): Нажатие кнопки воспроизведения в сервисе (V) Разработка системы репликации (Часть 64): Нажатие кнопки воспроизведения в сервисе (V)
В данной статье мы рассмотрим, как исправить две ошибки в коде. Однако я постараюсь объяснить их так, чтобы вы, начинающие программисты, поняли, что не всегда всё происходит так, как вы предполагали. Но это не повод отчаиваться, это возможность учиться. Представленные здесь материалы предназначены только для обучения. Ни в коем случае не рассматривайте это приложение как окончательное, цели которого иные, кроме изучения представленных концепций.
Возможности Мастера MQL5, которые вам нужно знать (Часть 30): Пакетная нормализация в машинном обучении Возможности Мастера MQL5, которые вам нужно знать (Часть 30): Пакетная нормализация в машинном обучении
Пакетная нормализация — это предварительная обработка данных перед их передачей в алгоритм машинного обучения, например, в нейронную сеть. При этом всегда следует учитывать тип активации, который будет использоваться алгоритмом. Мы рассмотрим различные подходы, которые можно использовать для извлечения выгоды с помощью советника, собранного в Мастере.