
Разработка торговой системы на основе стакана цен (Часть I): индикатор
Введение
Давайте вспомним, что такое Depth of Market- это ряд отложенных лимитных ордеров. Эти ордера представляют собой торговые «намерения» участников рынка и часто не превращаются в настоящую сделку. Это связано с тем, что у участников есть возможность отменить ранее установленные ордеры по различным причинам. К ним относятся изменения рыночных условий и, как следствие, отсутствие заинтересованности в выполнении ордера в объеме и по цене, указанной выше.
Значение, возвращаемое функцией `SymbolInfoInteger(_Symbol, SYMBOL_TICKS_BOOKDEPTH)`, как раз и является глубиной стакана цен и представляет собой половину массива, который заполнится ценовыми уровнями, с которыми предстоит работать. Одна половина данного массива отводится под количество лимитных ордеров на продажу, а другая - под количество установленных лимитных ордеров на покупку. Согласно документации, для символов без строки заявок значение данного свойства равно нулю. Пример этого мы видим на рисунке ниже, где показан стакан цен, глубина которого равна 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 распространяется на все графические экземпляры пользовательского символа, к которым мы его привязали. Поэтому нет разницы, работают ли индикатор и генератор событий на разных графиках или на одном графике, если они оба ссылаются на один и тот же пользовательский символ.
Анимация далее иллюстрирует данную последовательность, а также работу индикатора. Надеюсь, вам понравится!
Заключение
Стакан цен Depth of Market, несомненно, является очень важным элементом для выполнения быстрых сделок, особенно в алгоритмах высокочастотного трейдинга (HFT). Данный тип событий можно получить от рынка через брокера по многим торгуемым символам. Со временем брокеры могут заинтересоваться в расширении охвата и доступности этой информации для других символов.
Кроме того, я считаю, что не стоит строить торговую систему, основываясь только на стакане цен, хотя она может быть полезна для выявления зон ликвидности и обнаружения определенной корреляции с движением цены. Поэтому я считаю, что разумно сочетать анализ стакана цен с другими инструментами и индикаторами, чтобы в совокупности они давали стабильные результаты.
Остаются открытыми и будущие усовершенствования индикатора, (такие как реализация способов хранения событий типа BookEvent), и последующее использование этих данных в бэктестах, как в ручном, так и в автоматическом режиме.
Перевод с португальского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/pt/articles/15748
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.





- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Это очень короткая статья с очень небольшим количеством кода. Посмотрим в следующей части, имеет ли смысл иметь часть 1, 2 и так далее.
SYMBOL_TICKS_BOOKDEPTH дает максимальное количество запросов, отображаемых в Depth of Market. Неверно, что это свойство дает тот же результат, что и подсчет количества уровней в DOM. Оно дает максимальное, а не точное число.
Вы можете сделать это с помощью данного скрипта:
Это очень короткая статья с очень небольшим количеством кода. Посмотрим в следующей части, имеет ли смысл иметь часть 1, 2 и так далее.
SYMBOL_TICKS_BOOKDEPTH дает максимальное количество запросов, отображаемых в Depth of Market. Неверно, что это свойство дает тот же результат, что и подсчет количества уровней в DOM. Оно дает максимальное, а не точное число.
Вы можете сделать это с помощью данного скрипта:
статья суппер! тоже хотел в планах по стакану написать ТС - точнее с использованием стакана котировок!