Автоматизация торговых стратегий в MQL5 (Часть 42): Сессионная система пробоя начального диапазона (ORB)
Введение
В предыдущей статье (часть 41) мы разработали торговую систему Candle Range Theory (CRT) с фазами накопления, манипуляции и распределения (AMD) на языке MetaQuotes Language 5 (MQL5). Система определяла диапазоны накопления на заданном таймфрейме, выявляла пробои этих диапазонов с фильтрацией по глубине манипуляции и подтверждала разворот по закрытиям баров, чтобы входить в сделки на фазе распределения. В части 42 мы разработаем гибко настраиваемую сессионную торговую систему пробоя начального диапазона (Opening Range Breakout, ORB).
Система позволяет задать время начала и продолжительность любой торговой сессии в минутах. Она автоматически определяет реальные максимум и минимум начального диапазона на выбранном таймфрейме, выявляет пробои и при необходимости подтверждает их по закрытию нескольких баров, чтобы отфильтровать часть ложных сигналов. Сделки открываются только в направлении пробоя. Уровни Stop Loss и Take Profit могут рассчитываться динамически — по размеру диапазона — либо задаваться статически. После достижения порога прибыли можно включить трейлинг-стоп, а количество позиций ограничивается отдельно для каждого направления. Рассмотрим следующие темы:
- Принцип работы стратегии пробоя начального диапазона (ORB)
- Реализация на MQL5
- Бэктестирование
- Заключение
К концу статьи у вас будет программа на MQL5, способная торговать на чётких пробоях начального диапазона в любой сессии — лондонской, нью-йоркской, азиатской или заданной пользователем. Её можно будет дополнительно настраивать. Приступим!
Принцип работы стратегии пробоя начального диапазона (ORB)
Стратегия Opening Range Breakout (ORB) — это классическая внутридневная стратегия на импульсном движении, опирающаяся на первичный вектор движения рынка, который формируется в начале торговой сессии. Под «начальным диапазоном» понимаются максимум и минимум первых нескольких минут после открытия рынка — обычно от 5 до 60 минут. Затем мы ждём уверенного выхода цены выше верхней границы диапазона для бычьего пробоя либо ниже нижней границы для медвежьего пробоя и открываем сделку в направлении пробоя. Идея проста, но эффективна: начальный диапазон часто отражает борьбу покупателей и продавцов в момент, когда рынок отыгрывает ночные новости и поступающий поток ордеров. Уверенный пробой показывает, что одна из сторон получила контроль, и нередко приводит к устойчивому направленному движению. В целом логика системы довольно проста. Ниже показаны возможные ORB-сетапы.

Мы создадим гибкую сессионную ORB-систему, которая сможет работать с любым инструментом и любой торговой сессией: нью-йоркской, лондонской, азиатской или заданной пользователем. Пользователь сможет указать точное время начала, например 09:30 для NYSE или 08:00 для Лондона, а также продолжительность начального диапазона в минутах. В пределах этого временного окна система автоматически рассчитает реальные максимум и минимум диапазона на выбранном таймфрейме. При необходимости можно включить подтверждение пробоя по закрытию нескольких баров.
Алгоритм будет открывать не более одной позиции в каждом направлении в рамках одной сессии. Для Stop Loss и Take Profit предусмотрены два способа расчёта: динамический, основанный на размере диапазона, и статический — в фиксированных пунктах. Соотношение риска и прибыли можно настраивать. Также доступен трейлинг-стоп по пунктам, который активируется после достижения минимального порога прибыли. Кроме того, инструмент обеспечивает расширенную визуализацию на графике: заполненный прямоугольник диапазона, вертикальные линии начала и окончания сессии, постоянно отображаемые уровни максимума и минимума, а также стрелки входа.
Как вы уже могли заметить, для наглядности мы уделяем большое внимание визуализации. Ниже схематично показаны задачи, которые предстоит реализовать.

Реализация на MQL5
Чтобы создать программу на MQL5, откройте MetaEditor, перейдите в окно «Навигатор», найдите папку «Эксперты», нажмите «New» и следуйте указаниям мастера создания файла. После этого объявим входные параметры и глобальные переменные, которые понадобятся в программе.
//+------------------------------------------------------------------+ //| ORB Opening Range Breakout EA.mq5 | //| Copyright 2025, Allan Munene Mutiiria. | //| https://t.me/Forex_Algo_Trader | //+------------------------------------------------------------------+ #property copyright "Copyright 2025, Allan Munene Mutiiria." #property link "https://t.me/Forex_Algo_Trader" #property version "1.00" #include <Trade\Trade.mqh> //+------------------------------------------------------------------+ //| Перечисления | //+------------------------------------------------------------------+ enum SLTP_Method { // Определяем перечисление способов расчёта SL/TP Dynamic_Method = 0, // Динамический расчёт по размеру диапазона Static_Method = 1 // Статический расчёт в фиксированных пунктах }; enum TrailingTypeEnum { // Определяем перечисление типов трейлинг-стопа Trailing_None = 0, // Нет Trailing_Points = 1 // По пунктам }; //+------------------------------------------------------------------+ //| Входные параметры | //+------------------------------------------------------------------+ input ENUM_TIMEFRAMES RangeTF = PERIOD_M5; // Таймфрейм расчёта начального диапазона input int RangeDurationMinutes = 30; // Продолжительность начального диапазона в минутах input string SessionStartTime = "09:00"; // Время начала сессии (ЧЧ:ММ) input double TradeVolume = 0.01; // Размер лота input double RR_Ratio = 2.0; // Соотношение риска и прибыли input SLTP_Method SLTP_Approach = Dynamic_Method; // Способ расчёта SL/TP input int SL_Points = 50; // Stop Loss в пунктах (для статического метода) input TrailingTypeEnum TrailingType = Trailing_None; // Тип трейлинг-стопа input double Trailing_Stop_Points = 20.0; // Расстояние трейлинг-стопа в пунктах input double Min_Profit_To_Trail_Points = 30.0; // Минимальная прибыль в пунктах для запуска трейлинга input int UniqueID = 987654321; // Уникальный идентификатор сделок input int MaxPositionsDir = 1; // Максимум позиций в каждом направлении input bool UseBreakoutFilter = true; // Использовать фильтр подтверждения пробоя input int ConfirmBars = 1; // Количество баров для подтверждения пробоя по закрытию (0 — отключить)
Начнём реализацию с подключения торговой библиотеки с помощью директивы «#include <Trade\Trade.mqh>». Она предоставляет класс CTrade и функции, необходимые для отправки торговых запросов и управления позициями. Затем определим два перечисления для упорядочения пользовательских настроек. Перечисление SLTP_Method содержит Dynamic_Method для расчёта Stop Loss и Take Profit на основе фактического размера начального диапазона и Static_Method для расчёта в фиксированных пунктах. В перечислении TrailingTypeEnum значение Trailing_None отключает трейлинг-стоп, а Trailing_Points включает сопровождение позиции с заданным расстоянием в пунктах после достижения минимального порога прибыли.
Далее объявим входные параметры для настройки программы непосредственно в окне свойств советника. RangeTF задаёт таймфрейм, на котором рассчитываются максимум и минимум начального диапазона; RangeDurationMinutes — его продолжительность в минутах после начала сессии; SessionStartTime — время начала новой сессии в формате HH:MM, например 09:30 для открытия NYSE или 08:00 для Лондона; TradeVolume — размер лота. Назначение остальных параметров поясняется в комментариях. Благодаря этим настройкам систему можно адаптировать к разным инструментам и торговым сессиям без изменения кода. Далее объявим глобальные переменные.
//+------------------------------------------------------------------+ //| Глобальные переменные | //+------------------------------------------------------------------+ CTrade obj_Trade; //--- Торговый объект datetime sessionStart = 0; //--- Время начала сессии datetime rangeEndTime = 0; //--- Время окончания диапазона double rangeHigh = 0.0; //--- Максимум диапазона double rangeLow = 0.0; //--- Минимум диапазона bool rangeDefined = false; //--- Флаг сформированного диапазона bool breakoutHigh = false; //--- Флаг пробоя верхней границы bool breakoutLow = false; //--- Флаг пробоя нижней границы double breakoutPrice = 0.0; //--- Цена пробоя string highLevelObj = "ORB_HighLevel"; //--- Имя объекта верхнего уровня string lowLevelObj = "ORB_LowLevel"; //--- Имя объекта нижнего уровня string highTextObj = "ORB_High_Text"; //--- Текстовый объект верхнего уровня string lowTextObj = "ORB_Low_Text"; //--- Текстовый объект нижнего уровня bool tradedLong = false; //--- Флаг выполненной длинной сделки bool tradedShort = false; //--- Флаг выполненной короткой сделки datetime lastConfirmTime = 0; //--- Время последнего подтверждающего бара
Следующим шагом объявим глобальные переменные, которые будут хранить состояние программы в течение каждой торговой сессии и обеспечивать работу алгоритма пробоя начального диапазона. Создадим объект obj_Trade класса CTrade для выполнения торговых операций и изменения параметров позиций. Переменная sessionStart хранит точное время начала новой сессии, а rangeEndTime — момент завершения периода формирования начального диапазона. Границы диапазона хранятся в rangeHigh, изначально равной 0,0, и rangeLow. Логическая переменная rangeDefined показывает, сформирован ли диапазон текущей сессии. Назначение остальных переменных достаточно очевидно. После этого инициализируем систему, задав магический номер.
//+------------------------------------------------------------------+ //| Функция запуска советника | //+------------------------------------------------------------------+ int OnInit() { obj_Trade.SetExpertMagicNumber(UniqueID); //--- Задаём магический номер return(INIT_SUCCEEDED); //--- Возвращаем код успешной инициализации }
В обработчике события OnInit, который автоматически вызывается при загрузке программы или её добавлении на график, передадим пользовательский UniqueID объекту obj_Trade в качестве магического номера с помощью obj_Trade.SetExpertMagicNumber(UniqueID). Благодаря этому каждая открытая программой сделка получает уникальный идентификатор, что позволяет точно фильтровать и управлять позициями даже при одновременной работе нескольких советников и наличии ручных сделок. Затем вернём INIT_SUCCEEDED, подтверждая успешную инициализацию. Далее определим вспомогательные функции визуализации, которые используем после подготовки основной логики.
//+------------------------------------------------------------------+ //| Отрисовка горизонтального уровня | //+------------------------------------------------------------------+ void RenderLevel(string objName, double levelVal, color levelClr, string levelDesc) { ObjectDelete(ChartID(), objName); //--- Удаляем объект ObjectCreate(ChartID(), objName, OBJ_HLINE, 0, 0, levelVal); //--- Создаём горизонтальную линию ObjectSetInteger(ChartID(), objName, OBJPROP_COLOR, levelClr); //--- Задаём цвет ObjectSetInteger(ChartID(), objName, OBJPROP_STYLE, STYLE_DOT); //--- Задаём стиль ObjectSetString(ChartID(), objName, OBJPROP_TOOLTIP, levelDesc); //--- Задаём всплывающую подсказку ChartRedraw(ChartID()); //--- Перерисовываем график } //+------------------------------------------------------------------+ //| Отрисовка вертикальной линии | //+------------------------------------------------------------------+ void RenderVLine(string objName, datetime timeVal, color lineClr, string desc) { ObjectDelete(ChartID(), objName); //--- Удаляем объект ObjectCreate(ChartID(), objName, OBJ_VLINE, 0, timeVal, 0); //--- Создаём вертикальную линию ObjectSetInteger(ChartID(), objName, OBJPROP_COLOR, lineClr); //--- Задаём цвет ObjectSetInteger(ChartID(), objName, OBJPROP_STYLE, STYLE_DOT); //--- Задаём стиль ObjectSetInteger(ChartID(), objName, OBJPROP_WIDTH, 1); //--- Задаём ширину ObjectSetInteger(ChartID(), objName, OBJPROP_BACK, true); //--- Размещаем объект на заднем плане ObjectSetInteger(ChartID(), objName, OBJPROP_RAY, true); //--- Включаем продолжение линии ObjectSetInteger(ChartID(), objName, OBJPROP_HIDDEN, true); //--- Скрываем объект из списка ObjectSetString(ChartID(), objName, OBJPROP_TOOLTIP, desc); //--- Задаём всплывающую подсказку ChartRedraw(ChartID()); //--- Перерисовываем график } //+------------------------------------------------------------------+ //| Отрисовка текстовой подписи | //+------------------------------------------------------------------+ void RenderText(string objName, datetime timeVal, double priceVal, string textStr, color textClr, int anchorVal) { ObjectDelete(ChartID(), objName); //--- Удаляем объект ObjectCreate(ChartID(), objName, OBJ_TEXT, 0, timeVal, priceVal); //--- Создаём текстовый объект ObjectSetString(ChartID(), objName, OBJPROP_TEXT, textStr); //--- Задаём текст ObjectSetInteger(ChartID(), objName, OBJPROP_COLOR, textClr); //--- Задаём цвет ObjectSetInteger(ChartID(), objName, OBJPROP_ANCHOR, anchorVal); //--- Задаём точку привязки ObjectSetInteger(ChartID(), objName, OBJPROP_FONTSIZE, 10); //--- Задаём размер шрифта ChartRedraw(ChartID()); //--- Перерисовываем график } //+------------------------------------------------------------------+ //| Отрисовка стрелки входа | //+------------------------------------------------------------------+ void DrawEntryArrow(datetime timeVal, double priceVal, bool isBuy) { string markerName = "EntryMarker_" + IntegerToString(timeVal); //--- Имя маркера ObjectCreate(ChartID(), markerName, OBJ_ARROW, 0, timeVal, priceVal); //--- Создаём стрелку int arrowCode = isBuy ? 233 : 234; //--- Код стрелки color arrowClr = isBuy ? clrBlue : clrRed; //--- Цвет стрелки int anchor = isBuy ? ANCHOR_BOTTOM : ANCHOR_TOP; //--- Точка привязки ObjectSetInteger(ChartID(), markerName, OBJPROP_ARROWCODE, arrowCode); //--- Задаём код ObjectSetInteger(ChartID(), markerName, OBJPROP_COLOR, arrowClr); //--- Задаём цвет ObjectSetInteger(ChartID(), markerName, OBJPROP_ANCHOR, anchor); //--- Задаём точку привязки ChartRedraw(ChartID()); //--- Перерисовываем график } //+------------------------------------------------------------------+ //| Подсчёт активных позиций по типу | //+------------------------------------------------------------------+ int ActivePositions(ENUM_POSITION_TYPE posType) { int total = 0; //--- Инициализируем счётчик for (int pos = PositionsTotal() - 1; pos >= 0; pos--) { //--- Перебираем позиции if (PositionGetSymbol(pos) == _Symbol && PositionGetInteger(POSITION_MAGIC) == UniqueID && PositionGetInteger(POSITION_TYPE) == posType) { //--- Проверяем позицию total++; //--- Увеличиваем счётчик } } return total; //--- Возвращаем количество }
Здесь создаются вспомогательные функции для визуализации и управления позициями. Они позволяют наглядно отображать начальный диапазон и торговые сигналы, сохраняя структуру кода понятной. Функция RenderLevel рисует или обновляет постоянно отображаемые горизонтальные линии максимума и минимума диапазона. Сначала она удаляет объект с тем же именем, затем создаёт новый OBJ_HLINE на заданном ценовом уровне, задаёт цвет — зелёный для максимума и красный для минимума, пунктирный стиль и текст всплывающей подсказки, после чего перерисовывает график.
Функция RenderVLine размещает вертикальные линии, обозначающие начало сессии и окончание формирования диапазона. Она удаляет предыдущий объект и создаёт OBJ_VLINE в заданный момент времени. Для линии задаются синий цвет, пунктирный стиль, ширина 1, размещение на заднем плане, продление линии в виде луча вправо, скрытие из списка объектов и всплывающая подсказка с точным временем. Затем график обновляется функцией ChartRedraw. Функция RenderText добавляет текстовые подписи, например время начала и окончания диапазона либо подписи «ORB High» и «ORB Low». Она удаляет существующий текстовый объект, создаёт OBJ_TEXT в заданных координатах времени и цены, после чего задаёт текст и остальные свойства.
Для отображения входов реализуем функцию DrawEntryArrow, которая размещает на графике стрелку в момент открытия сделки. Функция формирует уникальное имя по текущему времени, создаёт объект OBJ_ARROW, выбирает Wingdings с кодом 233 — стрелка вверх для покупки — или 234 — стрелка вниз для продажи. Для покупки используется синий цвет, для продажи — красный; также задаётся соответствующая точка привязки, после чего график перерисовывается. В MQL5 для стрелок предусмотрены специальные символы Wingdings, показанные ниже; при желании можно выбрать другой код.

Наконец, определим функцию ActivePositions, которая подсчитывает открытые позиции указанного типа — покупки или продажи — принадлежащие этой программе. Она перебирает все позиции в обратном порядке и проверяет совпадение символа, магического номера UniqueID и типа позиции POSITION_TYPE_BUY или POSITION_TYPE_SELL, после чего возвращает их количество. Теперь можно перейти к реализации стратегии и сначала определить диапазон каждой сессии.
//+------------------------------------------------------------------+ //| Функция обработки тиков | //+------------------------------------------------------------------+ void OnTick() { datetime currentTime = TimeCurrent(); //--- Получаем текущее время MqlDateTime timeStruct; //--- Структура времени TimeToStruct(currentTime, timeStruct); //--- Преобразуем время в структуру // Определяем начало новой сессии string currentTimeStr = StringFormat("%02d:%02d", timeStruct.hour, timeStruct.min); //--- Форматируем строку времени if (currentTimeStr == SessionStartTime && sessionStart != currentTime - (timeStruct.hour * 3600 + timeStruct.min * 60 + timeStruct.sec)) { //--- Проверяем начало новой сессии sessionStart = currentTime - timeStruct.sec; //--- Выравниваем время по началу минуты rangeEndTime = sessionStart + RangeDurationMinutes * 60; //--- Рассчитываем время окончания rangeHigh = 0.0; //--- Сбрасываем максимум rangeLow = DBL_MAX; //--- Сбрасываем минимум rangeDefined = false; //--- Сбрасываем флаг сформированного диапазона breakoutHigh = false; //--- Сбрасываем флаг пробоя верхней границы breakoutLow = false; //--- Сбрасываем флаг пробоя нижней границы tradedLong = false; //--- Сбрасываем флаг длинной сделки tradedShort = false; //--- Сбрасываем флаг короткой сделки lastConfirmTime = 0; //--- Сбрасываем время подтверждения // Удаляем прежние графические объекты текущих уровней ObjectDelete(ChartID(), highLevelObj); //--- Удаляем верхний уровень ObjectDelete(ChartID(), lowLevelObj); //--- Удаляем нижний уровень ObjectDelete(ChartID(), highTextObj); //--- Удаляем подпись верхнего уровня ObjectDelete(ChartID(), lowTextObj); //--- Удаляем подпись нижнего уровня } if (sessionStart == 0) return; //--- Завершаем работу, если сессия не определена double currBid = SymbolInfoDouble(_Symbol, SYMBOL_BID); //--- Получаем цену Bid double currAsk = SymbolInfoDouble(_Symbol, SYMBOL_ASK); //--- Получаем цену Ask // Формируем начальный диапазон if (currentTime < rangeEndTime) { //--- Проверяем период формирования диапазона rangeHigh = MathMax(rangeHigh, iHigh(_Symbol, RangeTF, 0)); //--- Обновляем максимум rangeLow = MathMin(rangeLow, iLow(_Symbol, RangeTF, 0)); //--- Обновляем минимум } else if (!rangeDefined) { //--- Проверяем, не сформирован ли диапазон rangeDefined = true; //--- Устанавливаем флаг сформированного диапазона // Рисуем прямоугольник начального диапазона string rectObj = "ORB_Rectangle_" + IntegerToString(sessionStart); //--- Имя прямоугольника ObjectCreate(ChartID(), rectObj, OBJ_RECTANGLE, 0, sessionStart, rangeHigh, rangeEndTime, rangeLow); //--- Создаём прямоугольник ObjectSetInteger(ChartID(), rectObj, OBJPROP_COLOR, clrLightBlue); //--- Задаём цвет ObjectSetInteger(ChartID(), rectObj, OBJPROP_FILL, true); //--- Включаем заливку ObjectSetInteger(ChartID(), rectObj, OBJPROP_BACK, true); //--- Размещаем объект на заднем плане ObjectSetInteger(ChartID(), rectObj, OBJPROP_STYLE, STYLE_SOLID); //--- Задаём стиль ChartRedraw(ChartID()); //--- Перерисовываем график // Добавляем вертикальные линии начала и окончания string startVLineObj = "ORB_StartVLine_" + IntegerToString(sessionStart); //--- Имя вертикальной линии начала RenderVLine(startVLineObj, sessionStart, clrBlue, "ORB Start at " + TimeToString(sessionStart, TIME_MINUTES)); //--- Отображаем вертикальную линию начала string endVLineObj = "ORB_EndVLine_" + IntegerToString(sessionStart); //--- Имя вертикальной линии окончания RenderVLine(endVLineObj, rangeEndTime, clrBlue, "ORB End at " + TimeToString(rangeEndTime, TIME_MINUTES)); //--- Отображаем вертикальную линию окончания // Добавляем текстовые подписи времени начала и окончания double textOffset = (rangeHigh - rangeLow) * 0.05; //--- Рассчитываем отступ string startTimeTextObj = "ORB_StartTime_Text_" + IntegerToString(sessionStart); //--- Имя подписи времени начала RenderText(startTimeTextObj, sessionStart, rangeLow - textOffset, TimeToString(sessionStart, TIME_MINUTES), clrBlue, ANCHOR_UPPER); //--- Отображаем подпись времени начала string endTimeTextObj = "ORB_EndTime_Text_" + IntegerToString(sessionStart); //--- Имя подписи времени окончания RenderText(endTimeTextObj, rangeEndTime, rangeLow - textOffset, TimeToString(rangeEndTime, TIME_MINUTES), clrBlue, ANCHOR_UPPER); //--- Отображаем подпись времени окончания // Отображаем верхний и нижний уровни RenderLevel(highLevelObj, rangeHigh, clrGreen, "ORB High"); //--- Отображаем верхний уровень RenderLevel(lowLevelObj, rangeLow, clrRed, "ORB Low"); //--- Отображаем нижний уровень // Добавляем текстовые подписи RenderText(highTextObj, rangeEndTime, rangeHigh, "ORB High", clrGreen, ANCHOR_RIGHT_LOWER); //--- Отображаем подпись верхнего уровня RenderText(lowTextObj, rangeEndTime, rangeLow, "ORB Low", clrRed, ANCHOR_RIGHT_UPPER); //--- Отображаем подпись нижнего уровня } }
В обработчике OnTick сначала получим текущее серверное время с помощью TimeCurrent и запишем его в currentTime. Затем преобразуем значение в структуру MqlDateTime с помощью TimeToStruct, чтобы получить доступ к отдельным компонентам времени, например часам и минутам. После этого сформируем строку времени в формате HH:MM функцией StringFormat и сохраним её в currentTimeStr. Ниже показана структура MqlDateTime.
struct MqlDateTime { int year; // Год int mon; // Месяц int day; // День int hour; // Час int min; // Минуты int sec; // Секунды int day_of_week; // День недели (0 — воскресенье, 1 — понедельник, …, 6 — суббота) int day_of_year; // Номер дня в году (1 января соответствует значению 0) };
Структура упрощает получение отдельных компонентов времени. Чтобы определить точное начало новой торговой сессии, сравним сформированную строку с пользовательским параметром SessionStartTime. Дополнительное условие не позволяет повторно инициализировать одну и ту же сессию: сохранённое значение sessionStart сравнивается с расчётной временной отметкой текущего дня. При начале новой сессии sessionStart выравнивается по началу минуты путём вычитания секунд, а rangeEndTime рассчитывается прибавлением RangeDurationMinutes × 60 секунд. Затем rangeHigh сбрасывается в 0,0, rangeLow — в максимальное значение типа double, а флаги rangeDefined, breakoutHigh, breakoutLow, tradedLong, tradedShort и lastConfirmTime очищаются. Также удаляются постоянные уровни и текстовые подписи предыдущей сессии. Если активная сессия ещё не определена, то есть sessionStart == 0, обработка прекращается. В противном случае текущие цены Bid и Ask получаются функцией SymbolInfoDouble.
Пока формируется начальный диапазон, то есть currentTime < rangeEndTime, его границы непрерывно обновляются. rangeHigh принимает максимальное из текущего значения и максимума последнего бара на RangeTF с индексом 0, полученного через iHigh. rangeLow принимает минимальное из текущего значения и минимума последнего бара, полученного функцией iLow. После завершения периода, если диапазон ещё не зафиксирован, то есть !rangeDefined, флаг rangeDefined устанавливается в true и начинается визуализация сформированного начального диапазона.
Заполненный светло-синий прямоугольник строится от sessionStart на уровне rangeHigh до rangeEndTime на уровне rangeLow. Его уникальное имя формируется из времени сессии; объект получает сплошной стиль и размещается на заднем плане. В начале и конце диапазона функция RenderVLine создаёт синие пунктирные вертикальные линии со всплывающими подсказками. Чуть ниже диапазона функция RenderText выводит время начала и окончания; отступ равен 5% размера диапазона, подписи окрашены в синий цвет и привязаны вверх. Затем RenderLevel создаёт постоянные горизонтальные уровни: зелёный для максимума и красный для минимума. Справа от времени завершения диапазона размещаются соответствующие текстовые подписи, поэтому точные уровни пробоя остаются видимыми и спустя несколько часов или дней. Результат компиляции показан ниже.

После формирования диапазона остаётся отслеживать выход цены за его верхнюю или нижнюю границу, определять направление пробоя и открывать сделку по соответствующему сетапу. Всё довольно просто — ниже приведена соответствующая логика.
if (!rangeDefined) return; //--- Завершаем работу, если диапазон не сформирован // Определяем пробой bool justBreached = false; //--- Инициализируем флаг нового пробоя if (currAsk > rangeHigh && !breakoutHigh) { //--- Проверяем пробой верхней границы breakoutHigh = true; //--- Устанавливаем флаг пробоя верхней границы justBreached = true; //--- Устанавливаем флаг нового пробоя breakoutPrice = currAsk; //--- Сохраняем цену пробоя } else if (currBid < rangeLow && !breakoutLow) { //--- Проверяем пробой нижней границы breakoutLow = true; //--- Устанавливаем флаг пробоя нижней границы justBreached = true; //--- Устанавливаем флаг нового пробоя breakoutPrice = currBid; //--- Сохраняем цену пробоя } if ((breakoutHigh || breakoutLow) && !(tradedLong || tradedShort)) { //--- Проверяем наличие пробоя и отсутствие сделки // При необходимости подтверждаем пробой по закрытию баров bool confirmed = false; //--- Инициализируем флаг подтверждения if (ConfirmBars == 0) { //--- Проверяем, отключено ли подтверждение confirmed = true; //--- Устанавливаем флаг подтверждения } else { //--- Иначе datetime currConfirmTime = iTime(_Symbol, RangeTF, 0); //--- Получаем время подтверждающего бара if (currConfirmTime != lastConfirmTime) { //--- Проверяем новый подтверждающий бар lastConfirmTime = currConfirmTime; //--- Обновляем время последнего подтверждения int confirmCount = 0; //--- Инициализируем счётчик for (int i = 1; i <= ConfirmBars; i++) { //--- Перебираем бары double closePrice = iClose(_Symbol, RangeTF, i); //--- Получаем цену закрытия if (breakoutHigh && closePrice > rangeHigh) confirmCount++; //--- Проверяем верхнюю границу if (breakoutLow && closePrice < rangeLow) confirmCount++; //--- Проверяем нижнюю границу } if (confirmCount >= ConfirmBars) confirmed = true; //--- Устанавливаем флаг подтверждения } } if (confirmed && UseBreakoutFilter) { //--- Проверяем подтверждение и фильтр // Здесь можно добавить дополнительные фильтры; пока используем полученное подтверждение } if (confirmed) { //--- Проверяем подтверждение double sl = 0.0, tp = 0.0; //--- Инициализируем SL и TP if (breakoutHigh && ActivePositions(POSITION_TYPE_BUY) < MaxPositionsDir && !tradedLong) { //--- Проверяем условия входа в покупку if (SLTP_Approach == Dynamic_Method) { //--- Проверяем динамический метод double rangeSize = rangeHigh - rangeLow; //--- Рассчитываем размер диапазона sl = NormalizeDouble(rangeLow, _Digits); //--- Задаём Stop Loss tp = NormalizeDouble(currAsk + rangeSize * RR_Ratio, _Digits); //--- Задаём Take Profit } else { //--- Статический метод sl = NormalizeDouble(currAsk - SL_Points * _Point, _Digits); //--- Задаём Stop Loss tp = NormalizeDouble(currAsk + (SL_Points * _Point) * RR_Ratio, _Digits); //--- Задаём Take Profit } if (obj_Trade.Buy(TradeVolume, _Symbol, currAsk, sl, tp, "ORB Long Breakout")) { //--- Открываем сделку на покупку if (obj_Trade.ResultRetcode() == TRADE_RETCODE_DONE) { //--- Проверяем успешность операции Print("Long Breakout: Entry at ", DoubleToString(currAsk, _Digits), " SL at ", DoubleToString(sl, _Digits), " TP at ", DoubleToString(tp, _Digits)); //--- Записываем сведения о входе в журнал DrawEntryArrow(currentTime, currBid, true); //--- Рисуем стрелку tradedLong = true; //--- Устанавливаем флаг длинной сделки } } } else if (breakoutLow && ActivePositions(POSITION_TYPE_SELL) < MaxPositionsDir && !tradedShort) { //--- Проверяем условия входа в продажу if (SLTP_Approach == Dynamic_Method) { //--- Проверяем динамический метод double rangeSize = rangeHigh - rangeLow; //--- Рассчитываем размер диапазона sl = NormalizeDouble(rangeHigh, _Digits); //--- Задаём Stop Loss tp = NormalizeDouble(currBid - rangeSize * RR_Ratio, _Digits); //--- Задаём Take Profit } else { //--- Статический метод sl = NormalizeDouble(currBid + SL_Points * _Point, _Digits); //--- Задаём Stop Loss tp = NormalizeDouble(currBid - (SL_Points * _Point) * RR_Ratio, _Digits); //--- Задаём Take Profit } if (obj_Trade.Sell(TradeVolume, _Symbol, currBid, sl, tp, "ORB Short Breakout")) { //--- Открываем сделку на продажу if (obj_Trade.ResultRetcode() == TRADE_RETCODE_DONE) { //--- Проверяем успешность операции Print("Short Breakout: Entry at ", DoubleToString(currBid, _Digits), " SL at ", DoubleToString(sl, _Digits), " TP at ", DoubleToString(tp, _Digits)); //--- Записываем сведения о входе в журнал DrawEntryArrow(currentTime, currAsk, false); //--- Рисуем стрелку tradedShort = true; //--- Устанавливаем флаг короткой сделки } } } } }
Если начальный диапазон ещё не сформирован, то есть rangeDefined == false, функция сразу завершает работу, чтобы не запускать логику пробоя преждевременно. Затем фиксируется новый пробой: если текущая цена Ask превышает rangeHigh и breakoutHigh ещё не установлен, фиксируется пробой верхней границы — breakoutHigh получает true, точная цена записывается в breakoutPrice, а событие отмечается как новый выход за границу. Для выхода ниже нижней границы используется аналогичная логика. Если активно хотя бы одно направление пробоя и в текущей сессии ещё не открывались ни длинная, ни короткая сделки, то есть !tradedLong && !tradedShort, начинается подтверждение. При ConfirmBars == 0 пробой считается подтверждённым сразу. В противном случае на каждом новом баре таймфрейма диапазона, определяемом сравнением iTime с индексом 0 и lastConfirmTime, подсчитывается число предыдущих баров ConfirmBars, закрывшихся за пределами диапазона: выше rangeHigh для пробоя вверх или ниже rangeLow для пробоя вниз. confirmed устанавливается в true только после получения необходимого количества подтверждающих закрытий.
Параметр UseBreakoutFilter учитывается, хотя в текущей реализации он лишь пропускает уже полученное подтверждение; при желании сюда можно добавить дополнительные фильтры. После подтверждения рассчитываются Stop Loss и Take Profit выбранным способом. При бычьем пробое, если число позиций на покупку меньше MaxPositionsDir и длинная сделка в этой сессии ещё не открывалась, динамический режим размещает нормализованный Stop Loss на нижней границе диапазона, а Take Profit — выше текущей цены Ask на полный размер диапазона, умноженный на RR_Ratio. В статическом режиме Stop Loss устанавливается на SL_Points ниже Ask, а Take Profit — выше на то же расстояние, умноженное на коэффициент. Затем вызывается obj_Trade.Buy. При успешном выполнении, то есть TRADE_RETCODE_DONE, сведения о входе записываются на вкладку «Эксперты», функция DrawEntryArrow рисует синюю стрелку вверх, а tradedLong устанавливается в true, запрещая дальнейшие покупки в этой сессии. Для пробоя вниз применяется зеркальная логика. Ниже показан результат работы.

На изображении видно, что после подтверждения пробоя открывается сделка. Теперь остаётся управлять открытыми позициями и при необходимости включать трейлинг-стоп после движения рынка в нашу пользу.
//+------------------------------------------------------------------+ //| Применение трейлинг-стопа по пунктам | //+------------------------------------------------------------------+ void ApplyPointsTrailing() { double point = _Point; //--- Получаем размер пункта for (int i = PositionsTotal() - 1; i >= 0; i--) { //--- Перебираем позиции if (PositionGetTicket(i) > 0) { //--- Проверяем тикет if (PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == UniqueID) { //--- Проверяем символ и магический номер double sl = PositionGetDouble(POSITION_SL); //--- Получаем Stop Loss double tp = PositionGetDouble(POSITION_TP); //--- Получаем Take Profit double openPrice = PositionGetDouble(POSITION_PRICE_OPEN); //--- Получаем цену открытия ulong ticket = PositionGetInteger(POSITION_TICKET); //--- Получаем номер тикета if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { //--- Проверяем позицию на покупку double newSL = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID) - Trailing_Stop_Points * point, _Digits); //--- Рассчитываем новый Stop Loss if (newSL > sl && SymbolInfoDouble(_Symbol, SYMBOL_BID) - openPrice > Min_Profit_To_Trail_Points * point) { //--- Проверяем условия obj_Trade.PositionModify(ticket, newSL, tp); //--- Изменяем позицию } } else if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) { //--- Проверяем позицию на продажу double newSL = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK) + Trailing_Stop_Points * point, _Digits); //--- Рассчитываем новый Stop Loss if (newSL < sl && openPrice - SymbolInfoDouble(_Symbol, SYMBOL_ASK) > Min_Profit_To_Trail_Points * point) { //--- Проверяем условия obj_Trade.PositionModify(ticket, newSL, tp); //--- Изменяем позицию } } } } } } //--- Вызываем функцию на каждом тике в обработчике OnTick if (TrailingType == Trailing_Points && PositionsTotal() > 0) { //--- Проверяем, включён ли трейлинг-стоп ApplyPointsTrailing(); //--- Применяем трейлинг-стоп }
Функция ApplyPointsTrailing динамически перемещает Stop Loss в режиме Trailing_Points, защищая накопленную прибыль по мере движения рынка в нужном направлении. Сначала значение пункта символа сохраняется в переменной point с помощью _Point. Затем все открытые позиции перебираются в обратном порядке, что позволяет безопасно изменять их без конфликтов индексов. Для каждого действительного тикета проверяется соответствие текущему символу и магическому номеру UniqueID. После этого считываются текущие Stop Loss, Take Profit, цена открытия и номер тикета.
Для позиций на покупку потенциальный новый Stop Loss рассчитывается вычитанием Trailing_Stop_Points × point из текущей цены Bid с нормализацией по числу знаков символа. Изменение применяется только тогда, когда новый уровень выше действующего Stop Loss, то есть сокращает риск, а нереализованная прибыль превышает Min_Profit_To_Trail_Points × point. Позиция изменяется через obj_Trade.PositionModify с сохранением исходного Take Profit. Для позиций на продажу используется зеркальная логика. В конце обработчика OnTick проверяется, включён ли Trailing_Points и имеются ли открытые позиции. Если оба условия выполнены, ApplyPointsTrailing вызывается на каждом тике, обеспечивая непрерывное сопровождение. Далее реализуем удаление графических объектов при снятии программы с графика.
//+------------------------------------------------------------------+ //| Функция завершения работы советника | //+------------------------------------------------------------------+ void OnDeinit(const int code) { ObjectDelete(ChartID(), highLevelObj); //--- Удаляем верхний уровень ObjectDelete(ChartID(), lowLevelObj); //--- Удаляем нижний уровень ObjectDelete(ChartID(), highTextObj); //--- Удаляем подпись верхнего уровня ObjectDelete(ChartID(), lowTextObj); //--- Удаляем подпись нижнего уровня // Удаляем динамические объекты ObjectsDeleteAll(ChartID(), "ORB_Rectangle_", OBJ_RECTANGLE); //--- Удаляем прямоугольники ObjectsDeleteAll(ChartID(), "ORB_StartVLine_", OBJ_VLINE); //--- Удаляем вертикальные линии начала ObjectsDeleteAll(ChartID(), "ORB_EndVLine_", OBJ_VLINE); //--- Удаляем вертикальные линии окончания ObjectsDeleteAll(ChartID(), "ORB_StartTime_Text_", OBJ_TEXT); //--- Удаляем подписи времени начала ObjectsDeleteAll(ChartID(), "ORB_EndTime_Text_", OBJ_TEXT); //--- Удаляем подписи времени окончания ObjectsDeleteAll(ChartID(), "EntryMarker_", OBJ_ARROW); //--- Удаляем маркеры входа }
В обработчике OnDeinit, вызываемом при удалении программы с графика или завершении работы терминала, сначала удаляются четыре постоянных объекта: горизонтальные линии highLevelObj и lowLevelObj, а также текстовые подписи highTextObj и lowTextObj. Затем функция ObjectsDeleteAll удаляет все динамически созданные объекты текущей и предыдущих сессий. Такая очистка предотвращает накопление объектов после нескольких сессий или повторной загрузки графика. Ниже показан результат при включённом трейлинг-стопе.

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

Отчёт о бэктесте:

Заключение
В заключение мы разработали на MQL5 сессионную торговую систему Opening Range Breakout (ORB). Она позволяет задавать время начала сессии и продолжительность начального диапазона в минутах, автоматически определяет реальные максимум и минимум на выбранном таймфрейме, выявляет пробои с необязательным подтверждением по закрытию нескольких баров и открывает сделки только в направлении пробоя.
Предупреждение о рисках: статья предназначена исключительно для образовательных целей. Торговля связана со значительными финансовыми рисками, а рыночная волатильность может привести к убыткам. Перед использованием программы на реальном счёте необходимо провести тщательное бэктестирование и обеспечить надлежащее управление рисками.
Сессионная стратегия Opening Range Breakout позволяет торговать на внутридневных сетапах пробоя в любой выбранной торговой сессии и готова к дальнейшей оптимизации в рамках вашей торговой практики. Удачной торговли!
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/20339
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Алгоритм направления потока — Flow Direction Algorithm (FDA)
От начального до среднего уровня: Очереди, списки и деревья (IV)
VQC-фильтр с живым обучением прямо в MQL5 поверх графового fair-value скальпера
Изучение Стандартной библиотеки MQL5 (Часть 3): Советник на основе канала стандартного отклонения
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Здравствуйте. Спасибо за отзыв. Вы можете определить время в своём коде, используя местное время вместо времени брокера, или задать своё собственное. См. пример.
Местное время:
TimeLocal()Вы также можете использовать время, соответствующее настройкам вашего компьютера, через торговый сервер:
TimeTradeServer()Время по Гринвичу (GMT):
TimeGMT()Кроме того, вы можете задать собственные дату и время, как показано ниже:
Выбор оптимального подхода остается за вами. Спасибо.
Ваша проблема сформулирована недостаточно чётко. Но вы можете попробовать обширную библиотеку Local Timezones and Local Session Hours или более простую TimeServerDaylightSavings. Без корректировки времени вы не сможете надёжно протестировать свою стратегию на исторических данных, на которые обычно влияют переход на летнее время (DST) и смены часовых поясов. Или, возможно, вам нужна функция Determine Broker's Daylight (DST) schedule для онлайн-определения смены часовых поясов.
К сожалению, встроенный API MQL5 не предоставляет готового и более элегантного решения.