Автоматизация торговых стратегий в MQL5 (Часть 43): Адаптивная стратегия на основе канала линейной регрессии
Введение
В нашей предыдущей статье (Часть 42) мы разработали сессионную систему пробоя начального диапазона (ORB) на языке MetaQuotes Language 5 (MQL5), которая позволяла задавать время начала сессии и продолжительность начального диапазона в минутах, автоматически определяла фактические максимум и минимум на выбранном таймфрейме и открывала позиции только в направлении пробоя. В Части 43 мы разработаем адаптивную стратегию на основе канала линейной регрессии.
Система рассчитывает линию линейной регрессии с полосами стандартного отклонения за заданный пользователем период. Активируется она только тогда, когда абсолютное значение наклона превышает минимальный порог, подтверждая наличие выраженного тренда. Кроме того, канал автоматически перестраивается, если цена отклоняется от него более чем на заданный процент ширины канала, и открывает позиции при чётком пробое изнутри канала. Рассмотрим следующие темы:
К концу статьи у нас будет работоспособная программа MQL5, которая поддерживает динамический регрессионный канал с закрашенными зонами отклонения, выявляет пробои, закрывает позиции при пересечении средней линии и работает в режимах Normal и Inverse (обычном и обратном). Приступим!
Принцип работы адаптивного канала линейной регрессии
Стратегия на основе канала линейной регрессии применяет линию линейной регрессии, рассчитанную методом наименьших квадратов по заданному числу баров, чтобы определить направление и силу основного тренда. Затем выше и ниже линии регрессии добавляются параллельные полосы на заданном количестве стандартных отклонений, образующие верхнюю и нижнюю границы. Так формируется динамический ценовой канал, отражающий ожидаемый диапазон цены на трендовом рынке. Движение цены внутри канала указывает на продолжение тренда, а касания или небольшие выходы за его границы могут служить точками входа на откате, а значительные отклонения сигнализируют о возможном истощении движения или необходимости пересчитать регрессию по более свежим данным. Обычно мы покупаем, когда цена закрывается ниже нижней границы канала, и продаём, когда цена закрывается выше верхней.
В нашей реализации мы рассчитаем наклон и свободный член регрессии, а также стандартное отклонение за настраиваемый период. Канал будет создаваться только тогда, когда абсолютное значение наклона превышает минимальный порог и подтверждает направленное движение. Он будет привязан к самому старому бару периода и продлён до будущей точки на заданную долю своей продолжительности. Канал состоит из двух закрашенных цветных зон: розовой верхней половины и светло-зелёной нижней, а его верхняя, средняя и нижняя границы отображаются сплошными трендовыми линиями. На каждом новом баре канал либо продлевается на один бар вправо, если цена по-прежнему удерживается внутри канала, либо полностью перестраивается, если отклонение цены превышает заданную долю ширины канала.
Позиции будут открываться при чётком пробое изнутри канала с фиксированными уровнями Stop Loss и Take Profit в пипсах, ограничением максимального числа одновременно открытых позиций в каждом направлении и возможностью использовать режим Inverse. Все позиции одного направления немедленно закрываются при пересечении средней линии. Метки верхнего, среднего и нижнего уровней канала перемещаются вместе с его правым краем, а каждое открытие отмечается стрелкой. Режим Inverse меняет местами логику покупок и продаж, позволяя той же программе торговать сценарии возврата к среднему вместо торговли по продолжению тренда. Мы решили добавить этот вариант на случай, если понадобится торговать по противоположной логике. Ниже показана общая схема поставленных задач.

Реализация на MQL5
Чтобы создать программу на MQL5, откройте MetaEditor, перейдите в окно «Навигатор», найдите папку «Эксперты», нажмите «New» и следуйте указаниям мастера создания файла. После этого объявим входные параметры и глобальные переменные, которые понадобятся в программе.
//+------------------------------------------------------------------+ //| Linear Regression Channel 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> //+------------------------------------------------------------------+ //| Глобальные переменные | //+------------------------------------------------------------------+ CTrade obj_Trade; //--- Торговый объект //+------------------------------------------------------------------+ //| Перечисления | //+------------------------------------------------------------------+ enum TradeMode { // Определяем перечисление режимов торговли Normal, // Обычный режим Inverse // Инверсный режим }; //+------------------------------------------------------------------+ //| Входные параметры | //+------------------------------------------------------------------+ input int RegressionPeriod = 100; // Период расчёта регрессии input double Deviations = 2.0; // Множитель стандартного отклонения для канала input double MinSlopeThreshold = 0.00001; // Минимальный абсолютный наклон для выявления выраженного тренда input int UpdateThresholdPercent = 30; // Порог обновления в процентах от ширины канала (например, 30 означает 30%) input double ExtensionPercent = 50.0; // Начальное продление канала вправо в процентах от его длины input TradeMode TradeDirection = Normal; // Режим торговли input double Lots = 0.01; // Размер лота input int StopLossPips = 100; // Stop Loss в пипсах input int TakeProfitPips = 100; // Take Profit в пипсах input int MaxBuys = 2; // Максимум открытых позиций на покупку input int MaxSells = 2; // Максимум открытых позиций на продажу input int MagicNumber = 123456; // Магический номер позиций input int Slippage = 3; // Проскальзывание
Сначала подключим торговую библиотеку директивой «#include <Trade\Trade.mqh>». Она предоставляет класс CTrade для выполнения торговых операций и управления позициями. Глобально объявим объект obj_Trade класса CTrade, который будет использоваться во всей программе для отправки торговых запросов и изменения позиций.
Определим перечисление TradeMode с двумя вариантами: Normal для стандартной торговли на откатах к границам канала — покупки при снижении ниже нижней полосы в восходящем тренде и продажи при подъёме выше верхней полосы в нисходящем тренде — и Inverse для инвертирования сигналов и торговли сценариями возврата к среднему вместо продолжения тренда. Затем зададим входные параметры, которые можно менять непосредственно в свойствах программы. RegressionPeriod определяет число баров для расчёта линейной регрессии, Deviations — множитель стандартного отклонения, задающий ширину канала (обычно 2,0 для охвата примерно 95% наблюдений), а MinSlopeThreshold — минимальное абсолютное значение наклона, при котором рынок считается трендовым и создаётся канал. Назначение остальных параметров поясняется комментариями. Эти настройки позволяют управлять поведением канала, рисками и стилем торговли без изменения кода. Значения по умолчанию можно менять в любое время. После компиляции в окне свойств советника будут доступны следующие параметры.

После настройки входных параметров объявим дополнительные глобальные переменные, которые будут использоваться во всей программе.
//--- Глобальные переменные datetime lastBarTime = 0; //--- Время последнего бара double channelUpper, channelLower, channelMiddle; //--- Уровни канала string channelName = "LRC_Channel"; //--- Имя канала string upperLabelName = "LRC_Upper_Label"; //--- Имя верхней подписи string middleLabelName = "LRC_Middle_Label"; //--- Имя средней подписи string lowerLabelName = "LRC_Lower_Label"; //--- Имя нижней подписи bool hasValidChannel = false; //--- Флаг действующего канала datetime fixedTimeOld = 0; //--- Фиксированное время старого бара double slope_global = 0, intercept_global = 0, stdDev_global = 0; //--- Глобальные наклон, intercept (свободный член) и стандартное отклонение long period_sec = PeriodSeconds(_Period); //--- Продолжительность периода в секундах int arrowCounter = 0; //--- Счётчик стрелок double current_right_x = 0; //--- Текущая правая координата X
Далее объявим дополнительные глобальные переменные для поддержки адаптивной логики канала и его визуализации. lastBarTime хранит время последнего обработанного бара. Текущие расчётные уровни канала сохраняются в channelUpper, channelLower и channelMiddle, чтобы их можно было быстро использовать при проверке пробоев. Постоянные строки задают имена графических объектов: channelName содержит базовое имя LRC_Channel для составного объекта канала, а upperLabelName, middleLabelName и lowerLabelName — имена перемещаемых текстовых подписей соответствующих границ.
Логический тип hasValidChannel показывает, активен ли в данный момент корректный трендовый регрессионный канал. fixedTimeOld хранит временную привязку самого старого бара расчётного периода для согласованного вычисления координаты X. Рассчитанные параметры регрессии — slope_global, intercept_global и stdDev_global — сохраняются глобально, чтобы использовать их для проекций без повторного расчёта на каждом тике. period_sec получает продолжительность одного бара в секундах через «PeriodSeconds(_Period)», что обеспечивает точное преобразование времени в координату X. Целочисленная переменная arrowCounter позволяет создавать уникальные имена стрелок входа, а current_right_x хранит координату X правой точки канала при его побаровом продлении. Это важно, поскольку позволяет продлевать канал без полного перестроения и тем самым избегать его мерцания. Теперь создадим вспомогательные функции для его отрисовки.
//+-----------------------------------------------------------------------------------+ //| Создание канала линейной регрессии с заливкой и трендовыми линиями | //+-----------------------------------------------------------------------------------+ bool ChannelCreate(const long chart_ID, const string name, const int sub_window, datetime time1, datetime time2) { double price1_middle = intercept_global; //--- Средняя цена в первой точке double price2_middle = intercept_global + slope_global * current_right_x; //--- Средняя цена во второй точке double price1_upper = price1_middle + Deviations * stdDev_global; //--- Верхняя цена в первой точке double price2_upper = price2_middle + Deviations * stdDev_global; //--- Верхняя цена во второй точке double price1_lower = price1_middle - Deviations * stdDev_global; //--- Нижняя цена в первой точке double price2_lower = price2_middle - Deviations * stdDev_global; //--- Нижняя цена во второй точке // Канал с заливкой между верхней и средней линиями string um_name = name + "_um"; //--- Имя верхней половины канала if (!ObjectCreate(chart_ID, um_name, OBJ_CHANNEL, sub_window, time1, price1_upper, time2, price2_upper, time1, price1_middle)) { //--- Создаём верхне-средний канал Print(__FUNCTION__, ": failed to create upper-middle channel! Error code = ", GetLastError()); //--- Записываем ошибку в журнал return(false); //--- Возвращаем ошибку } ObjectSetInteger(chart_ID, um_name, OBJPROP_COLOR, clrPink); //--- Задаём цвет ObjectSetInteger(chart_ID, um_name, OBJPROP_STYLE, STYLE_SOLID); //--- Задаём стиль ObjectSetInteger(chart_ID, um_name, OBJPROP_WIDTH, 1); //--- Задаём ширину ObjectSetInteger(chart_ID, um_name, OBJPROP_FILL, true); //--- Включаем заливку ObjectSetInteger(chart_ID, um_name, OBJPROP_BACK, true); //--- Размещаем на заднем плане ObjectSetInteger(chart_ID, um_name, OBJPROP_SELECTABLE, true); //--- Разрешаем выделение ObjectSetInteger(chart_ID, um_name, OBJPROP_SELECTED, false); //--- Снимаем выделение ObjectSetInteger(chart_ID, um_name, OBJPROP_RAY_RIGHT, false); //--- Отключаем луч ObjectSetInteger(chart_ID, um_name, OBJPROP_HIDDEN, false); //--- Оставляем объект видимым ObjectSetInteger(chart_ID, um_name, OBJPROP_ZORDER, 0); //--- Задаём порядок отображения // Канал с заливкой между средней и нижней линиями string ml_name = name + "_ml"; //--- Имя нижней половины канала if (!ObjectCreate(chart_ID, ml_name, OBJ_CHANNEL, sub_window, time1, price1_middle, time2, price2_middle, time1, price1_lower)) { //--- Создаём средне-нижний канал Print(__FUNCTION__, ": failed to create middle-lower channel! Error code = ", GetLastError()); //--- Записываем ошибку в журнал return(false); //--- Возвращаем ошибку } ObjectSetInteger(chart_ID, ml_name, OBJPROP_COLOR, clrLightGreen); //--- Задаём цвет ObjectSetInteger(chart_ID, ml_name, OBJPROP_STYLE, STYLE_SOLID); //--- Задаём стиль ObjectSetInteger(chart_ID, ml_name, OBJPROP_WIDTH, 1); //--- Задаём ширину ObjectSetInteger(chart_ID, ml_name, OBJPROP_FILL, true); //--- Включаем заливку ObjectSetInteger(chart_ID, ml_name, OBJPROP_BACK, true); //--- Размещаем на заднем плане ObjectSetInteger(chart_ID, ml_name, OBJPROP_SELECTABLE, true); //--- Разрешаем выделение ObjectSetInteger(chart_ID, ml_name, OBJPROP_SELECTED, false); //--- Снимаем выделение ObjectSetInteger(chart_ID, ml_name, OBJPROP_RAY_RIGHT, false); //--- Отключаем луч ObjectSetInteger(chart_ID, ml_name, OBJPROP_HIDDEN, false); //--- Оставляем объект видимым ObjectSetInteger(chart_ID, ml_name, OBJPROP_ZORDER, 0); //--- Задаём порядок отображения // Верхняя трендовая линия string upper_name = name + "_upper"; //--- Имя верхней линии if (!ObjectCreate(chart_ID, upper_name, OBJ_TREND, sub_window, time1, price1_upper, time2, price2_upper)) { //--- Создаём верхнюю трендовую линию Print(__FUNCTION__, ": failed to create upper trendline! Error code = ", GetLastError()); //--- Записываем ошибку в журнал return(false); //--- Возвращаем ошибку } ObjectSetInteger(chart_ID, upper_name, OBJPROP_COLOR, clrRed); //--- Задаём цвет ObjectSetInteger(chart_ID, upper_name, OBJPROP_STYLE, STYLE_SOLID); //--- Задаём стиль ObjectSetInteger(chart_ID, upper_name, OBJPROP_WIDTH, 1); //--- Задаём ширину ObjectSetInteger(chart_ID, upper_name, OBJPROP_BACK, false); //--- Размещаем на переднем плане ObjectSetInteger(chart_ID, upper_name, OBJPROP_SELECTABLE, true); //--- Разрешаем выделение ObjectSetInteger(chart_ID, upper_name, OBJPROP_SELECTED, false); //--- Снимаем выделение ObjectSetInteger(chart_ID, upper_name, OBJPROP_RAY_RIGHT, false); //--- Отключаем луч ObjectSetInteger(chart_ID, upper_name, OBJPROP_HIDDEN, false); //--- Оставляем объект видимым ObjectSetInteger(chart_ID, upper_name, OBJPROP_ZORDER, 1); //--- Задаём порядок отображения // Средняя трендовая линия string middle_name = name + "_middle"; //--- Имя средней линии if (!ObjectCreate(chart_ID, middle_name, OBJ_TREND, sub_window, time1, price1_middle, time2, price2_middle)) { //--- Создаём среднюю трендовую линию Print(__FUNCTION__, ": failed to create middle trendline! Error code = ", GetLastError()); //--- Записываем ошибку в журнал return(false); //--- Возвращаем ошибку } ObjectSetInteger(chart_ID, middle_name, OBJPROP_COLOR, clrBlue); //--- Задаём цвет ObjectSetInteger(chart_ID, middle_name, OBJPROP_STYLE, STYLE_SOLID); //--- Задаём стиль ObjectSetInteger(chart_ID, middle_name, OBJPROP_WIDTH, 1); //--- Задаём ширину ObjectSetInteger(chart_ID, middle_name, OBJPROP_BACK, false); //--- Размещаем на переднем плане ObjectSetInteger(chart_ID, middle_name, OBJPROP_SELECTABLE, true); //--- Разрешаем выделение ObjectSetInteger(chart_ID, middle_name, OBJPROP_SELECTED, false); //--- Снимаем выделение ObjectSetInteger(chart_ID, middle_name, OBJPROP_RAY_RIGHT, false); //--- Отключаем луч ObjectSetInteger(chart_ID, middle_name, OBJPROP_HIDDEN, false); //--- Оставляем объект видимым ObjectSetInteger(chart_ID, middle_name, OBJPROP_ZORDER, 1); //--- Задаём порядок отображения // Нижняя трендовая линия string lower_name = name + "_lower"; //--- Имя нижней линии if (!ObjectCreate(chart_ID, lower_name, OBJ_TREND, sub_window, time1, price1_lower, time2, price2_lower)) { //--- Создаём нижнюю трендовую линию Print(__FUNCTION__, ": failed to create lower trendline! Error code = ", GetLastError()); //--- Записываем ошибку в журнал return(false); //--- Возвращаем ошибку } ObjectSetInteger(chart_ID, lower_name, OBJPROP_COLOR, clrGreen); //--- Задаём цвет ObjectSetInteger(chart_ID, lower_name, OBJPROP_STYLE, STYLE_SOLID); //--- Задаём стиль ObjectSetInteger(chart_ID, lower_name, OBJPROP_WIDTH, 1); //--- Задаём ширину ObjectSetInteger(chart_ID, lower_name, OBJPROP_BACK, false); //--- Размещаем на переднем плане ObjectSetInteger(chart_ID, lower_name, OBJPROP_SELECTABLE, true); //--- Разрешаем выделение ObjectSetInteger(chart_ID, lower_name, OBJPROP_SELECTED, false); //--- Снимаем выделение ObjectSetInteger(chart_ID, lower_name, OBJPROP_RAY_RIGHT, false); //--- Отключаем луч ObjectSetInteger(chart_ID, lower_name, OBJPROP_HIDDEN, false); //--- Оставляем объект видимым ObjectSetInteger(chart_ID, lower_name, OBJPROP_ZORDER, 1); //--- Задаём порядок отображения return(true); //--- Возвращаем успешный результат } //+------------------------------------------------------------------+ //| Удаление канала | //+------------------------------------------------------------------+ bool ChannelDelete(const long chart_ID, const string name) { bool success = true; //--- Инициализируем признак успеха if (!ObjectDelete(chart_ID, name + "_um")) { //--- Удаляем верхне-средний канал Print(__FUNCTION__, ": failed to delete um! Error code = ", GetLastError()); //--- Записываем ошибку в журнал success = false; //--- Фиксируем ошибку } if (!ObjectDelete(chart_ID, name + "_ml")) { //--- Удаляем средне-нижний канал Print(__FUNCTION__, ": failed to delete ml! Error code = ", GetLastError()); //--- Записываем ошибку в журнал success = false; //--- Фиксируем ошибку } if (!ObjectDelete(chart_ID, name + "_upper")) { //--- Удаляем верхнюю линию Print(__FUNCTION__, ": failed to delete upper! Error code = ", GetLastError()); //--- Записываем ошибку в журнал success = false; //--- Фиксируем ошибку } if (!ObjectDelete(chart_ID, name + "_middle")) { //--- Удаляем среднюю линию Print(__FUNCTION__, ": failed to delete middle! Error code = ", GetLastError()); //--- Записываем ошибку в журнал success = false; //--- Фиксируем ошибку } if (!ObjectDelete(chart_ID, name + "_lower")) { //--- Удаляем нижнюю линию Print(__FUNCTION__, ": failed to delete lower! Error code = ", GetLastError()); //--- Записываем ошибку в журнал success = false; //--- Фиксируем ошибку } return(success); //--- Возвращаем успешный результат }
Здесь реализуется функция ChannelCreate, которая строит графический канал линейной регрессии с помощью сочетания закрашенных объектов каналов и трендовых линий. Такой подход позволяет получить цветные зоны и чёткие границы, не ограничиваясь одним встроенным объектом канала. Как вы могли заметить, визуализации уделяется особое внимание, чтобы наглядно представить работу системы. Сначала по сохранённым глобальным параметрам регрессии рассчитываются точные ценовые координаты обоих концов канала: левая точка средней линии равна intercept_global, правая — intercept_global + slope_global * current_right_x, а верхние и нижние точки получаются прибавлением или вычитанием Deviations * stdDev_global.
Для создания закрашенных зон сначала формируется верхне-средняя часть. К базовому имени добавляется суффикс _um, после чего функция ObjectCreate с типом OBJ_CHANNEL строит трёхточечный канал от левой верхней точки к правой верхней и затем к левой средней. Для него задаются розовый цвет, сплошной стиль, ширина 1, заливка, размещение на заднем плане (объект можно выделить, но изначально он не выделен), отключённый правый луч, видимость в списке объектов и порядок отображения 0. Затем аналогично создаётся средне-нижняя часть с суффиксом _ml: объект OBJ_CHANNEL строится от левой средней точки к правой средней и левой нижней и окрашивается в светло-зелёный цвет. Так получается двухцветная заливка канала.
После этого три видимые границы рисуются отдельными трендовыми линиями для большей чёткости. Верхняя линия с суффиксом _upper создаётся как OBJ_TREND от левой верхней точки к правой верхней и получает красный цвет, сплошной стиль, ширину 1, размещение на переднем плане, возможность выделения, отключённый луч и порядок отображения 1. Средняя линия с суффиксом _middle создаётся синим цветом, а нижняя с суффиксом _lower — зелёным. Разделение заливки и линий обеспечивает одновременно цветные зоны и чёткие границы, которые хорошо видны даже при движении цены внутри канала. Если какой-либо вызов ObjectCreate завершается ошибкой, её код записывается в журнал с помощью GetLastError, после чего функция возвращает false. При успешном создании всех объектов возвращается true.
Также создаётся вспомогательная функция ChannelDelete, которая удаляет все пять компонентов при перестроении или сбросе канала. Она последовательно пытается удалить объекты с суффиксами _um, _ml, _upper, _middle и _lower, записывает ошибки в журнал, но продолжает работу, а затем возвращает true, если удаление прошло успешно, и false при наличии ошибки. Для этого используется функция ObjectDelete. Эти две функции позволяют полностью перестраивать графический канал при значительном выходе цены за его границы или формировании нового тренда. Далее добавим стрелки сигналов и обновление подписей канала.
//+------------------------------------------------------------------+ //| Отрисовка стрелки сигнала на графике | //+------------------------------------------------------------------+ void DrawArrow(bool isBuy, datetime time, double price) { string name = "SignalArrow_" + IntegerToString(arrowCounter++); //--- Имя стрелки ObjectCreate(0, name, OBJ_ARROW, 0, time, price); //--- Создаём стрелку ObjectSetInteger(0, name, OBJPROP_ARROWCODE, isBuy ? 233 : 234); //--- Задаём код ObjectSetInteger(0, name, OBJPROP_COLOR, isBuy ? clrGreen : clrRed); //--- Задаём цвет ObjectSetInteger(0, name, OBJPROP_WIDTH, 2); //--- Задаём ширину ObjectSetInteger(0, name, OBJPROP_ANCHOR, isBuy ? ANCHOR_TOP : ANCHOR_BOTTOM); //--- Задаём точку привязки ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false); //--- Запрещаем выделение ChartRedraw(0); //--- Перерисовываем график } //+------------------------------------------------------------------+ //| Обновление подписей канала | //+------------------------------------------------------------------+ void UpdateLabels(datetime labelTime) { double label_x = (double)(labelTime - fixedTimeOld) / period_sec; //--- Рассчитываем координату X подписи double middlePrice = intercept_global + slope_global * label_x; //--- Рассчитываем среднюю линию double upperPrice = middlePrice + Deviations * stdDev_global; //--- Рассчитываем верхнюю линию double lowerPrice = middlePrice - Deviations * stdDev_global; //--- Рассчитываем нижнюю линию // Верхняя подпись if (ObjectFind(0, upperLabelName) < 0) { //--- Проверяем отсутствие верхней подписи ObjectCreate(0, upperLabelName, OBJ_TEXT, 0, labelTime, upperPrice); //--- Создаём верхнюю подпись } else { //--- Объект существует ObjectMove(0, upperLabelName, 0, labelTime, upperPrice); //--- Перемещаем верхнюю подпись } ObjectSetString(0, upperLabelName, OBJPROP_TEXT, "Upper Channel"); //--- Задаём текст ObjectSetInteger(0, upperLabelName, OBJPROP_COLOR, clrRed); //--- Задаём цвет ObjectSetInteger(0, upperLabelName, OBJPROP_ANCHOR, ANCHOR_LEFT_UPPER); //--- Задаём точку привязки ObjectSetInteger(0, upperLabelName, OBJPROP_SELECTABLE, false); //--- Запрещаем выделение // Средняя подпись if (ObjectFind(0, middleLabelName) < 0) { //--- Проверяем отсутствие средней подписи ObjectCreate(0, middleLabelName, OBJ_TEXT, 0, labelTime, middlePrice); //--- Создаём среднюю подпись } else { //--- Объект существует ObjectMove(0, middleLabelName, 0, labelTime, middlePrice); //--- Перемещаем среднюю подпись } ObjectSetString(0, middleLabelName, OBJPROP_TEXT, "Middle Channel"); //--- Задаём текст ObjectSetInteger(0, middleLabelName, OBJPROP_COLOR, clrBlue); //--- Задаём цвет ObjectSetInteger(0, middleLabelName, OBJPROP_ANCHOR, ANCHOR_LEFT); //--- Задаём точку привязки ObjectSetInteger(0, middleLabelName, OBJPROP_SELECTABLE, false); //--- Запрещаем выделение // Нижняя подпись if (ObjectFind(0, lowerLabelName) < 0) { //--- Проверяем отсутствие нижней подписи ObjectCreate(0, lowerLabelName, OBJ_TEXT, 0, labelTime, lowerPrice); //--- Создаём нижнюю подпись } else { //--- Объект существует ObjectMove(0, lowerLabelName, 0, labelTime, lowerPrice); //--- Перемещаем нижнюю подпись } ObjectSetString(0, lowerLabelName, OBJPROP_TEXT, "Lower Channel"); //--- Задаём текст ObjectSetInteger(0, lowerLabelName, OBJPROP_COLOR, clrGreen); //--- Задаём цвет ObjectSetInteger(0, lowerLabelName, OBJPROP_ANCHOR, ANCHOR_LEFT_LOWER); //--- Задаём точку привязки ObjectSetInteger(0, lowerLabelName, OBJPROP_SELECTABLE, false); //--- Запрещаем выделение ChartRedraw(0); //--- Перерисовываем график }
Реализуем функцию DrawArrow, которая размещает на графике заметный маркер при появлении торгового сигнала. Уникальное имя объекта формируется из строки SignalArrow_ и увеличиваемого счётчика arrowCounter, после чего в заданных координатах времени и цены создаётся объект OBJ_ARROW. Для сигналов на покупку используется символ Wingdings с кодом 233, а для продаж — код 234. MQL5 предоставляет коды символов Wingdings; ниже показана таблица, по которой можно выбрать и подставить нужные варианты. Стрелки покупок окрашиваются в зелёный, продаж — в красный; ширина устанавливается равной 2. Для покупки используется верхняя точка привязки, чтобы стрелка отображалась под ценой, а для продажи — нижней, чтобы отображаться над ценой. Объект нельзя выделить, после чего график принудительно перерисовывается.

Далее создадим функцию UpdateLabels, которая удерживает поясняющие текстовые подписи у текущего правого края канала и плавно перемещает их при продлении или перестроении. Сначала координата X для labelTime рассчитывается как разница между labelTime и fixedTimeOld, делённая на period_sec, после чего по сохранённым параметрам регрессии вычисляются точные цены верхней, средней и нижней границ в этой точке.
Для каждой из трёх подписей — верхней, средней и нижней — функция ObjectFind проверяет, существует ли объект. Если нет, создаётся новый объект OBJ_TEXT в заданных координатах времени и цены; если объект уже существует, он перемещается функцией ObjectMove. Подписям присваиваются тексты Upper Channel, Middle Channel и Lower Channel, соответствующие цвета — красный, синий и зелёный — и соответствующие точки привязки: к левому верхнему краю, к левому краю и к левому нижнему краю. Объекты нельзя выделять. Благодаря этому подписи всегда остаются у правого края канала и обновляются без создания дубликатов.
//+------------------------------------------------------------------+ //| Создание канала при наличии выраженного тренда | //+------------------------------------------------------------------+ void CreateChannelIfTrend() { if (Bars(_Symbol, _Period) < RegressionPeriod + 1) return; //--- Выходим при недостаточном числе баров double closeArray[]; //--- Массив цен закрытия ArraySetAsSeries(closeArray, true); //--- Устанавливаем индексацию как в таймсерии if (CopyClose(_Symbol, _Period, 1, RegressionPeriod, closeArray) != RegressionPeriod) return; //--- Копируем цены закрытия или выходим double sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0; //--- Инициализируем суммы int n = RegressionPeriod; //--- Задаём число наблюдений for (int i = 0; i < n; i++) { //--- Перебираем позиции double x = (double)(n - 1 - i); //--- Рассчитываем X double y = closeArray[i]; //--- Получаем значение Y sumX += x; //--- Добавляем x sumY += y; //--- Добавляем y sumXY += x * y; //--- Добавляем xy sumX2 += x * x; //--- Добавляем x2 } double slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX); //--- Рассчитываем наклон // Проверяем наличие выраженного тренда if (MathAbs(slope) < MinSlopeThreshold) { //--- Проверяем отсутствие тренда hasValidChannel = false; //--- Сбрасываем канал ChannelDelete(0, channelName); //--- Удаляем канал ObjectDelete(0, upperLabelName); //--- Удаляем верхнюю подпись ObjectDelete(0, middleLabelName); //--- Удаляем среднюю подпись ObjectDelete(0, lowerLabelName); //--- Удаляем нижнюю подпись return; //--- Выходим } double intercept = (sumY - slope * sumX) / n; //--- Рассчитываем свободный член // Рассчитываем стандартное отклонение double sumRes2 = 0; //--- Инициализируем сумму квадратов остатков for (int i = 0; i < n; i++) { //--- Перебираем позиции double x = (double)(n - 1 - i); //--- Рассчитываем X double predicted = intercept + slope * x; //--- Рассчитываем прогнозное значение double res = closeArray[i] - predicted; //--- Рассчитываем остаток sumRes2 += res * res; //--- Добавляем res2 } double variance = sumRes2 / (n - 2); //--- Рассчитываем дисперсию double stdDev = MathSqrt(variance); //--- Рассчитываем стандартное отклонение // Сохраняем значения для проекции slope_global = slope; //--- Сохраняем глобальный наклон intercept_global = intercept; //--- Сохраняем глобальный свободный член stdDev_global = stdDev; //--- Сохраняем глобальное стандартное отклонение // Фиксированные опорные точки с начальным продлением (будущее time2) fixedTimeOld = iTime(_Symbol, _Period, RegressionPeriod); //--- Задаём время левой точки datetime fixedTimeNew = iTime(_Symbol, _Period, 1); //--- Задаём время правой точки long channel_sec = fixedTimeNew - fixedTimeOld; //--- Рассчитываем длительность канала в секундах long extension_sec = (long)(channel_sec * (ExtensionPercent / 100.0)); //--- Рассчитываем продление datetime time_extended = fixedTimeNew + (datetime)extension_sec; //--- Рассчитываем продлённое время current_right_x = (double)(time_extended - fixedTimeOld) / period_sec; //--- Рассчитываем правую координату X // Удаляем старый канал и создаём новый ChannelDelete(0, channelName); //--- Удаляем канал if (!ChannelCreate(0, channelName, 0, fixedTimeOld, time_extended)) return; //--- Создаём канал или выходим hasValidChannel = true; //--- Устанавливаем признак действующего канала double channelWidth = 2 * Deviations * stdDev_global; //--- Рассчитываем ширину double channelWidthPoints = channelWidth / _Point; //--- Рассчитываем ширину в пунктах Print("Channel created: slope=" + DoubleToString(slope, 8) + ", range=" + DoubleToString(channelWidth, _Digits) + " (" + DoubleToString(channelWidthPoints, 0) + " points), times: " + TimeToString(fixedTimeOld) + " to " + TimeToString(time_extended)); //--- Записываем данные канала в журнал UpdateLabels(time_extended); //--- Обновляем подписи ChartRedraw(0); //--- Перерисовываем график }
Здесь реализуется функция CreateChannelIfTrend, которая выполняет полный расчёт линейной регрессии и решает, следует ли построить или обновить канал. Это позволяет отображать канал только при наличии выраженного тренда. Сначала проверяется, достаточно ли исторических баров — не менее RegressionPeriod + 1. Затем создаётся массив closeArray, размером RegressionPeriod, для него включается индексация как у временного ряда с помощью ArraySetAsSeries, и функцией CopyClose копируются цены закрытия ровно за RegressionPeriod баров начиная со сдвига 1. При ошибке копирования функция завершает работу.
Инициализируются суммы для формулы наименьших квадратов, после чего выполняется цикл: for каждого бара i значение x задаётся как n - 1 - i, поэтому самый старый бар получает x = n - 1, а самый новый — x = 0; y соответствует цене закрытия. Затем накапливаются sumX, sumY, sumXY и sumX2. Это необходимо для расчёта наклона. Наклон рассчитывается по стандартной формуле. Если его абсолютное значение меньше MinSlopeThreshold, канал признаётся недействительным: hasValidChannel устанавливается в false, существующий канал и подписи удаляются функцией ObjectDelete, после чего выполняется выход. Такая проверка исключает бессмысленные горизонтальные каналы на боковом рынке, однако её можно убрать, если требуется торговать во флэте.
Если наклон достаточен, свободный член рассчитывается как (sumY - slope × sumX) / n. Затем определяется стандартное отклонение: во втором цикле для каждого x вычисляется расчётная цена, квадрат отклонения фактического закрытия от неё добавляется к сумме, результат делится на n - 2 для получения variance, после чего извлекается квадратный корень. Наклон, свободный член и стандартное отклонение сохраняются в глобальных переменных для последующих проекций. Левая граница канала привязывается к времени бара со сдвигом RegressionPeriod через iTime, правая базовая граница — к последнему завершённому бару со сдвигом 1. Затем рассчитывается длительность канала в секундах, и правая граница продлевается на ExtensionPercent процентов. current_right_x получает соответствующую X-координату продлённой правой точки.
Предыдущий канал удаляется функцией ChannelDelete, после чего ChannelCreate строит новый составной канал от fixedTimeOld до продлённой правой точки. При успехе hasValidChannel устанавливается в true, в журнал выводятся параметры нового канала — наклон, ширина в цене и пунктах и временной диапазон, — подписи перемещаются к правому краю, а график перерисовывается. Теперь вызовем эту функцию в обработчике OnInit, чтобы построить первый канал при наличии достаточного объёма данных.
//+------------------------------------------------------------------+ //| Функция инициализации советника | //+------------------------------------------------------------------+ int OnInit() { obj_Trade.SetExpertMagicNumber(MagicNumber); //--- Задаём магический номер CreateChannelIfTrend(); //--- Первая попытка return(INIT_SUCCEEDED); //--- Возвращаем успешный результат } //+------------------------------------------------------------------+ //| Функция деинициализации советника | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { ChannelDelete(0, channelName); //--- Удаляем канал ObjectDelete(0, upperLabelName); //--- Удаляем верхнюю подпись ObjectDelete(0, middleLabelName); //--- Удаляем среднюю подпись ObjectDelete(0, lowerLabelName); //--- Удаляем нижнюю подпись }
В обработчике OnInit сначала объекту obj_Trade присваивается MagicNumber с помощью SetExpertMagicNumber, чтобы все открытые программой позиции можно было корректно идентифицировать и управлять ими. Затем вызывается CreateChannelIfTrend для построения канала по последним завершённым барам. После этого возвращается INIT_SUCCEEDED, подтверждающий успешную инициализацию.
В функции OnDeinit выполняется полная очистка: ChannelDelete удаляет все пять компонентов регрессионного канала — две закрашенные зоны и три трендовые линии, — а затем функцией ObjectDelete отдельно удаляются три перемещаемые подписи upperLabelName, middleLabelName и lowerLabelName. Благодаря этому после остановки программы на графике не остаются лишние объекты. После компиляции и запуска получаем следующий результат.

На изображении видно, что канал создаётся при наличии тренда и достаточного числа баров для расчёта. Теперь можно перейти к управлению каналом и его обновлению по мере появления новых баров в обработчике тиков. Чтобы не перегружать программу, выполнять эти действия будем только на новых барах.
//+------------------------------------------------------------------+ //| Проверка открытия нового бара | //+------------------------------------------------------------------+ bool IsNewBar() { datetime currentBarTime = iTime(_Symbol, _Period, 0); //--- Получаем текущее время if (currentBarTime != lastBarTime) { //--- Проверяем новый бар lastBarTime = currentBarTime; //--- Обновляем время последнего бара return true; //--- Возвращаем true } return false; //--- Возвращаем false }
Здесь определяется функция IsNewBar, которая проверяет формирование нового бара на текущем таймфрейме. Благодаря этому основная расчётная и торговая логика выполняется один раз на бар, а не на каждом тике. Время открытия текущего бара со сдвигом 0 получается функцией iTime для текущего символа и таймфрейма и сохраняется в currentBarTime. Затем оно сравнивается с глобальной переменной lastBarTime. Если значения различаются, появился новый бар: lastBarTime обновляется, а функция возвращает true. В противном случае возвращается false. Далее используем её в обработчике OnTick при формировании нового завершённого бара.
//+------------------------------------------------------------------+ //| Тиковая функция советника | //+------------------------------------------------------------------+ void OnTick() { // Проверка нового бара if (!IsNewBar()) return; //--- Выходим, если нового бара нет // Если канала нет, пытаемся создать его на каждом новом баре if (!hasValidChannel) { //--- Проверяем отсутствие канала CreateChannelIfTrend(); //--- Создаём канал при наличии тренда if (!hasValidChannel) return; //--- Выходим, если канал не создан } // Вручную рассчитываем проекцию для завершённого бара (сдвиг 1) datetime previousTime = iTime(_Symbol, _Period, 1); //--- Получаем время предыдущего бара int x = Bars(_Symbol, _Period, fixedTimeOld, previousTime) - 1; //--- Рассчитываем X channelMiddle = intercept_global + slope_global * x; //--- Рассчитываем среднюю линию channelUpper = channelMiddle + Deviations * stdDev_global; //--- Рассчитываем верхнюю линию channelLower = channelMiddle - Deviations * stdDev_global; //--- Рассчитываем нижнюю линию // Рассчитываем проекцию для сдвига 2 datetime time2 = iTime(_Symbol, _Period, 2); //--- Получаем время второго предыдущего бара if (time2 <= fixedTimeOld) return; //--- Выходим при некорректных данных int x2 = Bars(_Symbol, _Period, fixedTimeOld, time2) - 1; //--- Рассчитываем X для второго бара double middle2 = intercept_global + slope_global * x2; //--- Рассчитываем среднюю линию для второго бара double upper2 = middle2 + Deviations * stdDev_global; //--- Рассчитываем верхнюю линию для второго бара double lower2 = middle2 - Deviations * stdDev_global; //--- Рассчитываем нижнюю линию для второго бара // Получаем цены закрытия double closePrevious = iClose(_Symbol, _Period, 1); //--- Получаем закрытие предыдущего бара double close2 = iClose(_Symbol, _Period, 2); //--- Получаем закрытие второго предыдущего бара if (closePrevious == 0 || close2 == 0) return; //--- Выходим при некорректных данных // Проверяем выход за правую границу канала datetime current_time2 = (datetime)ObjectGetInteger(0, channelName + "_middle", OBJPROP_TIME, 1); //--- Получаем текущее время правой точки if (previousTime > current_time2) { //--- Проверяем выход за пределы Print("Bars beyond channel end: previousTime=" + TimeToString(previousTime) + ", current_time2=" + TimeToString(current_time2) + " - recreating channel"); //--- Записываем перестроение в журнал CreateChannelIfTrend(); //--- Перестраиваем канал return; //--- Выходим } }
В функции OnTick сначала вызывается IsNewBar. Если нового бара нет, функция немедленно завершает работу, чтобы не выполнять логику несколько раз внутри одной свечи и синхронизировать расчёты только с завершёнными барами. Если hasValidChannel равен false, вызывается CreateChannelIfTrend; если канал по-прежнему не создан, функция завершается: система пытается восстановить канал при появлении подходящего тренда.
После подтверждения действующего канала вручную рассчитываются его уровни для только что завершившегося бара со сдвигом 1. Его время получается функцией iTime и сохраняется в previousTime. Точная координата X относительно фиксированной левой привязки определяется функцией Bars между fixedTimeOld и previousTime минус 1, после чего вычисляются средняя, верхняя и нижняя границы. Так получаются точные положения канала на момент закрытия предыдущего бара. Аналогичный расчёт выполняется для бара со сдвигом 2, чтобы получить close2 и соответствующие уровни middle2, upper2 и lower2 для выявления пересечений изнутри наружу.
Наконец выполняется защитная проверка. Текущее время правой точки средней трендовой линии channelName + "_middle" считывается функцией ObjectGetInteger со свойством OBJPROP_TIME и индексом 1 в current_time2. Если время предыдущего бара уже превышает эту точку, предыдущий бар вышел за правую временную границу текущего канала. В журнал записывается сообщение, вызывается CreateChannelIfTrend для немедленного перестроения, после чего выполняется выход. Это не позволяет каналам устаревать.

Пока всё работает корректно: при подтверждённом тренде канал обновляется на новых барах. Теперь можно перейти к выявлению пробоев и в зависимости от ситуации продлевать или перестраивать канал. Ниже приведён соответствующий фрагмент кода.
// Проверяем значительный выход за канал для его перестроения double channelWidth = channelUpper - channelLower; //--- Рассчитываем ширину double channelWidthPoints = channelWidth / _Point; //--- Рассчитываем ширину в пунктах double updateThreshold = UpdateThresholdPercent / 100.0; //--- Рассчитываем порог double deviation = MathMax(closePrevious - channelUpper, channelLower - closePrevious); //--- Рассчитываем отклонение double deviationPercent = (deviation / channelWidth) * 100; //--- Рассчитываем процент if (deviation > updateThreshold * channelWidth) { //--- Проверяем пробой Print("Breakout detected - deviation: " + DoubleToString(deviation, _Digits) + " (" + DoubleToString(deviationPercent, 2) + "%), threshold: " + DoubleToString(updateThreshold * channelWidth, _Digits) + " (" + IntegerToString(UpdateThresholdPercent) + "%) - recreating channel"); //--- Записываем пробой в журнал CreateChannelIfTrend(); //--- Перестраиваем канал return; //--- Выходим } else { //--- Пробой отсутствует // Продлеваем вправо на один бар, если цена остаётся внутри канала datetime new_time2 = current_time2 + (datetime)period_sec; //--- Рассчитываем новое time2 current_right_x += 1.0; //--- Увеличиваем x double new_price_middle = intercept_global + slope_global * current_right_x; //--- Рассчитываем новую среднюю линию double new_price_upper = new_price_middle + Deviations * stdDev_global; //--- Рассчитываем новую верхнюю линию double new_price_lower = new_price_middle - Deviations * stdDev_global; //--- Рассчитываем новую нижнюю линию // Перемещаем каналы ObjectMove(0, channelName + "_um", 1, new_time2, new_price_upper); //--- Перемещаем верхнюю половину канала ObjectMove(0, channelName + "_ml", 1, new_time2, new_price_middle); //--- Перемещаем нижнюю половину канала // Перемещаем трендовые линии ObjectMove(0, channelName + "_upper", 1, new_time2, new_price_upper); //--- Перемещаем верхнюю линию ObjectMove(0, channelName + "_middle", 1, new_time2, new_price_middle); //--- Перемещаем среднюю линию ObjectMove(0, channelName + "_lower", 1, new_time2, new_price_lower); //--- Перемещаем нижнюю линию UpdateLabels(new_time2); //--- Обновляем подписи ChartRedraw(0); //--- Перерисовываем график }
Теперь реализуем адаптивное поведение канала: он либо продлевается побарово, либо полностью перестраивается при значительном выходе цены за текущую регрессию. Сначала рассчитывается полная ширина канала как расстояние между channelUpper и channelLower и переводится в пункты для журнала. Затем определяется отклонение закрытия предыдущего бара от ближайшей границы с помощью функции MathMax, после чего оно выражается в процентах от полной ширины канала. Если оно превышает порог — по умолчанию 30% ширины, — это считается существенным пробоем или признаком истощения движения. Событие записывается в журнал, и CreateChannelIfTrend полностью пересчитывает канал по последним данным, адаптируя наклон, ширину и положение к новому ценовому режиму.
Если цена остаётся в допустимых пределах и отклонение не превышает порог, существующий канал продлевается на один бар вправо. Время правой точки увеличивается на один период бара: current_time2 + period_sec сохраняется в new_time2, current_right_x увеличивается на 1,0, а новые правые ценовые координаты средней, верхней и нижней линий рассчитываются по сохранённым параметрам регрессии. Затем ObjectMove перемещает правые точки обоих закрашенных каналов и трёх трендовых линий на новые время и цены. Подписи обновляются у новой правой границы, график перерисовывается. Таким образом, канал плавно продлевается без мерцания и полного перестроения, пока цена сохраняет связь с текущим трендом.

Теперь канал полностью управляется, поэтому можно формировать сигналы при выходе цены за его границы. Одновременно будем закрывать существующие позиции при пересечении средней линии. Это дополнительная логика управления, которую можно убрать, если позиции должны закрываться только по Stop Loss или Take Profit. В следующем блоке кода выполняется именно эта задача.
// Подсчитываем открытые позиции int buyCount = 0, sellCount = 0; //--- Инициализируем счётчики int total = PositionsTotal(); //--- Получаем общее число позиций for (int i = total - 1; i >= 0; i--) { //--- Перебираем в обратном порядке ulong ticket = PositionGetTicket(i); //--- Получаем тикет if (PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == MagicNumber) { //--- Проверяем символ и магический номер if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { //--- Проверяем позицию на покупку buyCount++; //--- Увеличиваем счётчик покупок } else if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) { //--- Проверяем позицию на продажу sellCount++; //--- Увеличиваем счётчик продаж } } } // Логика закрытия позиций при пересечении средней линии if (closePrevious > channelMiddle) { //--- Проверяем закрытие выше средней линии for (int i = PositionsTotal() - 1; i >= 0; i--) { //--- Перебираем в обратном порядке ulong ticket = PositionGetTicket(i); //--- Получаем тикет if (PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == MagicNumber && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { //--- Проверяем позицию на покупку obj_Trade.PositionClose(ticket, Slippage); //--- Закрываем позицию Print("Closing Buy: Price " + DoubleToString(closePrevious, _Digits) + " crossed above middle channel " + DoubleToString(channelMiddle, _Digits)); //--- Записываем закрытие в журнал } } } if (closePrevious < channelMiddle) { //--- Проверяем закрытие ниже средней линии for (int i = PositionsTotal() - 1; i >= 0; i--) { //--- Перебираем в обратном порядке ulong ticket = PositionGetTicket(i); //--- Получаем тикет if (PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == MagicNumber && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) { //--- Проверяем позицию на продажу obj_Trade.PositionClose(ticket, Slippage); //--- Закрываем позицию Print("Closing Sell: Price " + DoubleToString(closePrevious, _Digits) + " crossed below middle channel " + DoubleToString(channelMiddle, _Digits)); //--- Записываем закрытие в журнал } } } // Открываем позицию при пробое с учётом инверсного режима bool buySignal = (close2 >= lower2) && (closePrevious < channelLower); //--- Сигнал на покупку bool sellSignal = (close2 <= upper2) && (closePrevious > channelUpper); //--- Сигнал на продажу if (TradeDirection == Inverse) { //--- Проверяем режим Inverse bool temp = buySignal; //--- Временно сохраняем сигнал на покупку buySignal = sellSignal; //--- Подменяем сигнал на покупку sellSignal = temp; //--- Подменяем сигнал на продажу } double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); //--- Получаем Ask double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); //--- Получаем Bid if (buySignal && buyCount < MaxBuys) { //--- Проверяем сигнал на покупку // Покупка double sl = (StopLossPips == 0) ? 0 : NormalizeDouble(ask - StopLossPips * _Point, _Digits); //--- Рассчитываем SL double tp = (TakeProfitPips == 0) ? 0 : NormalizeDouble(ask + TakeProfitPips * _Point, _Digits); //--- Рассчитываем TP if (obj_Trade.Buy(Lots, _Symbol, 0, sl, tp, "LRC Buy")) { //--- Открываем покупку Print("Buy signal: Price " + DoubleToString(closePrevious, _Digits) + " broke below lower channel from inside"); //--- Записываем сигнал в журнал DrawArrow(true, previousTime, closePrevious); //--- Рисуем стрелку } else { //--- Ошибка Print("Buy order failed: " + obj_Trade.ResultRetcodeDescription()); //--- Записываем ошибку в журнал } } if (sellSignal && sellCount < MaxSells) { //--- Проверяем сигнал на продажу // Продажа double sl = (StopLossPips == 0) ? 0 : NormalizeDouble(bid + StopLossPips * _Point, _Digits); //--- Рассчитываем SL double tp = (TakeProfitPips == 0) ? 0 : NormalizeDouble(bid - TakeProfitPips * _Point, _Digits); //--- Рассчитываем TP if (obj_Trade.Sell(Lots, _Symbol, 0, sl, tp, "LRC Sell")) { //--- Открываем продажу Print("Sell signal: Price " + DoubleToString(closePrevious, _Digits) + " broke above upper channel from inside"); //--- Записываем сигнал в журнал DrawArrow(false, previousTime, closePrevious); //--- Рисуем стрелку } else { //--- Ошибка Print("Sell order failed: " + obj_Trade.ResultRetcodeDescription()); //--- Записываем ошибку в журнал } }
Перейдём к управлению позициями и выполнению торговых операций на каждом новом баре. Сначала подсчитывается число открытых программой позиций на покупку и продажу. Для этого выполняется обратный перебор PositionsTotal, для каждой позиции получается тикет с помощью PositionGetTicket, после чего проверяются символ и MagicNumber. buyCount увеличивается для POSITION_TYPE_BUY, а sellCount — для POSITION_TYPE_SELL. Это позволяет соблюдать ограничения до открытия новых позиций. Затем реализуется закрытие при пересечении средней линии: если закрытие предыдущего бара closePrevious находится выше channelMiddle, закрываются все позиции на покупку; если ниже — закрываются все позиции на продажу. Перебор снова выполняется в обратном порядке, а позиция закрывается через obj_Trade.PositionClose.
Сигналы входа формируются по чётким пробоям изнутри канала. Сигнал на покупку возникает, когда закрытие бара со сдвигом 2 close2 находилось на нижней границе или выше неё, а закрытие предыдущего бара оказалось ниже текущей нижней границы. Это означает чёткий пробой нижней границы изнутри канала. Сигнал на продажу возникает в зеркальной ситуации: close2 находилось на верхней границе или ниже неё, а закрытие предыдущего бара оказалось выше текущей верхней границы. В режиме Inverse эти сигналы меняются местами, что позволяет торговать сценарии возврата к среднему вместо продолжения тренда.
Получаем текущие цены Ask и Bid. Если имеется сигнал на покупку и число позиций меньше MaxBuys, рассчитываются Stop Loss и Take Profit, после чего функцией obj_Trade.Buy отправляется рыночный запрос на покупку с фиксированным объёмом Lots, без заранее заданной цены, с рассчитанными SL/TP и комментарием LRC Buy. В режиме Inverse меняются местами только сигналы покупки и продажи; расчёт SL/TP выполняется стандартно для соответствующего типа позиции. При успешном открытии рисуется зелёная стрелка. Аналогично при сигнале на продажу и наличии места до MaxSells функция obj_Trade.Sell отправляет рыночный ордер на продажу с соответствующими SL/TP и комментарием LRC Sell, после чего рисуется красная стрелка.

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

Отчёт тестера стратегий:

Заключение
В заключение мы разработали адаптивную систему на основе канала линейной регрессии в MQL5. Она рассчитывает линию регрессии и полосы стандартного отклонения за заданный пользователем период и активируется только тогда, когда абсолютное значение наклона превышает минимальный порог. Пока цена остаётся внутри канала, он автоматически продлевается на каждом новом баре. При значительном отклонении канал полностью перестраивается по свежим данным. Система выявляет выходы цены из канала, поддерживает режимы Normal и Inverse (обычный и обратный), ограничивает число позиций в каждом направлении и закрывает позиции при пересечении средней линии.
Отказ от ответственности: статья предназначена исключительно для образовательных целей. Торговля сопряжена со значительными финансовыми рисками, а волатильность рынка может привести к убыткам. Перед использованием программы на реальном счёте необходимо провести тщательное бэктестирование и обеспечить надлежащее управление рисками.
Адаптивная стратегия на основе канала линейной регрессии поддерживает динамическое обновление, режимы Normal и Inverse, а также закрытие позиций при пересечении средней линии. Теперь вы можете применять сигналы канала на трендовых рынках. Стратегия готова к дальнейшей оптимизации в вашей торговой практике. Удачной торговли!
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/20347
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Моделирование рынка: Position View (XII)
VQC-фильтр с живым обучением прямо в MQL5 поверх графового fair-value скальпера
Алгоритм направления потока — Flow Direction Algorithm (FDA)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования