
Разработка советника на основе стратегии прорыва диапазона консолидации на MQL5
Введение
В этой статье мы рассмотрим разработку советника на основе стратегии прорыва диапазона консолидации (Consolidation Range Breakout) на MetaQuotes Language 5 (MQL5) - языке программирования для MetaTrader 5. В быстро меняющемся мире финансовой торговли стратегии, которые извлекают выгоду из рыночных моделей и поведения, имеют решающее значение для успеха, и одной из таких стратегий является прорыв диапазона консолидации. Стратегия фокусируется на определении периодов консолидации рынка и торговле на последующих прорывах. Эта стратегия особенно эффективна для захвата значительных ценовых движений, следующих за периодом низкой волатильности. В статье будут рассмотрены следующие темы:
- Обзор стратегии
- План стратегии
- Реализация в MetaQuotes Language 5 (MQL5)
- Результаты тестирования на истории
- Заключение
К концу этой статьи вы получите полное представление о том, как разработать и внедрить надежный советник на основе стратегии прорыва диапазона консолидации в MQL5, расширив таким образом ваш торговый инструментарий. Мы будем активно использовать язык MetaQuotes Language 5 (MQL5) в качестве нашей базовой среды разработки (IDE) и запускать файлы в торговом терминале MetaTrader 5. Итак, начнем.
Обзор стратегии
Чтобы легче понять стратегию прорыва диапазона консолидации, давайте разделим ее на части.
- Определение диапазона консолидации
Диапазон консолидации, также известный как торговый диапазон, представляет собой период, в течение которого цена финансового инструмента колеблется горизонтально в пределах определенного диапазона, не демонстрируя сильных восходящих или нисходящих движений. Этот период характеризуется низкой волатильностью. Цена колеблется между четко определенным уровнем поддержки (нижняя граница) и уровнем сопротивления (верхняя граница). Трейдеры часто используют эту фазу для прогнозирования потенциальных точек прорыва, когда цена может резко пойти в определенном направлении после окончания периода консолидации.
- Как работает стратегия
Стратегия прорыва диапазона консолидации использует поведение цен в периоды консолидации для выявления прорывов и торговли на них. Вот как это работает:
Определение диапазона консолидации: Первый шаг — выявить диапазон консолидации, изучив недавние движения цен. Это подразумевает определение самых высоких и самых низких цен за определенное количество баров (свечей) для определения верхней и нижней границ диапазона, которые обычно выступают в качестве уровней сопротивления и поддержки диапазона. Выбор таймфрейма здесь не статичен, поскольку для процесса идентификации может использоваться любой график, поэтому вам необходимо выбрать таймфрейм графика, который подходит вашему стилю торговли.
Мониторинг прорывов: После установления диапазона консолидации стратегия отслеживает ценовые движения, которые нарушают верхнюю или нижнюю границы диапазона. Прорыв происходит, когда цена закрывается выше уровня сопротивления или ниже уровня поддержки. Другие трейдеры рассматривают возможность скальпинга на той же свече, которая пробивает диапазон, то есть, как только цена падает ниже или поднимается выше диапазона, они уже рассматривают это событие как прорыв.
Торговля на прорыве: При обнаружении прорыва стратегия инициирует сделку в его направлении. Если цена преодолевает уровень сопротивления, выставляется ордер на покупку. И наоборот, если цена опускается ниже уровня поддержки, выставляется ордер на продажу. Опять же, некоторые трейдеры ждут коррекции, то есть после прорыва, для дальнейшего подтверждения они ждут, когда цена снова войдет в диапазон, и как только она снова выйдет из него в том же направлении, они входят на рынок. Для использования мы не будем рассматривать вариант отката.
- Реализация стратегии
Реализация стратегии прорыва диапазона консолидации будет включать несколько этапов:
Определение параметров диапазона: Определяем количество баров для анализа с целью определения диапазона консолидации и устанавливаем критерии того, что считается прорывом. Определяем и устанавливаем диапазон баров и целевой диапазон цен. Например, чтобы диапазон консолидации был действительным, требуется не менее 10 баров в ценовом диапазоне 700 пунктов или 70 пипсов.
Разработка логики обнаружения: Напишем код для сканирования исторических данных о ценах, определения самого высокого максимума и самого низкого минимума в указанном диапазоне и нанесения этих уровней на график. В коде должны быть четко указаны условия, которые необходимо выполнить для того, чтобы диапазон консолидации считался действительным, а сделанные допущения должны быть четко изложены, чтобы избежать двусмысленности.
Мониторинг данных о ценах в реальном времени: Постоянно отслеживаем входящие ценовые данные, чтобы как можно скорее обнаруживать прорывы. Мониторинг необходимо проводить на каждом тике, а если в этом нет необходимости, то на каждом новом поколении свечей.
Совершение сделок: Реализуем логику исполнения сделок для размещения ордеров на покупку или продажу при обнаружении пробоя, включая установку соответствующих уровней стоп-лосса и тейк-профита для управления рисками.
Оптимизация и тестирование: Протестируем стратегию с использованием исторических данных, чтобы оптимизировать параметры и убедиться в ее эффективности перед внедрением в реальную торговлю. Это поможет нам определить наилучшие параметры и выявить ключевые характеристики, которые необходимо улучшить или отфильтровать для улучшения и совершенствования системы.
Выполнив эти шаги, мы сможем создать мощный инструмент на основе стратегии прорыва диапазона консолидации.
План стратегии
Чтобы легче понять изложенную идею, визуализируем ее в виде схемы.
- Прорыв верхнего уровня диапазона консолидации:
- Прорыв нижнего уровня диапазона консолидации:
Реализация в MetaQuotes Language 5 (MQL5)
С теорией покончено. Давайте создадим советника на MQL5 для MetaTrader 5.
В терминале MetaTrader 5 выберите "Сервис" > "Редактор MetaQuotes Language" или просто нажмите F4. Кроме того, вы можете щелкнуть иконку IDE (интегрированная среда разработки) на панели инструментов. Откроется среда разработки на MQL5, которая позволяет писать торговых роботов, технические индикаторы, скрипты и библиотеки функций.
На панели инструментов выберите "Файл" - "Новый файл" или нажмите CTRL + N, чтобы создать новый документ. Также вы можете нажать на иконку "Создать" в панели инструментов. Откроется окно Мастера MQL.
В открывшемся Мастере выберите Советник (шаблон) и нажмите Далее.
В общих свойствах укажите имя файла вашего советника. Чтобы указать или создать папку, если она не существует, используйте обратную косую черту перед именем советника. По умолчанию указана папка Experts\. Это значит, что наш советник будет создан в папке Experts. Остальные разделы довольно просты, но вы можете перейти по ссылке в нижней части Мастера, чтобы узнать детали.
После указания имени файла советника нажмите "Далее" > "Далее" > "Готово". Мы готовы к воплощению стратегии в коде.
Прежде всего используем #include в начале исходного кода. Он даст нам доступ к классу CTrade, который мы будем использовать для создания торгового объекта. Доступ нужен для открытия сделок.
#include <Trade/Trade.mqh> // Include the trade library CTrade obj_Trade; // Create an instance of the CTrade class
Препроцессор заменит строку #include <Trade/Trade.mqh> содержимым файла Trade.mqh. Угловые скобки указывают, что файл Trade.mqh будет взят из стандартного каталога (обычно это каталог_установки_терминала\MQL5\Include). Текущий каталог не включен в поиск. Строку можно разместить в любом месте программы, но обычно все включения размещаются в начале исходного кода для лучшей структуры кода и удобства ссылок. Благодаря разработчикам MQL5 объявление объекта obj_Trade класса CTrade предоставит нам легкий доступ к методам, содержащимся в этом классе.
Поскольку нам нужно будет графически изобразить диапазон на графике, нам понадобится его название. Мы будем использовать тот же прямоугольный объект диапазона. Таким образом, для визуализации будет использоваться один объект. После построения графика мы просто обновим его настройки, не перерисовывая его заново. Чтобы получить статическое имя, которое можно было бы легко вспомнить и использовать повторно, мы определяем его следующим образом.
#define rangeNAME "CONSOLIDATION RANGE" // Define the name of the consolidation range
Мы используем ключевое слово #define для определения макроса rangeNAME со значением CONSOLIDATION RANGE для простого сохранения имени диапазона консолидации, вместо того, чтобы повторно вводить имя при каждом создании уровня, что значительно экономит наше время и снижает вероятность неправильного указания имени. По сути, макросы используются для подстановки текста во время компиляции.
Опять же, нам нужно будет сохранить координаты построенного прямоугольника. Это двухмерные (2D) ординаты формата (x, y), используемые для явного определения первого и второго местоположений, задокументированных как x1,y1 и x2,y2 соответственно. На ценовом графике ось X представлена шкалой даты и времени, а ось Y — шкалой цен. Для более легкого понимания и ссылок приведем наглядную иллюстрацию.
Теперь понятно, почему нам нужны координаты для построения прямоугольного объекта. Ниже приведена логика, которая используется для обеспечения сохранения координат диапазона без необходимости объявлять их каждый раз при обновлении координат.
datetime TIME1_X1, TIME2_Y2; // Declare datetime variables to hold range start and end times double PRICE1_Y1, PRICE2_Y2; // Declare double variables to hold range high and low prices
Здесь мы объявляем две переменные datetime. Метод, который мы здесь используем, называется декларацией одного типа данных (single data-type declaration), при котором объявляется несколько переменных одного и того же типа данных. Это сокращенный способ объявления нескольких переменных одного типа в одном выражении. Это полезно, поскольку обеспечивает краткость, поскольку сокращает количество строк кода и группирует связанные переменные вместе, что упрощает понимание того, что они относятся к одному типу и потенциально используются для схожих целей, при этом сохраняя согласованность. Мы также можем записать их следующим образом:
datetime TIME1_X1; datetime TIME2_Y2;
Переменная TIME1_X1 хранит значение времени первой координаты вдоль оси x, а TIME2_Y2 хранит значение времени второй координаты по-прежнему вдоль оси x. Аналогично мы объявляем координаты цены следующим образом:
double PRICE1_Y1, PRICE2_Y2; // Declare double variables to hold range high and low prices
Нам необходимо будет постоянно сканировать рынок на каждом новом баре, чтобы оценить установление диапазона консолидации, вызванного низкой волатильностью. Таким образом, для хранения флагов обозначающих, что диапазон существует и цена находится в пределах диапазона, понадобятся две переменные.
bool isRangeExist = false; // Flag to check if the range exists bool isInRange = false; // Flag to check if we are currently within the range
Здесь мы определяем и инициализируем две булевы переменные isRangeExist и isInRange. Переменная isRangeExist будет служить флагом, указывающим, был ли идентифицирован и нанесен на график диапазон консолидации. Мы инициализируем ее как false, поскольку изначально диапазон не был установлен. Опять же, переменная isInRange, также инициализированная значением false, используется для определения того, находится ли текущая рыночная цена в пределах выявленного диапазона консолидации. Эти флаги имеют решающее значение для логики работы советника, поскольку они помогают управлять состоянием процесса обнаружения диапазона и мониторинга прорывов, гарантируя, что действия будут предприняты только при соблюдении соответствующих условий.
На глобальном масштабе нам по-прежнему необходимо будет определить минимальное количество рассматриваемых свечей, а также размер диапазона в пунктах. Как я уже отмечал, эти параметры имеют решающее значение для поддержания обоснованности диапазона консолидации и обеспечения того, чтобы у нас были значимые диапазоны консолидации.
int rangeBars = 10; // Number of bars to consider for the range int rangeSizePoints = 400; // Maximum range size in points
Мы снова объявляем и инициализируем две целевые переменные - rangeBars и rangeSizePoints. Переменная rangeBars установлена на 10, чтобы указать количество баров (или свечей), которые мы будем анализировать для определения диапазона консолидации. Это означает, что мы просматриваем последние 10 баров, чтобы найти самый высокий максимум и самый низкий минимум и определить наш диапазон. Переменная rangeSizePoints имеет значение 400, что определяет максимально допустимый размер диапазона консолидации в пунктах. Если диапазон между самой высокой и самой низкой ценой в пределах этих 10 баров превышает 400 пунктов, это не считается допустимым диапазоном консолидации. Эти параметры необходимы для установления критериев диапазона и обеспечения выявления значимых периодов консолидации в ценовых данных.
Наконец, поскольку мы будем открывать позиции, определим точки стоп-лосса и тейк-профита.
double sl_points = 500.0; // Stop loss points double tp_points = 500.0; // Take profit points
Это все, что нам нужно в глобальной области видимости. Возможно, вам интересно, что такое глобальная область видимости. Это область программы, где переменные, функции и другие элементы доступны по всему коду, за пределами каких-либо функций или блоков. Когда переменная или функция объявлена в глобальной области видимости, любая часть программы может получить к ней доступ и изменить ее.
Вся работа будет проводиться в обработчике событий OnTick. Это будет чистое ценовое действие, и мы будем в значительной степени полагаться на этот обработчик событий. Итак, давайте рассмотрим параметры, которые принимает функция, поскольку это центральная часть всего кода.
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick(){ //--- }
Как уже было видно, это простая, но важная функция, которая не принимает никаких аргументов и ничего не возвращает. Это просто функция void, то есть она не должна ничего возвращать. Функция используется в советниках и выполняется при появлении нового тика, то есть изменении котировок на конкретный товар.
Теперь, когда мы увидели, что функция OnTick генерируется при каждом изменении котировок цен, нам нужно определить некоторую логику управления, которая позволит нам запускать код, который будет выполняться один раз за бар, а не на каждом тике, по крайней мере, чтобы избежать ненужных запусков кода, тем самым экономя память устройства. Это будет необходимо при поиске настроек диапазона консолидации. Нам не нужно искать настройки на каждом тике, но мы всегда будем получать одни и те же результаты, при условии, что мы находимся на той же свече. Логика представлена ниже:
int currBars = iBars(_Symbol,_Period); // Get the current number of bars static int prevBars = currBars; // Static variable to store the previous number of bars static bool isNewBar = false; // Static flag to check if a new bar has appeared if (prevBars == currBars){isNewBar = false;} // Check if the number of bars has not changed else {isNewBar = true; prevBars = currBars;} // If the number of bars has changed, set isNewBar to true and update prevBars
Сначала мы объявляем целочисленную переменную currBars, которая хранит рассчитанное количество текущих баров на графике для указанного торгового символа и таймфрейма. Это делается с помощью функции iBars, которая принимает всего два аргумента: symbol и period. Во-вторых, мы объявляем статическую целочисленную переменную prevBars и инициализируем ее текущим количеством баров. Ключевое слово static гарантирует, что переменная prevBars сохраняет свое значение между вызовами функций, эффективно запоминая количество баров с предыдущего тика. В-третьих, мы заявляем статическую булеву переменную isNewBar и также инициализируем ее значением false. Эта переменная поможет нам отслеживать, появился ли новый бар. Далее мы используем условный оператор, чтобы проверить, равно ли текущее количество баров предыдущему. Если они равны, это означает, что новый бар не сформировался, и поэтому мы устанавливаем флаг для генерации нового бара на false. В противном случае, если предыдущие бары не равны текущим барам, это означает, что количество баров увеличилось, что указывает на появление нового бара. Таким образом, мы установили флаг для генерации нового бара на значение true и обновили значение предыдущих баров до текущих баров.
Теперь для каждого сгенерированного бара, у которого нет диапазона консолидации, нам нужно просканировать предопределенные бары на предмет потенциального периода низкой волатильности.
if (isRangeExist == false && isNewBar){ // If no range exists and a new bar has appeared ... }
Мы проверяем, имеет ли isRangeExist значение false (диапазон еще не установлен), а isNewBar - значение true (появился новый бар). Это гарантирует, что мы продолжим работу только в том случае, если диапазон консолидации еще не определен и сформировался новый бар.
Для определения координат первой точки нашего прямоугольного объекта, которая будет нанесена на график, нам понадобятся экстремальные уровни сопротивления, то есть время последнего бара в нашем предопределенном диапазоне сканирования баров и цена самого высокого бара в пределах диапазона.
TIME1_X1 = iTime(_Symbol,_Period,rangeBars); // Get the start time of the range int highestHigh_BarIndex = iHighest(_Symbol,_Period,MODE_HIGH,rangeBars,1); // Get the bar index with the highest high in the range PRICE1_Y1 = iHigh(_Symbol,_Period,highestHigh_BarIndex); // Get the highest high price in the range
Сначала мы устанавливаем время начала диапазона с помощью функции iTime, которая возвращает время открытия определенного бара для указанного символа и периода. Функция принимает три входных параметра или аргумента, где _Symbol - торговый символ (например, "AUDUSD"), _Period - таймфрейм (например, PERIOD_M1 означает минутные бары), а rangeBars - индекс бара, который находится на указанное количество периодов назад. Результат сохраняется в TIME1_X1, отмечая время начала диапазона консолидации.
Далее мы находим бар с самым высоким максимумом в указанном диапазоне, используя функцию iHighest, которая возвращает индекс бара с самой высокой ценой за указанное количество баров. Функция принимает пять аргументов. Думаю, мне не нужно снова объяснять, что делают первые два параметра. Третий параметр (MODE_HIGH) используется для указания того, что мы ищем самую высокую цену. Четвертый параметр, rangeBars, указывает количество баров, которые следует учитывать при анализе сканирования, и, наконец, 1 означает, что мы начинаем просмотр с бара, предшествующего текущему бару, который формируется. Технически, формирующийся в данный момент бар имеет индекс 0, а предшествующий ему — индекс 1. Полученный индекс сохраняется в целочисленной переменной highestHigh_BarIndex.
Наконец, мы извлекаем самую высокую цену из этого бара, используя функцию iHigh, которая возвращает максимальную цену определенного бара. Функция принимает три входных параметра, первые два из которых довольно просты. Третий аргумент (highestHigh_BarIndex) — это индекс бара, определенный на предыдущем шаге. Высокая цена сохраняется в PRICE1_Y1. Эти переменные позволяют нам определить начальную точку и наивысшую точку диапазона консолидации, которые имеют решающее значение для построения графика диапазона и последующего обнаружения прорывов.
Для получения вторых координат используется подход, аналогичный тому, который использовался для определения координат первой.
TIME2_Y2 = iTime(_Symbol,_Period,0); // Get the current time int lowestLow_BarIndex = iLowest(_Symbol,_Period,MODE_LOW,rangeBars,1); // Get the bar index with the lowest low in the range PRICE2_Y2 = iLow(_Symbol,_Period,lowestLow_BarIndex); // Get the lowest low price in the range
Разница в коде заключается в том, что, во-первых, наше время привязано к текущему бару, который имеет индекс 0. Во-вторых, чтобы получить индекс самого нижнего бара в пределах предопределенного диапазона, мы используем функцию iLowest, а также MODE_LOW, чтобы указать, что мы ищем самую низкую цену. Наконец, функция iLow используется для получения цены самого низкого бара. Ниже представлена визуализация требуемых координат, если бы мы взяли произвольный график.
Теперь, когда у нас есть точки диапазона консолидации, нам необходимо проверить их на действительность, чтобы убедиться в соблюдении условий действительного диапазона, как было указано ранее во вводной части, прежде чем они будут рассмотрены и нанесены на график.
isInRange = (PRICE1_Y1 - PRICE2_Y2)/_Point <= rangeSizePoints; // Check if the range size is within the allowed points
Вычисляем разницу между самой высокой (PRICE1_Y1) и самой низкой ценой (PRICE2_Y2) в пределах диапазона, а затем преобразуем разницу в пункты, разделив ее на размер пункта, _Point. Например, мы могли бы иметь 0,66777 как наивысшее значение цены и 0,66773 как наименьшее. Их математическая разность составляет 0,66777 - 0,66773 = 0,00004. Значение пункта предполагаемого символа составит 0,00001. Теперь разделив результат на количество баллов, получим 0,00004/0,00001 = 4 пункта. Затем это значение сравнивается с rangeSizePoints, который представляет собой максимально допустимый размер диапазона, определенный в пунктах.
Наконец, мы проверяем, был ли идентифицирован допустимый диапазон, и если да, то наносим его на график и сообщаем об успешном создании.
if (isInRange){ // If the range size is valid plotConsolidationRange(rangeNAME,TIME1_X1,PRICE1_Y1,TIME2_Y2,PRICE2_Y2); // Plot the consolidation range isRangeExist = true; // Set the range exist flag to true Print("RANGE PLOTTED"); // Print a message indicating the range is plotted }
Здесь мы проверяем, является ли выявленный диапазон консолидации допустимым, оценивая переменную isInRange. Если флаг переменной равен true, что указывает на то, что размер диапазона находится в допустимых пределах, мы приступаем к построению графика диапазона консолидации. Для построения графика мы вызываем функцию plotConsolidationRange с входными параметрами rangeNAME, TIME1_X1, PRICE1_Y1, TIME2_Y2 и PRICE2_Y2, которая создает визуальное представление диапазона. После успешного построения графика диапазона мы устанавливаем флаг isRangeExist в значение true, чтобы указать, что допустимый диапазон был определен и построен. Кроме того, мы выводим сообщение "RANGE PLOTTED" (диапазон построен) в терминал, подтверждая, что диапазон консолидации успешно визуализирован.
Фрагмент кода для функции, отвечающей за построение или обновление диапазона консолидации, выглядит следующим образом:
//+------------------------------------------------------------------+ //| Function to plot the consolidation range | //| rangeName - name of the range object | //| time1_x1 - start time of the range | //| price1_y1 - high price of the range | //| time2_x2 - end time of the range | //| price2_y2 - low price of the range | //+------------------------------------------------------------------+ void plotConsolidationRange(string rangeName,datetime time1_x1,double price1_y1, datetime time2_x2,double price2_y2){ if (ObjectFind(0,rangeName) < 0){ // If the range object does not exist ObjectCreate(0,rangeName,OBJ_RECTANGLE,0,time1_x1,price1_y1,time2_x2,price2_y2); // Create the range object ObjectSetInteger(0,rangeName,OBJPROP_COLOR,clrBlue); // Set the color of the range ObjectSetInteger(0,rangeName,OBJPROP_FILL,true); // Enable fill for the range ObjectSetInteger(0,rangeName,OBJPROP_WIDTH,5); // Set the width of the range } else { // If the range object exists ObjectSetInteger(0,rangeName,OBJPROP_TIME,0,time1_x1); // Update the start time of the range ObjectSetDouble(0,rangeName,OBJPROP_PRICE,0,price1_y1); // Update the high price of the range ObjectSetInteger(0,rangeName,OBJPROP_TIME,1,time2_x2); // Update the end time of the range ObjectSetDouble(0,rangeName,OBJPROP_PRICE,1,price2_y2); // Update the low price of the range } ChartRedraw(0); // Redraw the chart to reflect changes }
Сначала мы определяем функцию void под названием plotConsolidationRange и передаем ей 5 параметров или аргументов, а именно - имя диапазона, 2 координаты первой точки и 2 координаты второй точки. Затем мы используем условный оператор для проверки существования объекта с помощью функции ObjectFind, которая возвращает отрицательное целое число в случае, если объект не найден. Если это так, то мы приступаем к созданию объекта, идентифицированного как OBJ_RECTANGLE, для текущего времени и указанных цен первой и второй координат. Затем мы задаем его цвет, заливку области и ширину. Если объект найден, мы просто обновляем его время и цены до указанных значений и перерисовываем график для применения текущих изменений. Значение модификатора 0 используется для указания первой координаты, а 1 — второй.
Это все, что нам нужно, чтобы отобразить выявленный диапазон консолидации на графике. Полный исходный код, отвечающий за это, выглядит следующим образом:
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick(){ //--- int currBars = iBars(_Symbol,_Period); // Get the current number of bars static int prevBars = currBars; // Static variable to store the previous number of bars static bool isNewBar = false; // Static flag to check if a new bar has appeared if (prevBars == currBars){isNewBar = false;} // Check if the number of bars has not changed else {isNewBar = true; prevBars = currBars;} // If the number of bars has changed, set isNewBar to true and update prevBars if (isRangeExist == false && isNewBar){ // If no range exists and a new bar has appeared TIME1_X1 = iTime(_Symbol,_Period,rangeBars); // Get the start time of the range int highestHigh_BarIndex = iHighest(_Symbol,_Period,MODE_HIGH,rangeBars,1); // Get the bar index with the highest high in the range PRICE1_Y1 = iHigh(_Symbol,_Period,highestHigh_BarIndex); // Get the highest high price in the range TIME2_Y2 = iTime(_Symbol,_Period,0); // Get the current time int lowestLow_BarIndex = iLowest(_Symbol,_Period,MODE_LOW,rangeBars,1); // Get the bar index with the lowest low in the range PRICE2_Y2 = iLow(_Symbol,_Period,lowestLow_BarIndex); // Get the lowest low price in the range isInRange = (PRICE1_Y1 - PRICE2_Y2)/_Point <= rangeSizePoints; // Check if the range size is within the allowed points if (isInRange){ // If the range size is valid plotConsolidationRange(rangeNAME,TIME1_X1,PRICE1_Y1,TIME2_Y2,PRICE2_Y2); // Plot the consolidation range isRangeExist = true; // Set the range exist flag to true Print("RANGE PLOTTED"); // Print a message indicating the range is plotted } } }
После компиляции мы получаем следующие результаты:
Мы строим диапазон в пределах предопределенных точек и сообщаем об экземпляре построенного диапазона в журнале. Если вам не нужно заполнять диапазон, все, что нужно сделать, это установить флаг свойства заполнения в значение false. Это нарисует свойство линии прямоугольника, а ширина будет включена и применена, как необходимо. Ниже представлена логика:
ObjectSetInteger(0,rangeName,OBJPROP_FILL,false); // Disable fill for the range
В результате получится следующий диапазон:
В статье мы будем использовать заполненный диапазон. Теперь, когда мы уверены, что можем установить диапазон, нам нужно продолжить и разработать логику, которая будет следить за прорывом диапазона и открывать позиции соответственно, либо отслеживать расширение диапазона и обновлять его координаты до новых значений.
Далее мы просто идентифицируем случаи прорыва, как описано в теоретической части, и если они есть, мы открываем рыночные позиции. Это необходимо делать на каждом тике без ограничения на новые бары. Сначала мы объявляем цены Ask и Bid, которые будем использовать для открытия позиций после выполнения соответствующих условий. Обратите внимание, что это необходимо делать на каждом тике, чтобы мы получали самые свежие котировки цен.
double Ask = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits); // Get and normalize the current Ask price double Bid = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits); // Get and normalize the current Bid price
Здесь мы объявляем переменные типа данных double для хранения последних цен и нормализуем их по цифрам символа валюты, округляя число с плавающей запятой для сохранения точности.
Поскольку мы сканируем прорывы на каждом тике, нам понадобится логика, чтобы гарантировать, что мы нагружаем программу только в том случае, если диапазон действительно существует и мы находимся в нем. Большую часть времени рынок будет находиться в состоянии средней или высокой волатильности, поэтому частота появления диапазона консолидации будет ограниченной, а если диапазон будет определен и окажется в пределах графика, цена в конечном итоге выйдет за пределы диапазона. Если какое-либо из этих условий выполняется, то нет необходимости проверять наличие прорывов или дальнейших обновлений диапазона. Мы просто расслабляемся и ждем, когда будет определен другой диапазон.
if (isRangeExist && isInRange){ // If the range exists and we are in range ... }
Здесь мы проверяем, существует ли диапазон консолидации (isRangeExist) и находится ли текущая цена в этом диапазоне (isInRange). Если оба условия верны, переходим к расчету потенциальных цен прорыва.
double R_HighBreak_Prc = (PRICE2_Y2+rangeSizePoints*_Point); // Calculate the high breakout price double R_LowBreak_Prc = (PRICE1_Y1-rangeSizePoints*_Point); // Calculate the low breakout price
Чтобы найти цену прорыва вверх, мы прибавляем максимально допустимый размер диапазона в пунктах к минимальной цене. Этот расчет выполняется путем умножения размера диапазона в пунктах на размер пункта и добавления результата к переменной PRICE2_Y2, сохраняя окончательное значение в переменной типа данных double с именем R_HighBreak_Prc. Например, если по-прежнему предположить, что наша самая низкая цена составляет 0,66773, то размер диапазона пунктов составит 400, а стоимость пункта — 0,00001. Умножая 400 на 0,00001, получаем (400 * 0,00001) 0,00400. Прибавляя это значение к цене, получаем (0,66773 + 0,00400) 0,67173. Этот окончательный вычисленный результат представляет собой значение, которое хранится в цене прорыва вверх, и мы будем использовать это значение для сравнения с рыночной ценой, чтобы определить прорыв, если запрашиваемая цена превысит эту цену. Аналогично, чтобы определить цену прорыва вниз, мы вычитаем максимально допустимый размер диапазона в пунктах из максимальной цены. Это делается путем умножения размера диапазона в пунктах на размер пункта и вычитания результата из максимальной цены, а итоговое значение сохраняется в переменной R_LowBreak_Prc.
Затем мы переходим к поиску прорыва и, если он есть, открываем соответствующие позиции. Сначала давайте рассмотрим ситуацию, когда рыночная цена прорывается выше определенной цены прорыва вверх, сигнализируя о возможности покупки.
if (Ask > R_HighBreak_Prc){ // If the Ask price breaks the high breakout price Print("BUY NOW, ASK = ",Ask,", L = ",PRICE2_Y2,", H BREAK = ",R_HighBreak_Prc); // Print a message to buy isInRange = false; isRangeExist = false; // Reset range flags if (PositionsTotal() > 0){return;} // Exit the function obj_Trade.Buy(0.01,_Symbol,Ask,Bid-sl_points*_Point,Bid+tp_points*_Point); return; // Exit the function }
Сначала мы проверяем, превышает ли текущая цена Ask цену прорыва вверх. Если это условие выполняется, это означает, что рыночная цена вышла за верхнюю границу диапазона консолидации. Выводим сообщение в терминал, в котором регистрируется событие и предоставляется контекст для сигнала на покупку, включая текущую цену Ask, самую низкую цену диапазона и цену прорыва вверх. Затем мы сбрасываем флаги isInRange и isRangeExist на значение false, указывая на то, что текущий диапазон консолидации больше не действителен, и предотвращая принятие дальнейших решений на основе этого диапазона. Далее мы проверяем, есть ли какие-либо существующие позиции, используя функцию PositionsTotal, и выходим из функции заранее, если таковые имеются, чтобы избежать открытия нескольких позиций одновременно. Если нет существующих позиций, мы приступаем к размещению ордера на покупку, используя метод obj_Trade объекта CTrade, который определяет объем, символ сделки, цену открытия как цену Ask, а также цены стоп-лосса и тейк-профита. Наконец, мы выходим из функции, чтобы завершить процесс инициирования торговли, гарантируя отсутствие дальнейшего выполнения кода в течение этого тика.
Чтобы справиться с ситуацией, когда рыночная цена опускается ниже цены прорыва вниз, сигнализируя о возможности продажи, применяется аналогичная логика управления, показанная во фрагменте кода ниже.
else if (Bid < R_LowBreak_Prc){ // If the Bid price breaks the low breakout price Print("SELL NOW"); // Print a message to sell isInRange = false; isRangeExist = false; // Reset range flags if (PositionsTotal() > 0){return;} // Exit the function obj_Trade.Sell(0.01,_Symbol,Bid,Ask+sl_points*_Point,Ask-tp_points*_Point); return; // Exit the function }
После компиляции мы получаем следующее.
Из изображения выше видно, что как только мы выходим за пределы диапазона, в данном случае — вверх, мы открываем позицию на покупку со стоп-лоссом и тейк-профитом соответственно. Условия входа и торговые уровни полностью настраиваются, и вы можете использовать те, которые считаете подходящими или которые соответствуют вашему стилю торговли. Например, у вас могут быть уровни в диапазоне экстремумов или соотношения риска к прибыли. Для подтверждения вы можете видеть, что запрашиваемая цена составляет 0,68313, а минимальная — 0,67911, что делает цену прорыва вверх равной (0,67911 + 0,00400) 0,68311. Математически текущая запрашиваемая цена составляет 0,68313, что выше вычисленной максимальной цены 0,68311, что соответствует нашим условиям прорыва верхнего диапазона, что приводит к открытию позиции на покупку по текущей запрашиваемой цене.
В настоящее время диапазон статичен и не перемещается. То есть прямоугольник фиксирован. Даже если мы правильно установим диапазон, объект диапазона не обновится. Таким образом, нам необходимо обновить диапазон, если цена превышает заданную цену диапазона объекта. Чтобы вдохнуть жизнь в прямоугольник, давайте рассмотрим логику, которая всегда будет обновлять расширение диапазона сгенерированными барами. Сначала рассмотрим сценарий, при котором текущая цена Ask превышает ранее зафиксированную максимальную цену в пределах диапазона консолидации. Если это условие выполняется, это означает, что верхнюю границу диапазона консолидации необходимо обновить, чтобы она отражала новую максимальную цену. Это достигается с помощью фрагмента кода ниже.
if (Ask > PRICE1_Y1){ // If the Ask price is higher than the current high price PRICE1_Y1 = Ask; // Update the high price to the Ask price TIME2_Y2 = iTime(_Symbol,_Period,0); // Update the end time to the current time Print("UPDATED RANGE PRICE1_Y1 TO ASK, NEEDS REPLOT"); // Print a message indicating the range needs to be replotted plotConsolidationRange(rangeNAME,TIME1_X1,PRICE1_Y1,TIME2_Y2,PRICE2_Y2); // Replot the consolidation range }
Если запрашиваемая цена выше ранее зафиксированной максимальной цены, мы устанавливаем PRICE1_Y1 на текущую запрашиваемую цену. Одновременно мы обновляем конечное время диапазона TIME2_Y2 на текущее время, полученное путем использования функции iTime и передачи индекса целевого бара в качестве текущего бара, 0. Чтобы отслеживать эти изменения и обеспечивать ясность, мы печатаем сообщение на терминал о том, что диапазон обновлен и требует повторного отображения. Затем мы вызываем функцию plotConsolidationRange с обновленными параметрами, включая новую максимальную цену и текущее время, чтобы визуально отразить изменения на графике.
Для обработки сценария, когда текущая цена Bid падает ниже ранее зафиксированной минимальной цены в пределах диапазона консолидации, что указывает на необходимость обновления нижней границы диапазона консолидации для отражения новой минимальной цены, применяется аналогичный подход.
else if (Bid < PRICE2_Y2){ // If the Bid price is lower than the current low price PRICE2_Y2 = Bid; // Update the low price to the Bid price TIME2_Y2 = iTime(_Symbol,_Period,0); // Update the end time to the current time Print("UPDATED RANGE PRICE2_Y2 TO BID, NEEDS REPLOT"); // Print a message indicating the range needs to be replotted plotConsolidationRange(rangeNAME,TIME1_X1,PRICE1_Y1,TIME2_Y2,PRICE2_Y2); // Replot the consolidation range }
Чтобы отслеживать эти изменения, давайте рассмотрим случай, когда у нас нет обновлений и когда у нас есть обновления в формате GIF, чтобы было легче провести сравнение.
До обновления:
После обновления:
Наконец, все еще возможен сценарий, при котором ни цена Ask не превзойдет максимальную цену, ни цена Bid не опустится ниже минимальной цены. Если это так, нам необходимо по-прежнему расширять диапазон консолидации, включив в него последний завершенный бар. Ниже приведен фрагмент кода, который поможет это сделать.
else{ if (isNewBar){ // If a new bar has appeared TIME2_Y2 = iTime(_Symbol,_Period,1); // Update the end time to the previous bar time Print("EXTEND THE RANGE TO PREV BAR TIME"); // Print a message indicating the range is extended plotConsolidationRange(rangeNAME,TIME1_X1,PRICE1_Y1,TIME2_Y2,PRICE2_Y2); // Replot the consolidation range } }
Мы проверяем, появился ли новый бар, с помощью флага isNewBar. Если новый бар действительно появился, мы обновляем время окончания диапазона консолидации TIME2_Y2 до времени предыдущего бара, которое получаем с помощью функции iTime, передающей целевой индекс бара как 1 (бар, предшествующий текущему). Чтобы обеспечить ясность и отслеживать эту корректировку, мы выводим на терминал сообщение о том, что конечное время диапазона продлено до времени предыдущего бара. Затем мы вызываем функцию plotConsolidationRange с обновленными параметрами, включая новое время окончания, чтобы визуально отразить изменения на графике.
Ниже представлена иллюстрация этапа обновления в диапазоне.
Полный исходный код, отвечающий за создание советника на основе стратегии прорыва диапазона консолидации на языке MQL5, представлен ниже:
//+------------------------------------------------------------------+ //| CONSOLIDATION RANGE BREAKOUT.mq5 | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #include <Trade/Trade.mqh> // Include the trade library CTrade obj_Trade; // Create an instance of the CTrade class #define rangeNAME "CONSOLIDATION RANGE" // Define the name of the consolidation range datetime TIME1_X1, TIME2_Y2; // Declare datetime variables to hold range start and end times double PRICE1_Y1, PRICE2_Y2; // Declare double variables to hold range high and low prices bool isRangeExist = false; // Flag to check if the range exists bool isInRange = false; // Flag to check if we are currently within the range int rangeBars = 10; // Number of bars to consider for the range int rangeSizePoints = 400; // Maximum range size in points double sl_points = 500.0; // Stop loss points double tp_points = 500.0; // Take profit points //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit(){ //--- // Initialization code here (we don't initialize anything) //--- return(INIT_SUCCEEDED); // Return initialization success } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason){ //--- // Deinitialization code here (we don't deinitialize anything) } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick(){ //--- int currBars = iBars(_Symbol,_Period); // Get the current number of bars static int prevBars = currBars; // Static variable to store the previous number of bars static bool isNewBar = false; // Static flag to check if a new bar has appeared if (prevBars == currBars){isNewBar = false;} // Check if the number of bars has not changed else {isNewBar = true; prevBars = currBars;} // If the number of bars has changed, set isNewBar to true and update prevBars if (isRangeExist == false && isNewBar){ // If no range exists and a new bar has appeared TIME1_X1 = iTime(_Symbol,_Period,rangeBars); // Get the start time of the range int highestHigh_BarIndex = iHighest(_Symbol,_Period,MODE_HIGH,rangeBars,1); // Get the bar index with the highest high in the range PRICE1_Y1 = iHigh(_Symbol,_Period,highestHigh_BarIndex); // Get the highest high price in the range TIME2_Y2 = iTime(_Symbol,_Period,0); // Get the current time int lowestLow_BarIndex = iLowest(_Symbol,_Period,MODE_LOW,rangeBars,1); // Get the bar index with the lowest low in the range PRICE2_Y2 = iLow(_Symbol,_Period,lowestLow_BarIndex); // Get the lowest low price in the range isInRange = (PRICE1_Y1 - PRICE2_Y2)/_Point <= rangeSizePoints; // Check if the range size is within the allowed points if (isInRange){ // If the range size is valid plotConsolidationRange(rangeNAME,TIME1_X1,PRICE1_Y1,TIME2_Y2,PRICE2_Y2); // Plot the consolidation range isRangeExist = true; // Set the range exist flag to true Print("RANGE PLOTTED"); // Print a message indicating the range is plotted } } double Ask = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits); // Get and normalize the current Ask price double Bid = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits); // Get and normalize the current Bid price if (isRangeExist && isInRange){ // If the range exists and we are in range double R_HighBreak_Prc = (PRICE2_Y2+rangeSizePoints*_Point); // Calculate the high breakout price double R_LowBreak_Prc = (PRICE1_Y1-rangeSizePoints*_Point); // Calculate the low breakout price if (Ask > R_HighBreak_Prc){ // If the Ask price breaks the high breakout price Print("BUY NOW, ASK = ",Ask,", L = ",PRICE2_Y2,", H BREAK = ",R_HighBreak_Prc); // Print a message to buy isInRange = false; isRangeExist = false; // Reset range flags if (PositionsTotal() > 0){return;} // Exit the function obj_Trade.Buy(0.01,_Symbol,Ask,Bid-sl_points*_Point,Bid+tp_points*_Point); return; // Exit the function } else if (Bid < R_LowBreak_Prc){ // If the Bid price breaks the low breakout price Print("SELL NOW"); // Print a message to sell isInRange = false; isRangeExist = false; // Reset range flags if (PositionsTotal() > 0){return;} // Exit the function obj_Trade.Sell(0.01,_Symbol,Bid,Ask+sl_points*_Point,Ask-tp_points*_Point); return; // Exit the function } if (Ask > PRICE1_Y1){ // If the Ask price is higher than the current high price PRICE1_Y1 = Ask; // Update the high price to the Ask price TIME2_Y2 = iTime(_Symbol,_Period,0); // Update the end time to the current time Print("UPDATED RANGE PRICE1_Y1 TO ASK, NEEDS REPLOT"); // Print a message indicating the range needs to be replotted plotConsolidationRange(rangeNAME,TIME1_X1,PRICE1_Y1,TIME2_Y2,PRICE2_Y2); // Replot the consolidation range } else if (Bid < PRICE2_Y2){ // If the Bid price is lower than the current low price PRICE2_Y2 = Bid; // Update the low price to the Bid price TIME2_Y2 = iTime(_Symbol,_Period,0); // Update the end time to the current time Print("UPDATED RANGE PRICE2_Y2 TO BID, NEEDS REPLOT"); // Print a message indicating the range needs to be replotted plotConsolidationRange(rangeNAME,TIME1_X1,PRICE1_Y1,TIME2_Y2,PRICE2_Y2); // Replot the consolidation range } else{ if (isNewBar){ // If a new bar has appeared TIME2_Y2 = iTime(_Symbol,_Period,1); // Update the end time to the previous bar time Print("EXTEND THE RANGE TO PREV BAR TIME"); // Print a message indicating the range is extended plotConsolidationRange(rangeNAME,TIME1_X1,PRICE1_Y1,TIME2_Y2,PRICE2_Y2); // Replot the consolidation range } } } } //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Function to plot the consolidation range | //| rangeName - name of the range object | //| time1_x1 - start time of the range | //| price1_y1 - high price of the range | //| time2_x2 - end time of the range | //| price2_y2 - low price of the range | //+------------------------------------------------------------------+ void plotConsolidationRange(string rangeName,datetime time1_x1,double price1_y1, datetime time2_x2,double price2_y2){ if (ObjectFind(0,rangeName) < 0){ // If the range object does not exist ObjectCreate(0,rangeName,OBJ_RECTANGLE,0,time1_x1,price1_y1,time2_x2,price2_y2); // Create the range object ObjectSetInteger(0,rangeName,OBJPROP_COLOR,clrBlue); // Set the color of the range ObjectSetInteger(0,rangeName,OBJPROP_FILL,true); // Enable fill for the range ObjectSetInteger(0,rangeName,OBJPROP_WIDTH,5); // Set the width of the range } else { // If the range object exists ObjectSetInteger(0,rangeName,OBJPROP_TIME,0,time1_x1); // Update the start time of the range ObjectSetDouble(0,rangeName,OBJPROP_PRICE,0,price1_y1); // Update the high price of the range ObjectSetInteger(0,rangeName,OBJPROP_TIME,1,time2_x2); // Update the end time of the range ObjectSetDouble(0,rangeName,OBJPROP_PRICE,1,price2_y2); // Update the low price of the range } ChartRedraw(0); // Redraw the chart to reflect changes }
Результаты тестирования на истории
Вот результаты тестирования в тестере стратегий.
- График баланса/эквити:
- Результаты тестирования на истории:
- Входы в сделки по периодам:
Заключение
В заключение можно с уверенностью сказать, что автоматизация стратегии прорыва консолидации не так сложна, как может показаться. Как видим, для ее создания требовалось лишь четкое понимание стратегии и фактических требований или, скорее, целей, которые необходимо достичь.
В целом, в статье сделан акцент на теоретической части, которую необходимо учитывать и четко понимать для разработки стратегии. Код содержит шаги, необходимые для анализа свечей, определения периодов низкой волатильности, уровней поддержки и сопротивления для периодов, а также для отслеживания прорывов, визуализации результатов и открытия позиций по сигналам. В долгосрочной перспективе это позволяет автоматизировать стратегию, способствуя ее более быстрому исполнению и масштабируемости.
Надеюсь, что статья оказалась для вас полезной, интересной и простой для понимания, и вы сможете использовать представленные в ней знания при разработке собственных советников.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/15311







- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования