English Deutsch 日本語
preview
Автоматизация торговых стратегий на MQL5 (Часть 34): Система пробоя линии тренда с оценкой качества аппроксимации по коэффициенту R2

Автоматизация торговых стратегий на MQL5 (Часть 34): Система пробоя линии тренда с оценкой качества аппроксимации по коэффициенту R2

MetaTrader 5Трейдинг |
185 2
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

В нашей предыдущей статье (Часть 33) мы разработали на MetaQuotes Language 5 (MQL5) систему на основе паттерна «Акула», которая распознавала бычьи и медвежьи гармонические паттерны «Акула» по соотношениям Фибоначчи, автоматизировала сделки с настраиваемыми уровнями тейк-профита и стоп-лосса и визуализировала паттерны на графике с помощью таких объектов, как треугольники и линии тренда. В Части 34 мы создадим систему пробоя линии тренда. Она будет определять линии поддержки и сопротивления по свинговым точкам и проверять их по коэффициенту детерминации R² и ограничениям на угол наклона. Система будет открывать сделки на пробое и динамически отображать объекты на графике. Мы рассмотрим следующие темы:

  1. Основы стратегии пробоя линии тренда
  2. Реализация на MQL5
  3. Тестирование
  4. Заключение

К концу статьи у вас будет устойчивая и пригодная к настройке стратегия пробоя линии тренда на MQL5. Приступим!


Основы стратегии пробоя линии тренда

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

ПРИМЕР ПРОБОЯ ЛИНИИ ТРЕНДА

Мы будем определять свинговые максимумы и минимумы в пределах заданной глубины анализа, строить линии тренда с минимальным количеством точек касания и оценивать их по коэффициенту детерминации R² и ограничениям на угол наклона. Напомним, что R-квадрат, или коэффициент детерминации, — это статистический показатель, характеризующий, насколько хорошо регрессионная модель объясняет вариацию зависимой переменной на основе независимых переменных. Он показывает долю общей вариации результата, объясняемую моделью, и принимает значения от 0 до 1. Ниже приведена наглядная схема этой модели.

МОДЕЛЬ R-КВАДРАТ

Мы реализуем логику открытия сделок на пробое. Сигнал будет формироваться либо по закрытию свечи, либо когда вся свеча полностью находится выше или ниже линии тренда. Для наглядности будем использовать линии тренда, стрелки и подписи. Также организуем логику сопровождения линий тренда: неактуальные или пробитые ценой линии будем удалять. Ниже показан результат, который мы хотим получить; затем перейдем к реализации.

СИСТЕМА ЛИНИЙ ТРЕНДА


Реализация на MQL5

Чтобы создать программу на MQL5, откройте MetaEditor, затем откройте панель «Навигатор», найдите папку Indicators, нажмите «New» и следуйте указаниям мастера. После создания файла в среде разработки объявим входные параметры и структуры, которые сделают программу более гибкой и настраиваемой.

//+------------------------------------------------------------------+
//|                                 Trendline Breakout Trader 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"
#property strict

#include <Trade\Trade.mqh> //--- Подключаем торговую библиотеку для выполнения торговых операций
CTrade obj_Trade;          //--- Создаем торговый объект
//+------------------------------------------------------------------+
//| Перечисление способов определения пробоя                         |
//+------------------------------------------------------------------+
enum ENUM_BREAKOUT_TYPE {
   BREAKOUT_CLOSE = 0,      // Пробой по закрытию выше/ниже линии
   BREAKOUT_CANDLE = 1      // Пробой всей свечой выше/ниже линии
};
//+------------------------------------------------------------------+
//| Структура свинговой точки                                        |
//+------------------------------------------------------------------+
struct Swing {     //--- Определяем структуру свинговой точки
   datetime time;  //--- Сохраняем время свинга
   double price;   //--- Сохраняем цену свинга
};
//+------------------------------------------------------------------+
//| Структура начальной точки                                        |
//+------------------------------------------------------------------+
struct StartingPoint { //--- Определяем структуру начальной точки
   datetime time;      //--- Сохраняем время начальной точки
   double price;       //--- Сохраняем цену начальной точки
   bool is_support;    //--- Указываем признак поддержки или сопротивления
};
//+------------------------------------------------------------------+
//| Структура хранения линии тренда                                  |
//+------------------------------------------------------------------+
struct TrendlineInfo {      //--- Определяем структуру данных линии тренда
   string name;             //--- Сохраняем имя линии тренда
   datetime start_time;     //--- Сохраняем время начала
   datetime end_time;       //--- Сохраняем время окончания
   double start_price;      //--- Сохраняем начальную цену
   double end_price;        //--- Сохраняем конечную цену
   double slope;            //--- Сохраняем наклон
   bool is_support;         //--- Указываем признак поддержки или сопротивления
   int touch_count;         //--- Сохраняем количество касаний
   datetime creation_time;  //--- Сохраняем время создания
   int touch_indices[];     //--- Сохраняем массив индексов касаний
   bool is_signaled;        //--- Указываем признак сигнала
};
//+------------------------------------------------------------------+
//| Предварительные объявления                                       |
//+------------------------------------------------------------------+
void DetectSwings(); //--- Объявляем функцию поиска свингов
void SortSwings(Swing &swings[], int count); //--- Объявляем функцию сортировки свингов
double CalculateAngle(datetime time1, double price1, datetime time2, double price2); //--- Объявляем функцию расчета угла
bool ValidateTrendline(bool isSupport, datetime start_time, datetime ref_time, double ref_price, double slope, double tolerance_pen); //--- Объявляем функцию проверки линии тренда
void FindAndDrawTrendlines(bool isSupport); //--- Объявляем функцию поиска и отрисовки линий тренда
void UpdateTrendlines(); //--- Объявляем функцию обновления линий тренда
void RemoveTrendlineFromStorage(int index); //--- Объявляем функцию удаления линии тренда
bool IsStartingPointUsed(datetime time, double price, bool is_support); //--- Объявляем функцию проверки использования начальной точки
double CalculateRSquared(const datetime &times[], const double &prices[], int n, double slope, double intercept); //--- Объявляем функцию расчета R-квадрат
//+------------------------------------------------------------------+
//| Входные параметры                                                |
//+------------------------------------------------------------------+
input ENUM_BREAKOUT_TYPE BreakoutType = BREAKOUT_CLOSE; // Способ определения пробоя
input int LookbackBars = 200;                           // Задаем глубину анализа в барах для поиска свингов
input double TouchTolerance = 10.0;                     // Задаем допуск для точек касания (в пунктах)
input int MinTouches = 3;                               // Задаем минимальное количество касаний для признания линии тренда валидной
input double PenetrationTolerance = 5.0;                // Задаем допустимый выход бара за линию (в пунктах)
input int ExtensionBars = 100;                          // Задаем длину продолжения линии тренда вправо (в барах)
input int MinBarSpacing = 10;                           // Задаем минимальное расстояние между касаниями в барах
input double inpLot = 0.01;                             // Задаем размер лота
input double inpSLPoints = 100.0;                       // Задаем стоп-лосс (в пунктах)
input double inpRRRatio = 1.1;                          // Задаем соотношение риска и прибыли
input double MinAngle = 1.0;                            // Задаем минимальный угол наклона (в градусах)
input double MaxAngle = 89.0;                           // Задаем максимальный угол наклона (в градусах)
input double MinRSquared = 0.8;                         // Минимальный R-квадрат для принятия линии тренда
input bool DeleteExpiredObjects = false;                // Включаем удаление неактуальных или пробитых объектов
input bool EnableTradingSignals = true;                 // Включаем торговые сигналы и совершение сделок
input bool DrawTouchArrows = true;                      // Включаем отрисовку стрелок в точках касания
input bool DrawLabels = true;                           // Включаем отрисовку подписей линий тренда и точек
input color SupportLineColor = clrGreen;                // Задаем цвет линий поддержки
input color ResistanceLineColor = clrRed;               // Задаем цвет линий сопротивления
//+------------------------------------------------------------------+
//| Глобальные переменные                                            |
//+------------------------------------------------------------------+
Swing swingLows[];              //--- Сохраняем свинговые минимумы
int numLows = 0;                //--- Отслеживаем количество свинговых минимумов
Swing swingHighs[];             //--- Сохраняем свинговые максимумы
int numHighs = 0;               //--- Отслеживаем количество свинговых максимумов
TrendlineInfo trendlines[];     //--- Сохраняем линии тренда
int numTrendlines = 0;          //--- Отслеживаем количество линий тренда
StartingPoint startingPoints[]; //--- Сохраняем использованные начальные точки
int numStartingPoints = 0;      //--- Отслеживаем количество начальных точек

Реализацию системы пробоя линии тренда начнем с базовых компонентов, необходимых для обнаружения пробоев и открытия сделок по ним. Сначала подключим библиотеку Trade.mqh и создадим объект CTrade  с именем obj_Trade для выполнения торговых операций. Затем объявим перечисление ENUM_BREAKOUT_TYPE с вариантами BREAKOUT_CLOSE (пробой по закрытию свечи) и BREAKOUT_CANDLE (пробой всей свечой), что позволит гибко выбирать способ обнаружения пробоя. Далее создадим структуру Swing для хранения времени и цены свинговой точки, структуру StartingPoint для учета уже использованных начальных точек линии тренда с признаком поддержки или сопротивления и структуру TrendlineInfo для хранения сведений о линии: имени, времени и цены начала и конца, наклона, количества касаний, времени создания, индексов касаний и состояния сигнала.

Объявим прототипы основных функций: DetectSwings, SortSwings и CalculateAngle. Затем зададим входные параметры: BreakoutType со значением BREAKOUT_CLOSE, LookbackBars со значением 200 и остальные параметры, назначение которых понятно из комментариев. Наконец, создадим глобальные массивы swingLows, swingHighs, trendlines и startingPoints, а также счетчики numLows, numHighs, numTrendlines и numStartingPoints. Они составят основу механизма поиска и проверки линий тренда для торговли на пробое. Теперь можно инициализировать массивы хранения в функции инициализации.

//+------------------------------------------------------------------+
//| Функция инициализации советника                                  |
//+------------------------------------------------------------------+
int OnInit() {
   ArrayResize(trendlines, 0);     //--- Изменяем размер массива линий тренда
   numTrendlines = 0;              //--- Сбрасываем счетчик линий тренда
   ArrayResize(startingPoints, 0); //--- Изменяем размер массива начальных точек
   numStartingPoints = 0;          //--- Сбрасываем счетчик начальных точек
   return(INIT_SUCCEEDED);         //--- Возвращаем успешное выполнение
}
//+------------------------------------------------------------------+
//| Функция деинициализации советника                                |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   ArrayResize(trendlines, 0);     //--- Изменяем размер массива линий тренда
   numTrendlines = 0;              //--- Сбрасываем счетчик линий тренда
   ArrayResize(startingPoints, 0); //--- Изменяем размер массива начальных точек
   numStartingPoints = 0;          //--- Сбрасываем счетчик начальных точек
}

Реализуем функцию OnInit для корректной инициализации: с помощью ArrayResize обнуляем массивы "trendlines" и "startingPoints", сбрасываем соответствующие счетчики и возвращаем INIT_SUCCEEDED. В функции OnDeinit выполняем аналогичную очистку ресурсов. После завершения инициализации можно переходить к логике стратегии. Для удобства разобьем ее на отдельные функции и начнем с поиска свинговых точек (локальных экстремумов), которые послужат опорными точками линий тренда.

//+------------------------------------------------------------------+
//| Проверка появления нового бара                                   |
//+------------------------------------------------------------------+
bool IsNewBar() {
   static datetime lastTime = 0;                      //--- Сохраняем время последнего бара
   datetime currentTime = iTime(_Symbol, _Period, 0); //--- Получаем время текущего бара
   if (lastTime != currentTime) {                     //--- Проверка появления нового бара
      lastTime = currentTime;                         //--- Обновляем время последнего бара
      return true;                                    //--- Указываем появление нового бара
   }
   return false;                                      //--- Указываем отсутствие нового бара
}
//+------------------------------------------------------------------+
//| Сортировка свингов по времени (по возрастанию, сначала старые)   |
//+------------------------------------------------------------------+
void SortSwings(Swing &swings[], int count) {
   for (int i = 0; i < count - 1; i++) {            //--- Перебираем свинги
      for (int j = 0; j < count - i - 1; j++) {     //--- Сравниваем соседние свинги
         if (swings[j].time > swings[j + 1].time) { //--- Проверяем порядок по времени
            Swing temp = swings[j];                 //--- Сохраняем временный свинг
            swings[j] = swings[j + 1];              //--- Меняем местами свинги
            swings[j + 1] = temp;                   //--- Завершаем перестановку
         }
      }
   }
}
//+------------------------------------------------------------------+
//| Определение свинговых максимумов и минимумов                     |
//+------------------------------------------------------------------+
void DetectSwings() {
   numLows = 0;                                              //--- Сбрасываем счетчик минимумов
   ArrayResize(swingLows, 0);                                //--- Изменяем размер массива минимумов
   numHighs = 0;                                             //--- Сбрасываем счетчик максимумов
   ArrayResize(swingHighs, 0);                               //--- Изменяем размер массива максимумов
   int totalBars = iBars(_Symbol, _Period);                  //--- Получаем общее количество баров
   int effectiveLookback = MathMin(LookbackBars, totalBars); //--- Рассчитываем эффективную глубину анализа
   if (effectiveLookback < 5) {                              //--- Проверяем, достаточно ли баров
      Print("Not enough bars for swing detection.");         //--- Записываем в журнал сообщение о недостаточном количестве баров
      return;                                                //--- Выходим из функции
   }
   for (int i = 2; i < effectiveLookback - 2; i++) {         //--- Перебираем бары
      double low_i = iLow(_Symbol, _Period, i);              //--- Получаем текущий минимум
      double low_im1 = iLow(_Symbol, _Period, i - 1);        //--- Получаем предыдущий минимум
      double low_im2 = iLow(_Symbol, _Period, i - 2);        //--- Получаем минимум двумя барами ранее
      double low_ip1 = iLow(_Symbol, _Period, i + 1);        //--- Получаем следующий минимум
      double low_ip2 = iLow(_Symbol, _Period, i + 2);        //--- Получаем минимум двумя барами позже
      if (low_i < low_im1 && low_i < low_im2 && low_i < low_ip1 && low_i < low_ip2) { //--- Проверяем наличие свингового минимума
         Swing s;                                            //--- Создаем структуру свинговой точки
         s.time = iTime(_Symbol, _Period, i);                //--- Задаем время свинговой точки
         s.price = low_i;                                    //--- Задаем цену свинговой точки
         ArrayResize(swingLows, numLows + 1);                //--- Изменяем размер массива минимумов
         swingLows[numLows] = s;                             //--- Добавляем свинговый минимум
         numLows++;                                          //--- Увеличиваем счетчик минимумов
      }
      double high_i = iHigh(_Symbol, _Period, i);       //--- Получаем текущий максимум
      double high_im1 = iHigh(_Symbol, _Period, i - 1); //--- Получаем предыдущий максимум
      double high_im2 = iHigh(_Symbol, _Period, i - 2); //--- Получаем максимум двумя барами ранее
      double high_ip1 = iHigh(_Symbol, _Period, i + 1); //--- Получаем следующий максимум
      double high_ip2 = iHigh(_Symbol, _Period, i + 2); //--- Получаем максимум двумя барами позже
      if (high_i > high_im1 && high_i > high_im2 && high_i > high_ip1 && high_i > high_ip2) { //--- Проверяем наличие свингового максимума
         Swing s;                                       //--- Создаем структуру свинговой точки
         s.time = iTime(_Symbol, _Period, i);           //--- Задаем время свинговой точки
         s.price = high_i;                              //--- Задаем цену свинговой точки
         ArrayResize(swingHighs, numHighs + 1);         //--- Изменяем размер массива максимумов
         swingHighs[numHighs] = s;                      //--- Добавляем свинговый максимум
         numHighs++;                                    //--- Увеличиваем счетчик максимумов
      }
   }
   if (numLows > 0) SortSwings(swingLows, numLows);     //--- Сортируем свинговые минимумы
   if (numHighs > 0) SortSwings(swingHighs, numHighs);  //--- Сортируем свинговые максимумы
}

После подготовки базовых компонентов реализуем основную логику обнаружения свинговых точек и обработки новых баров. Сначала создадим функцию "IsNewBar". Статическая переменная "lastTime" хранит время предыдущего бара и сравнивается со временем текущего бара, полученным через iTime для текущего символа и периода со сдвигом 0. Если значения различаются, "lastTime" обновляется, а функция возвращает true, указывая на появление нового бара; иначе возвращается false. Затем реализуем функцию "SortSwings", которая сортирует массив структур "Swing" по времени в порядке возрастания методом пузырьковой сортировки. Вложенные циклы сравнивают поля "time" соседних элементов и при необходимости меняют элементы местами через временную структуру "Swing".

Далее создадим функцию DetectSwings. Она сбрасывает numLows и numHighs, а также изменяет размеры массивов swingLows и swingHighs до нуля. Эффективная глубина анализа определяется через MathMin  как минимальное значение между LookbackBars и общим количеством баров, полученным функцией iBars. Если доступно менее пяти баров, функция выводит сообщение через Print  и завершает работу. Затем перебираются бары от индекса 2 до effectiveLookback - 2. Для поиска свингового минимума минимум текущего бара iLow сравнивается с минимумами двух предыдущих и двух последующих баров. Аналогично свинговые максимумы определяются через iHigh. При обнаружении свинга создается структура Swing: поле time заполняется через iTime, а price — соответствующим минимумом или максимумом. Элемент добавляется в swingLows либо swingHighs с помощью ArrayResize, после чего увеличивается соответствующий счетчик. В завершение функция SortSwings сортирует оба непустых массива по времени, подготавливая точки к построению линий тренда. Теперь определим функции расчета угла наклона и проверки линии тренда.

//+------------------------------------------------------------------+
//| Расчет визуального угла наклона                                  |
//+------------------------------------------------------------------+
double CalculateAngle(datetime time1, double price1, datetime time2, double price2) {
   int x1, y1, x2, y2; //--- Объявляем переменные координат
   if (!ChartTimePriceToXY(0, 0, time1, price1, x1, y1)) return 0.0; //--- Преобразуем время и цену первой точки в координаты XY
   if (!ChartTimePriceToXY(0, 0, time2, price2, x2, y2)) return 0.0; //--- Преобразуем время и цену второй точки в координаты XY
   double dx = (double)(x2 - x1);                                    //--- Рассчитываем разность координат по оси X
   double dy = (double)(y2 - y1);                                    //--- Рассчитываем разность координат по оси Y
   if (dx == 0.0) return (dy > 0.0 ? -90.0 : 90.0);                  //--- Обрабатываем случай вертикальной линии
   double angle = MathArctan(-dy / dx) * 180.0 / M_PI;               //--- Рассчитываем угол в градусах
   return angle;                                                     //--- Возвращаем угол
}
//+------------------------------------------------------------------+
//| Проверка линии тренда                                            |
//+------------------------------------------------------------------+
bool ValidateTrendline(bool isSupport, datetime start_time, datetime ref_time, double ref_price, double slope, double tolerance_pen) {
   int bar_start = iBarShift(_Symbol, _Period, start_time); //--- Получаем индекс начального бара
   if (bar_start < 0) return false;                         //--- Проверяем недопустимый индекс бара
   for (int bar = bar_start; bar >= 0; bar--) {             //--- Перебираем бары
      datetime bar_time = iTime(_Symbol, _Period, bar);     //--- Получаем время бара
      double dk = (double)(bar_time - ref_time);            //--- Рассчитываем разность во времени
      double line_price = ref_price + slope * dk;           //--- Рассчитываем цену линии
      if (isSupport) {                                      //--- Обрабатываем линию поддержки
         double low = iLow(_Symbol, _Period, bar);          //--- Получаем минимум бара
         if (low < line_price - tolerance_pen) return false;//--- Проверяем наличие пробоя
      } else {                                              //--- Обрабатываем линию сопротивления
         double high = iHigh(_Symbol, _Period, bar);        //--- Получаем максимум бара
         if (high > line_price + tolerance_pen) return false;//--- Проверяем наличие пробоя
      }
   }
   return true; //--- Возвращаем признак успешной проверки
}

Здесь реализуются функции расчета угла линии тренда и проверки ее целостности. Функция CalculateAngle преобразует две точки «время–цена» (time1, price1 и time2, price2) в координаты графика (x1, y1 и x2, y2) с помощью ChartTimePriceToXY. Если любое преобразование завершается неудачно, возвращается 0.0. Затем рассчитываются разности dx и dy. Для вертикальной линии при dx = 0 возвращается -90.0 или 90.0 в зависимости от знака dy. В остальных случаях визуальный угол в градусах вычисляется через MathArctan  от -dy / dx с умножением на 180/M_PI.

Затем реализуем функцию ValidateTrendline. Она получает индекс бара, соответствующего start_time, через iBarShift и возвращает false при недопустимом индексе. Далее перебираются бары от этого индекса до текущего. Для времени каждого бара, полученного через iTime, рассчитывается цена линии по формуле ref_price + slope * (bar_time - ref_time). Для линии поддержки проверяется, не опустился ли минимум бара iLow ниже line_price - tolerance_pen; при таком нарушении возвращается false. Для сопротивления проверяется, не поднялся ли максимум бара iHigh выше line_price + tolerance_pen. Если нарушений нет, функция возвращает true. Так отбираются непробитые линии, соответствующие ограничениям по углу и пригодные для надежного обнаружения пробоя. Теперь определим функцию расчета качества аппроксимации по коэффициенту детерминации R².

//+------------------------------------------------------------------+
//| Расчет R-квадрата для оценки качества аппроксимации              |
//+------------------------------------------------------------------+
double CalculateRSquared(const datetime &times[], const double &prices[], int n, double slope, double intercept) {
   double sum_y = 0.0;                             //--- Инициализируем сумму значений y
   for (int k = 0; k < n; k++) {                   //--- Перебираем точки
      sum_y += prices[k];                          //--- Накапливаем y
   }
   double mean_y = sum_y / n;                      //--- Рассчитываем среднее значение y
   double ss_tot = 0.0, ss_res = 0.0;              //--- Инициализируем суммы квадратов
   for (int k = 0; k < n; k++) {                   //--- Перебираем точки
      double x = (double)times[k];                 //--- Получаем x (время)
      double y_pred = intercept + slope * x;       //--- Рассчитываем прогнозируемое y
      double y = prices[k];                        //--- Получаем фактическое y
      ss_res += (y - y_pred) * (y - y_pred);       //--- Накапливаем остаточную сумму квадратов
      ss_tot += (y - mean_y) * (y - mean_y);       //--- Накапливаем полную сумму квадратов
   }
   if (ss_tot == 0.0) return 1.0;                  //--- Обрабатываем случай постоянного значения y
   return 1.0 - ss_res / ss_tot;                   //--- Рассчитываем и возвращаем R-квадрат
}

Функция "CalculateRSquared" принимает массивы времени и цен, количество точек "n", наклон "slope" и свободный член (intercept). Сначала суммируются значения "prices" и рассчитывается их среднее. Затем для каждой точки определяется прогнозируемая цена, после чего вычисляются остаточная сумма квадратов "ss_res" и полная сумма квадратов "ss_tot". Если "ss_tot" равно нулю, функция возвращает 1.0. В остальных случаях значение R-квадрат рассчитывается как 1.0 - ss_res / ss_tot. Чем ближе результат к единице, тем точнее линия описывает расположение выбранных точек.

//+------------------------------------------------------------------+
//| Проверка использования начальной точки                           |
//+------------------------------------------------------------------+
bool IsStartingPointUsed(datetime time, double price, bool is_support) {
   for (int i = 0; i < numStartingPoints; i++) { //--- Перебираем начальные точки
      if (startingPoints[i].time == time && MathAbs(startingPoints[i].price - price) < TouchTolerance * _Point && startingPoints[i].is_support == is_support) { //--- Проверяем совпадение
         return true; //--- Указываем, что точка уже использовалась
      }
   }
   return false; //--- Указываем, что точка не использовалась
}
//+------------------------------------------------------------------+
//| Удаление линии тренда из хранилища и при необходимости с графика |
//+------------------------------------------------------------------+
void RemoveTrendlineFromStorage(int index) {
   if (index < 0 || index >= numTrendlines) return;             //--- Проверяем допустимый индекс
   Print("Removing trendline from storage: ", trendlines[index].name); //--- Записываем в журнал удаление
   if (DeleteExpiredObjects) {                                  //--- Проверяем признак удаления
      ObjectDelete(0, trendlines[index].name);                  //--- Удаляем объект линии тренда
      for (int m = 0; m < trendlines[index].touch_count; m++) { //--- Перебираем касания
         string arrow_name = trendlines[index].name + "_touch" + IntegerToString(m); //--- Формируем имя стрелки
         ObjectDelete(0, arrow_name);                           //--- Удаляем стрелку касания
         string text_name = trendlines[index].name + "_point_label" + IntegerToString(m); //--- Формируем имя текстового объекта
         ObjectDelete(0, text_name);                            //--- Удаляем подпись точки
      }
      string label_name = trendlines[index].name + "_label";    //--- Формируем имя подписи
      ObjectDelete(0, label_name);                              //--- Удаляем подпись линии тренда
      string signal_arrow = trendlines[index].name + "_signal_arrow"; //--- Формируем имя сигнальной стрелки
      ObjectDelete(0, signal_arrow);                            //--- Удаляем сигнальную стрелку
      string signal_text = trendlines[index].name + "_signal_text"; //--- Формируем имя текста сигнала
      ObjectDelete(0, signal_text);                             //--- Удаляем текст сигнала
   }
   for (int i = index; i < numTrendlines - 1; i++) {            //--- Сдвигаем массив
      trendlines[i] = trendlines[i + 1];                        //--- Копируем следующую линию тренда
   }
   ArrayResize(trendlines, numTrendlines - 1);                  //--- Изменяем размер массива линий тренда
   numTrendlines--;                                             //--- Уменьшаем количество линий тренда
}

Здесь реализуются функции учета начальных точек линий тренда и их очистки. Функция IsStartingPointUsed перебирает массив startingPoints и проверяет, совпадают ли заданные time, price и is_support с ранее сохраненной точкой с учетом допуска TouchTolerance * _Point. Для сравнения цены используется MathAbs. При совпадении возвращается true, иначе — false. Благодаря этому из одной точки не будет построено более одной линии тренда.

Затем создадим функцию RemoveTrendlineFromStorage. Она проверяет корректность index относительно numTrendlines и выводит через Print имя удаляемой линии. Если DeleteExpiredObjects равно true, с помощью ObjectDelete  удаляются графические объекты: сама линия name, стрелки касаний name + "_touch" + index, подписи точек name + "_point_label" + index, подпись линии name + "_label", сигнальная стрелка name + "_signal_arrow" и сигнальный текст name + "_signal_text". После этого элементы массива trendlines сдвигаются влево, его размер уменьшается через ArrayResize, а numTrendlines уменьшается на единицу. Это позволяет корректно удалять неактуальные линии и связанные с ними объекты. Теперь определим функцию поиска и отрисовки линий тренда с использованием созданных вспомогательных функций.

//+------------------------------------------------------------------+
//| Поиск и отрисовка линии тренда при отсутствии активной           |
//+------------------------------------------------------------------+
void FindAndDrawTrendlines(bool isSupport) {
   bool has_active = false;                        //--- Инициализируем признак активности
   for (int i = 0; i < numTrendlines; i++) {       //--- Перебираем линии тренда
      if (trendlines[i].is_support == isSupport) { //--- Проверяем совпадение типа
         has_active = true;                        //--- Задаем признак активности
         break;                                    //--- Выходим из цикла
      }
   }
   if (has_active) return;                          //--- Выходим, если активная линия тренда уже существует
   Swing swings[];                                  //--- Инициализируем массив свинговых точек
   int numSwings;                                   //--- Инициализируем количество свинговых точек
   color lineColor;                                 //--- Инициализируем цвет линии
   string prefix;                                   //--- Инициализируем префикс
   if (isSupport) {                                 //--- Обрабатываем линию поддержки
      numSwings = numLows;                          //--- Задаем количество минимумов
      ArrayResize(swings, numSwings);               //--- Изменяем размер массива свинговых точек
      for (int i = 0; i < numSwings; i++) {         //--- Перебираем минимумы
         swings[i].time = swingLows[i].time;        //--- Копируем время минимума
         swings[i].price = swingLows[i].price;      //--- Копируем цену минимума
      }
      lineColor = SupportLineColor;                 //--- Задаем цвет линии поддержки
      prefix = "Trendline_Support_";                //--- Задаем префикс линии поддержки
   } else {                                         //--- Обрабатываем линию сопротивления
      numSwings = numHighs;                         //--- Задаем количество максимумов
      ArrayResize(swings, numSwings);               //--- Изменяем размер массива свинговых точек
      for (int i = 0; i < numSwings; i++) {         //--- Перебираем максимумы
         swings[i].time = swingHighs[i].time;       //--- Копируем время максимума
         swings[i].price = swingHighs[i].price;     //--- Копируем цену максимума
      }
      lineColor = ResistanceLineColor;              //--- Задаем цвет линии сопротивления
      prefix = "Trendline_Resistance_";             //--- Задаем префикс линии сопротивления
   }
   if (numSwings < 2) return;                       //--- Выходим, если свинговых точек недостаточно
   double pointValue = _Point;                      //--- Получаем размер пункта
   double touch_tolerance = TouchTolerance * pointValue; //--- Рассчитываем допуск для касаний
   double pen_tolerance = PenetrationTolerance * pointValue; //--- Рассчитываем допустимый выход за линию
   int best_j = -1;                                 //--- Инициализируем лучший индекс j
   int max_touches = 0;                             //--- Инициализируем максимальное количество касаний
   double best_rsquared = -1.0;                     //--- Инициализируем лучший R-квадрат
   int best_touch_indices[];                        //--- Инициализируем лучший массив индексов касаний
   double best_slope = 0.0;                         //--- Инициализируем лучший наклон
   double best_intercept = 0.0;                     //--- Инициализируем лучший свободный член
   datetime best_min_time = 0;                      //--- Инициализируем лучшее минимальное время
   for (int i = 0; i < numSwings - 1; i++) {        //--- Перебираем первые точки
      for (int j = i + 1; j < numSwings; j++) {     //--- Перебираем вторые точки
         datetime time1 = swings[i].time;           //--- Получаем время первой точки
         double price1 = swings[i].price;           //--- Получаем цену первой точки
         datetime time2 = swings[j].time;           //--- Получаем время второй точки
         double price2 = swings[j].price;           //--- Получаем цену второй точки
         double dt = (double)(time2 - time1);       //--- Рассчитываем разность во времени
         if (dt <= 0) continue;                     //--- Пропускаем недопустимую разность во времени
         double initial_slope = (price2 - price1) / dt; //--- Рассчитываем начальный наклон
         int touch_indices[];                       //--- Инициализируем массив индексов касаний
         ArrayResize(touch_indices, 0);             //--- Изменяем размер массива индексов касаний
         int touches = 0;                           //--- Инициализируем количество касаний
         ArrayResize(touch_indices, touches + 1);   //--- Добавляем первый индекс
         touch_indices[touches] = i;                //--- Задаем первый индекс
         touches++;                                 //--- Увеличиваем касания
         ArrayResize(touch_indices, touches + 1);   //--- Добавляем второй индекс
         touch_indices[touches] = j;                //--- Задаем второй индекс
         touches++;                                 //--- Увеличиваем касания
         for (int k = 0; k < numSwings; k++) {      //--- Перебираем свинги
            if (k == i || k == j) continue;         //--- Пропускаем уже использованные индексы
            datetime tk = swings[k].time;           //--- Получаем время свинговой точки
            double dk = (double)(tk - time1);       //--- Рассчитываем разность во времени
            double expected = price1 + initial_slope * dk; //--- Рассчитываем ожидаемую цену
            double actual = swings[k].price;        //--- Получаем фактическую цену
            if (MathAbs(expected - actual) <= touch_tolerance) { //--- Проверяем, находится ли точка касания в пределах допуска
               ArrayResize(touch_indices, touches + 1); //--- Добавляем индекс
               touch_indices[touches] = k;          //--- Задаем индекс
               touches++;                           //--- Увеличиваем касания
            }
         }
         if (touches >= MinTouches) {               //--- Проверяем минимальное количество касаний
            ArraySort(touch_indices);               //--- Сортируем массив индексов касаний
            bool valid_spacing = true;              //--- Инициализируем признак допустимого расстояния
            for (int m = 0; m < touches - 1; m++) { //--- Перебираем касания
               int idx1 = touch_indices[m];         //--- Получаем первый индекс
               int idx2 = touch_indices[m + 1];     //--- Получаем второй индекс
               int bar1 = iBarShift(_Symbol, _Period, swings[idx1].time); //--- Получаем первый бар
               int bar2 = iBarShift(_Symbol, _Period, swings[idx2].time); //--- Получаем второй бар
               int diff = MathAbs(bar1 - bar2);     //--- Рассчитываем разность между барами
               if (diff < MinBarSpacing) {          //--- Проверяем минимальное расстояние
                  valid_spacing = false;            //--- Отмечаем недопустимое расстояние
                  break;                            //--- Выходим из цикла
               }
            }
            if (valid_spacing) {                        //--- Проверяем допустимость расстояния
               datetime touch_times[];                  //--- Инициализируем массив времени касаний
               double touch_prices[];                   //--- Инициализируем массив цен касаний
               ArrayResize(touch_times, touches);       //--- Изменяем размер массива времени
               ArrayResize(touch_prices, touches);      //--- Изменяем размер массива цен
               for (int m = 0; m < touches; m++) {      //--- Перебираем касания
                  int idx = touch_indices[m];           //--- Получаем индекс
                  touch_times[m] = swings[idx].time;    //--- Задаем время
                  touch_prices[m] = swings[idx].price;  //--- Задаем цену
               }
               double slope = initial_slope;            //--- Используем начальный наклон, рассчитанный по двум точкам
               double intercept = price1 - slope * (double)time1; //--- Рассчитываем свободный член
               double rsquared = CalculateRSquared(touch_times, touch_prices, touches, slope, intercept); //--- Рассчитываем R-квадрат
               if (rsquared >= MinRSquared) {           //--- Проверяем минимальное значение R-квадрата
                  int adjusted_touch_indices[];         //--- Инициализируем массив скорректированных индексов
                  ArrayResize(adjusted_touch_indices, touches); //--- Изменяем размер массива по текущему количеству касаний
                  ArrayCopy(adjusted_touch_indices, touch_indices); //--- Копируем индексы
                  int adjusted_touches = touches;       //--- Задаем скорректированное количество касаний
                  if (adjusted_touches >= MinTouches) { //--- Проверяем минимальное скорректированное количество касаний
                     datetime temp_min_time = swings[adjusted_touch_indices[0]].time; //--- Получаем минимальное время
                     double temp_ref_price = intercept + slope * (double)temp_min_time; //--- Рассчитываем опорную цену
                     if (ValidateTrendline(isSupport, temp_min_time, temp_min_time, temp_ref_price, slope, pen_tolerance)) { //--- Проверка линии тренда
                        datetime temp_max_time = swings[adjusted_touch_indices[adjusted_touches - 1]].time; //--- Получаем максимальное время
                        double temp_max_price = intercept + slope * (double)temp_max_time; //--- Рассчитываем максимальную цену
                        double angle = CalculateAngle(temp_min_time, temp_ref_price, temp_max_time, temp_max_price); //--- Рассчитываем угол
                        double abs_angle = MathAbs(angle); //--- Получаем абсолютное значение угла
                        if (abs_angle >= MinAngle && abs_angle <= MaxAngle) { //--- Проверяем диапазон угла
                           if (adjusted_touches > max_touches || (adjusted_touches == max_touches && rsquared > best_rsquared)) { //--- Проверяем, является ли линия тренда лучшей
                              max_touches = adjusted_touches; //--- Обновляем максимальное количество касаний
                              best_rsquared = rsquared;       //--- Обновляем лучший R-квадрат
                              best_j = j;                     //--- Обновляем лучший j
                              best_slope = slope;             //--- Обновляем лучший наклон
                              best_intercept = intercept;     //--- Обновляем лучший свободный член
                              best_min_time = temp_min_time;  //--- Обновляем лучшее минимальное время
                              ArrayResize(best_touch_indices, adjusted_touches); //--- Изменяем размер массива лучших индексов
                              ArrayCopy(best_touch_indices, adjusted_touch_indices); //--- Копируем индексы
                           }
                        }
                     }
                  }
               }
            }
         }
      }
   }
   if (max_touches < MinTouches) {                        //--- Проверяем, достаточно ли касаний
      string type = isSupport ? "Support" : "Resistance"; //--- Задаем строковое обозначение типа
      return;                                             //--- Выходим из функции
   }
   int touch_indices[];                                   //--- Инициализируем массив индексов касаний
   ArrayResize(touch_indices, max_touches);               //--- Изменяем размер массива индексов касаний
   ArrayCopy(touch_indices, best_touch_indices);          //--- Копируем лучшие индексы
   int touches = max_touches;                             //--- Задаем количество касаний
   datetime min_time = best_min_time;                     //--- Задаем минимальное время
   double price_min = best_intercept + best_slope * (double)min_time; //--- Рассчитываем минимальную цену
   datetime max_time = swings[touch_indices[touches - 1]].time; //--- Задаем максимальное время
   double price_max = best_intercept + best_slope * (double)max_time; //--- Рассчитываем максимальную цену
   datetime start_time_check = min_time;                  //--- Задаем время начальной точки для проверки
   double start_price_check = price_min;                  //--- Задаем начальную цену проверки (приблизительную, если точного значения нет)
   if (IsStartingPointUsed(start_time_check, start_price_check, isSupport)) { //--- Проверяем, использовалась ли начальная точка
      return; //--- Пропускаем уже использованную точку
   }
   datetime time_end = iTime(_Symbol, _Period, 0) + PeriodSeconds(_Period) * ExtensionBars; //--- Рассчитываем время окончания
   double dk_end = (double)(time_end - min_time);         //--- Рассчитываем разность до времени окончания
   double price_end = price_min + best_slope * dk_end;    //--- Рассчитываем конечную цену
   string unique_name = prefix + TimeToString(TimeCurrent(), TIME_DATE|TIME_MINUTES|TIME_SECONDS); //--- Формируем уникальное имя
   if (ObjectFind(0, unique_name) < 0) {                  //--- Проверяем, существует ли линия тренда
      ObjectCreate(0, unique_name, OBJ_TREND, 0, min_time, price_min, time_end, price_end); //--- Создаем линию тренда
      ObjectSetInteger(0, unique_name, OBJPROP_COLOR, lineColor); //--- Задаем цвет
      ObjectSetInteger(0, unique_name, OBJPROP_STYLE, STYLE_SOLID); //--- Задаем стиль
      ObjectSetInteger(0, unique_name, OBJPROP_WIDTH, 1);  //--- Задаем толщину
      ObjectSetInteger(0, unique_name, OBJPROP_RAY_RIGHT, false); //--- Отключаем луч вправо
      ObjectSetInteger(0, unique_name, OBJPROP_RAY_LEFT, false); //--- Отключаем луч влево
      ObjectSetInteger(0, unique_name, OBJPROP_BACK, false); //--- Выводим объект на передний план
   }
   ArrayResize(trendlines, numTrendlines + 1);               //--- Изменяем размер массива линий тренда
   trendlines[numTrendlines].name = unique_name;             //--- Задаем имя линии тренда
   trendlines[numTrendlines].start_time = min_time;          //--- Задаем время начала
   trendlines[numTrendlines].end_time = time_end;            //--- Задаем время окончания
   trendlines[numTrendlines].start_price = price_min;        //--- Задаем начальную цену
   trendlines[numTrendlines].end_price = price_end;          //--- Задаем конечную цену
   trendlines[numTrendlines].slope = best_slope;             //--- Задаем наклон
   trendlines[numTrendlines].is_support = isSupport;         //--- Задаем тип
   trendlines[numTrendlines].touch_count = touches;          //--- Задаем количество касаний
   trendlines[numTrendlines].creation_time = TimeCurrent();  //--- Задаем время создания
   trendlines[numTrendlines].is_signaled = false;            //--- Задаем признак сигнала
   ArrayResize(trendlines[numTrendlines].touch_indices, touches); //--- Изменяем размер массива индексов касаний
   ArrayCopy(trendlines[numTrendlines].touch_indices, touch_indices); //--- Копируем массив индексов касаний
   numTrendlines++;                                          //--- Увеличиваем счетчик линий тренда
   ArrayResize(startingPoints, numStartingPoints + 1);       //--- Изменяем размер массива начальных точек
   startingPoints[numStartingPoints].time = start_time_check;//--- Задаем время начальной точки
   startingPoints[numStartingPoints].price = start_price_check; //--- Задаем цену начальной точки
   startingPoints[numStartingPoints].is_support = isSupport; //--- Задаем тип начальной точки
   numStartingPoints++;                                      //--- Увеличиваем счетчик начальных точек
   if (DrawTouchArrows) {                                    //--- Проверяем отрисовку стрелок
      for (int m = 0; m < touches; m++) {                    //--- Перебираем касания
         int idx = touch_indices[m];                         //--- Получаем индекс касания
         datetime tk_time = swings[idx].time;                //--- Получаем время касания
         double tk_price = swings[idx].price;                //--- Получаем цену касания
         string arrow_name = unique_name + "_touch" + IntegerToString(m); //--- Формируем имя стрелки
         if (ObjectFind(0, arrow_name) < 0) { //--- Проверяем, существует ли стрелка
            ObjectCreate(0, arrow_name, OBJ_ARROW, 0, tk_time, tk_price); //--- Создаем стрелку касания
            ObjectSetInteger(0, arrow_name, OBJPROP_ARROWCODE, 159); //--- Задаем код стрелки
            ObjectSetInteger(0, arrow_name, OBJPROP_ANCHOR, isSupport ? ANCHOR_TOP : ANCHOR_BOTTOM); //--- Задаем привязку
            ObjectSetInteger(0, arrow_name, OBJPROP_COLOR, lineColor); //--- Задаем цвет
            ObjectSetInteger(0, arrow_name, OBJPROP_WIDTH, 1); //--- Задаем толщину
            ObjectSetInteger(0, arrow_name, OBJPROP_BACK, false); //--- Выводим объект на передний план
         }
      }
   }
   double angle = CalculateAngle(min_time, price_min, max_time, price_max); //--- Рассчитываем угол
   string type = isSupport ? "Support" : "Resistance"; //--- Задаем строковое обозначение типа
   Print(type + " Trendline " + unique_name + " drawn with " + IntegerToString(touches) + " touches. Inclination angle: " + DoubleToString(angle, 2) + " degrees."); //--- Записываем в журнал линию тренда
   if (DrawLabels) {                                            //--- Проверяем отрисовку подписей
      datetime mid_time = min_time + (max_time - min_time) / 2; //--- Рассчитываем среднее время
      double dk_mid = (double)(mid_time - min_time);            //--- Рассчитываем разность относительно среднего времени
      double mid_price = price_min + best_slope * dk_mid;       //--- Рассчитываем цену в средней точке
      double label_offset = 20 * _Point * (isSupport ? -1 : 1); //--- Рассчитываем смещение подписи
      double label_price = mid_price + label_offset;            //--- Рассчитываем цену размещения подписи
      int label_anchor = isSupport ? ANCHOR_TOP : ANCHOR_BOTTOM;//--- Задаем привязку подписи
      string label_text = type + " Trendline";                  //--- Задаем текст подписи
      string label_name = unique_name + "_label";               //--- Формируем имя подписи
      if (ObjectFind(0, label_name) < 0) {                      //--- Проверяем, существует ли подпись
         ObjectCreate(0, label_name, OBJ_TEXT, 0, mid_time, label_price); //--- Создаем подпись
         ObjectSetString(0, label_name, OBJPROP_TEXT, label_text); //--- Задаем текст
         ObjectSetInteger(0, label_name, OBJPROP_COLOR, clrBlack); //--- Задаем цвет
         ObjectSetInteger(0, label_name, OBJPROP_FONTSIZE, 8);  //--- Задаем размер шрифта
         ObjectSetInteger(0, label_name, OBJPROP_ANCHOR, label_anchor); //--- Задаем привязку
         ObjectSetDouble(0, label_name, OBJPROP_ANGLE, angle);  //--- Задаем угол
         ObjectSetInteger(0, label_name, OBJPROP_BACK, false);  //--- Выводим объект на передний план
      }
      color point_label_color = isSupport ? clrSaddleBrown : clrDarkGoldenrod; //--- Задаем цвет подписи точки
      double point_text_offset = 20.0 * _Point;    //--- Задаем смещение текста подписи точки
      for (int m = 0; m < touches; m++) {          //--- Перебираем касания
         int idx = touch_indices[m];               //--- Получаем индекс касания
         datetime tk_time = swings[idx].time;      //--- Получаем время касания
         double tk_price = swings[idx].price;      //--- Получаем цену касания
         double text_price;                        //--- Инициализируем цену размещения текста
         int point_text_anchor;                    //--- Инициализируем привязку текста
         if (isSupport) {                          //--- Обрабатываем поддержку
            text_price = tk_price - point_text_offset; //--- Размещаем текст ниже точки
            point_text_anchor = ANCHOR_LEFT;       //--- Задаем привязку слева
         } else {                                  //--- Обрабатываем сопротивление
            text_price = tk_price + point_text_offset; //--- Размещаем текст выше точки
            point_text_anchor = ANCHOR_BOTTOM;     //--- Задаем привязку снизу
         }
         string text_name = unique_name + "_point_label" + IntegerToString(m); //--- Формируем имя текстового объекта
         string point_text = "Pt " + IntegerToString(m + 1); //--- Задаем текст подписи точки
         if (ObjectFind(0, text_name) < 0) { //--- Проверяем, существует ли текстовый объект
            ObjectCreate(0, text_name, OBJ_TEXT, 0, tk_time, text_price); //--- Создаем текст
            ObjectSetString(0, text_name, OBJPROP_TEXT, point_text); //--- Задаем текст
            ObjectSetInteger(0, text_name, OBJPROP_COLOR, point_label_color); //--- Задаем цвет
            ObjectSetInteger(0, text_name, OBJPROP_FONTSIZE, 8); //--- Задаем размер шрифта
            ObjectSetInteger(0, text_name, OBJPROP_ANCHOR, point_text_anchor); //--- Задаем привязку
            ObjectSetDouble(0, text_name, OBJPROP_ANGLE, 0); //--- Задаем угол
            ObjectSetInteger(0, text_name, OBJPROP_BACK, false); //--- Выводим объект на передний план
         }
      }
   }
}

Реализуем логику обнаружения и визуализации линий тренда. В начале FindAndDrawTrendlines проверяет, есть ли в массиве trendlines активная линия типа isSupport. Если такая линия найдена, has_active устанавливается в true и функция завершает работу. Затем создается массив swings, в который копируются swingLows или swingHighs в зависимости от типа линии. Одновременно задаются цвет и префикс имени.

Далее рассчитываются допуски TouchTolerance и PenetrationTolerance, умноженные на _Point. Перебираются пары свинговых точек, для которых вычисляется initial_slope, а точки, находящиеся в пределах touch_tolerance, добавляются в touch_indices. Количество касаний проверяется по MinTouches, а расстояние между ними — по MinBarSpacing с помощью iBarShift  и ArraySort. Затем рассчитываются slope и intercept, после чего функции CalculateRSquared и ValidateTrendline оценивают каждую линию. Лучший вариант выбирается по максимальному числу касаний и наибольшему коэффициенту R-квадрат как мере качества аппроксимации. Если линия прошла проверку, она создается через ObjectCreate как объект OBJ_TREND с уникальным именем. Для нее задаются свойства OBJPROP_COLOR, OBJPROP_STYLE и параметры лучей, после чего сведения о линии — start_time, end_time, продленное на ExtensionBars, цены и индексы касаний — сохраняются в trendlines. Сначала с помощью IsStartingPointUsed проверяется, не использовалась ли начальная точка ранее; затем новая точка добавляется в startingPoints, чтобы исключить дублирование. Если DrawTouchArrows равно true, в точках касания создаются стрелки OBJ_ARROW соответствующего цвета и с подходящей привязкой.

Если DrawLabels равно true, в средней точке линии добавляется подпись OBJ_TEXT с текстом, обозначающим тип линии тренда. Ее угол задается через CalculateAngle. Для точек создаются подписи Pt 1 и т. д. с цветами clrSaddleBrown или clrDarkGoldenrod. В журнал выводятся параметры найденной линии. Остается организовать постоянное обновление существующих линий и проверку пересечений для формирования сигналов. Для простоты объединим эту логику в одной функции.

//+------------------------------------------------------------------+
//| Обновление линий тренда и проверка сигналов                      |
//+------------------------------------------------------------------+
void UpdateTrendlines() {
   datetime current_time = iTime(_Symbol, _Period, 0);     //--- Получаем текущее время
   double pointValue = _Point;                             //--- Получаем размер пункта
   double pen_tolerance = PenetrationTolerance * pointValue; //--- Рассчитываем допустимый выход за линию
   double touch_tolerance = TouchTolerance * pointValue;   //--- Рассчитываем допуск для касаний
   for (int i = numTrendlines - 1; i >= 0; i--) {          //--- Перебираем линии тренда в обратном порядке
      string type = trendlines[i].is_support ? "Support" : "Resistance"; //--- Определяем тип линии тренда
      string name = trendlines[i].name;                    //--- Получаем имя линии тренда
      if (current_time > trendlines[i].end_time) {         //--- Проверяем, утратила ли линия актуальность
         PrintFormat("%s trendline %s is no longer valid (expired). End time: %s, Current time: %s.", type, name, TimeToString(trendlines[i].end_time), TimeToString(current_time)); //--- Записываем в журнал утрату актуальности
         RemoveTrendlineFromStorage(i);                    //--- Удаляем линию тренда
         continue;                                         //--- Переходим к следующей линии
      }
      datetime prev_bar_time = iTime(_Symbol, _Period, 1); //--- Получаем время предыдущего бара
      double dk = (double)(prev_bar_time - trendlines[i].start_time); //--- Рассчитываем разность во времени
      double line_price = trendlines[i].start_price + trendlines[i].slope * dk; //--- Рассчитываем цену линии
      double prev_close = iClose(_Symbol, _Period, 1);     //--- Получаем цену закрытия предыдущего бара
      double prev_low = iLow(_Symbol, _Period, 1);         //--- Получаем минимум предыдущего бара
      double prev_high = iHigh(_Symbol, _Period, 1);       //--- Получаем максимум предыдущего бара
      bool broken = false;                                 //--- Инициализируем признак пробоя
      if (BreakoutType == BREAKOUT_CLOSE) {                //--- Проверяем пробой по закрытию свечи
         if (trendlines[i].is_support && prev_close < line_price) { //--- Пробой поддержки по закрытию
            PrintFormat("%s trendline %s is no longer valid (broken by close). Line price: %.5f, Prev close: %.5f.", type, name, line_price, prev_close); //--- Записываем пробой в журнал
            broken = true;                                 //--- Задаем признак пробоя
         } else if (!trendlines[i].is_support && prev_close > line_price) { //--- Пробой сопротивления по закрытию
            PrintFormat("%s trendline %s is no longer valid (broken by close). Line price: %.5f, Prev close: %.5f.", type, name, line_price, prev_close); //--- Записываем пробой в журнал
            broken = true;                                 //--- Задаем признак пробоя
         }
      } else if (BreakoutType == BREAKOUT_CANDLE) {        //--- Проверяем пробой всей свечой
         if (trendlines[i].is_support && prev_high < line_price) { //--- Вся свеча находится ниже линии поддержки
            PrintFormat("%s trendline %s is no longer valid (entire candle below). Line price: %.5f, Prev high: %.5f.", type, name, line_price, prev_high); //--- Записываем пробой в журнал
            broken = true;                                 //--- Задаем признак пробоя
         } else if (!trendlines[i].is_support && prev_low > line_price) { //--- Вся свеча находится выше линии сопротивления
            PrintFormat("%s trendline %s is no longer valid (entire candle above). Line price: %.5f, Prev low: %.5f.", type, name, line_price, prev_low); //--- Записываем пробой в журнал
            broken = true;                                  //--- Задаем признак пробоя
         }
      }
      if (broken && EnableTradingSignals && !trendlines[i].is_signaled) { //--- Проверяем наличие сигнала пробоя
         bool signaled = false;                           //--- Инициализируем признак сигнала
         string signal_type = "";                         //--- Инициализируем тип сигнала
         color signal_color = clrNONE;                    //--- Инициализируем цвет сигнала
         int arrow_code = 0;                              //--- Инициализируем код стрелки
         int anchor = 0;                                  //--- Инициализируем привязку
         double text_angle = 0.0;                         //--- Инициализируем угол наклона текста
         double text_offset = 0.0;                        //--- Инициализируем смещение текста
         double text_price = 0.0;                         //--- Инициализируем цену размещения текста
         int text_anchor = 0;                             //--- Инициализируем привязку текста
         if (trendlines[i].is_support) {                  //--- Пробой поддержки: продажа
            signaled = true;                              //--- Задаем признак сигнала
            signal_type = "SELL BREAK";                   //--- Задаем сигнал пробоя на продажу
            signal_color = clrRed;                        //--- Задаем красный цвет
            arrow_code = 218;                             //--- Задаем стрелку вниз
            anchor = ANCHOR_BOTTOM;                       //--- Задаем привязку снизу
            text_angle = 90.0;                            //--- Задаем вертикальное направление вниз
            text_offset = 20 * pointValue;                //--- Задаем смещение текста
            text_price = line_price + text_offset;        //--- Рассчитываем цену размещения текста
            text_anchor = ANCHOR_BOTTOM;                  //--- Задаем привязку снизу
            double Bid = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID), _Digits); //--- Получаем цену Bid
            double SL = NormalizeDouble(line_price + inpSLPoints * _Point, _Digits); //--- Устанавливаем стоп-лосс выше линии
            double risk = SL - Bid;                       //--- Рассчитываем риск
            double TP = NormalizeDouble(Bid - risk * inpRRRatio, _Digits); //--- Рассчитываем тейк-профит
            obj_Trade.Sell(inpLot, _Symbol, Bid, SL, TP); //--- Выполняем сделку на продажу
         } else {                                         //--- Пробой сопротивления: покупка
            signaled = true;                              //--- Задаем признак сигнала
            signal_type = "BUY BREAK";                    //--- Задаем сигнал пробоя на покупку
            signal_color = clrBlue;                       //--- Задаем синий цвет
            arrow_code = 217;                             //--- Задаем стрелку вверх
            anchor = ANCHOR_TOP;                          //--- Задаем привязку сверху
            text_angle = -90.0;                           //--- Задаем вертикальное направление вверх
            text_offset = -20 * pointValue;               //--- Задаем смещение текста
            text_price = line_price + text_offset;        //--- Рассчитываем цену размещения текста
            text_anchor = ANCHOR_LEFT;                    //--- Задаем привязку слева
            double Ask = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK), _Digits); //--- Получаем цену Ask
            double SL = NormalizeDouble(line_price - inpSLPoints * _Point, _Digits); //--- Устанавливаем стоп-лосс ниже линии
            double risk = Ask - SL;                       //--- Рассчитываем риск
            double TP = NormalizeDouble(Ask + risk * inpRRRatio, _Digits); //--- Рассчитываем тейк-профит
            obj_Trade.Buy(inpLot, _Symbol, Ask, SL, TP);  //--- Выполняем сделку на покупку
         }
         if (signaled) { //--- Проверяем, был ли уже сформирован сигнал
            PrintFormat("Breakout signal generated for %s trendline %s: %s at price %.5f, time %s.", type, name, signal_type, line_price, TimeToString(current_time)); //--- Записываем в журнал сигнал
            string arrow_name = name + "_signal_arrow"; //--- Формируем имя сигнальной стрелки
            if (ObjectFind(0, arrow_name) < 0) { //--- Проверяем, существует ли стрелка
               ObjectCreate(0, arrow_name, OBJ_ARROW, 0, prev_bar_time, line_price); //--- Создаем сигнальную стрелку
               ObjectSetInteger(0, arrow_name, OBJPROP_ARROWCODE, arrow_code); //--- Задаем код стрелки
               ObjectSetInteger(0, arrow_name, OBJPROP_ANCHOR, anchor); //--- Задаем привязку
               ObjectSetInteger(0, arrow_name, OBJPROP_COLOR, signal_color); //--- Задаем цвет
               ObjectSetInteger(0, arrow_name, OBJPROP_WIDTH, 1); //--- Задаем толщину
               ObjectSetInteger(0, arrow_name, OBJPROP_BACK, false); //--- Выводим объект на передний план
            }
            string text_name = name + "_signal_text"; //--- Формируем имя текста сигнала
            if (ObjectFind(0, text_name) < 0) { //--- Проверяем, существует ли текстовый объект
               ObjectCreate(0, text_name, OBJ_TEXT, 0, prev_bar_time, text_price); //--- Создаем текст сигнала
               ObjectSetString(0, text_name, OBJPROP_TEXT, " " + signal_type); //--- Задаем содержимое текста
               ObjectSetInteger(0, text_name, OBJPROP_COLOR, signal_color); //--- Задаем цвет
               ObjectSetInteger(0, text_name, OBJPROP_FONTSIZE, 10); //--- Задаем размер шрифта
               ObjectSetInteger(0, text_name, OBJPROP_ANCHOR, text_anchor); //--- Задаем привязку
               ObjectSetDouble(0, text_name, OBJPROP_ANGLE, text_angle); //--- Задаем угол
               ObjectSetInteger(0, text_name, OBJPROP_BACK, false); //--- Выводим объект на передний план
            }
            trendlines[i].is_signaled = true; //--- Задаем признак сигнала
         }
      }
      if (broken) {                           //--- Удаляем линию после пробоя
         RemoveTrendlineFromStorage(i);       //--- Удаляем линию тренда
      }
   }
}

В функции UpdateTrendlines реализуем обновление линий тренда и торговлю на пробоях. Сначала через iTime  получаем время текущего бара и рассчитываем pointValue, pen_tolerance = PenetrationTolerance * pointValue и touch_tolerance = TouchTolerance * pointValue. Затем массив trendlines перебирается в обратном порядке. Для каждой линии определяются тип линии — поддержка или сопротивление и имя, после чего проверяется срок ее действия по current_time. Если срок действия линии истек, информация выводится через PrintFormat, а линия удаляется функцией RemoveTrendlineFromStorage. 

Затем рассчитывается цена линии на предыдущем баре. Время prev_bar_time получаем через iTime, а цену — по формуле start_price + slope * (prev_bar_time - start_time). После этого проверяем пробой. При BreakoutType = BREAKOUT_CLOSE для линии поддержки анализируется, оказался ли prev_close iClose ниже line_price, а для сопротивления — выше. Результат выводится через PrintFormat, а broken устанавливается в true. При BREAKOUT_CANDLE для поддержки проверяется, находится ли prev_high, полученный через iHigh, ниже линии, а для сопротивления — находится ли prev_low iLow выше line_price. При выполнении условия линия также считается пробитой.

Если линия пробита, EnableTradingSignals равно true, а is_signaled равно false, задаются торговые параметры. Для пробоя поддержки формируется сигнал SELL BREAK («пробой вниз, сигнал на продажу») красного цвета со стрелкой вниз (код 218). Через SymbolInfoDouble получаем Bid, стоп-лосс устанавливаем на line_price + inpSLPoints * _Point, рассчитываем риск и тейк-профит по inpRRRatio, затем вызываем obj_Trade.Sell. Для пробоя сопротивления формируется сигнал BUY BREAK («пробой вверх, сигнал на покупку») синего цвета со стрелкой вверх (код 217). Аналогично получаем Ask, рассчитываем стоп-лосс и тейк-профит и вызываем obj_Trade.Buy. После этого через ObjectCreate создаются сигнальная стрелка OBJ_ARROW и текст OBJ_TEXT, задаются OBJPROP_ARROWCODE, OBJPROP_ANCHOR и OBJPROP_COLOR. Сигнал записывается в журнал через PrintFormat, is_signaled устанавливается в true, а пробитая линия удаляется из хранилища. Выбор кодов стрелок остается за разработчиком. Список доступных вариантов приведен среди кодов Wingdings, определенных в MQL5.

MQL5 WINGDINGS

Теперь вызовем эти функции в обработчике события  OnTick, чтобы система обрабатывала изменения на каждом тике.

//+------------------------------------------------------------------+
//| Тиковая функция советника                                        |
//+------------------------------------------------------------------+
void OnTick() {
   if (!IsNewBar()) return;      //--- Выходим, если новый бар не появился
   DetectSwings();               //--- Определяем свинги
   UpdateTrendlines();           //--- Обновляем линии тренда
   FindAndDrawTrendlines(true);  //--- Находим и рисуем линии тренда поддержки
}

В функции OnTick  сначала вызывается IsNewBar. Если нового бара нет, выполнение завершается, что снижает нагрузку. При появлении нового бара DetectSwings находит свинговые максимумы и минимумы, затем UpdateTrendlines проверяет пробои и линии с истекшим сроком действия и при необходимости открывает сделки. После этого FindAndDrawTrendlines вызывается со значением true для поиска и отрисовки линий поддержки. На графике отображаются только прошедшие проверку линии. После компиляции получаем следующий результат.

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

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

//--- Другие операции функции OnTick

FindAndDrawTrendlines(false);                   //--- Находим и рисуем линии сопротивления

//---

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

Подтвержденный сигнал пробоя сопротивления

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

ИТОГОВЫЙ РЕЗУЛЬТАТ

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


Тестирование

После тщательного бэктестинга получены следующие результаты.

График тестирования:

График

Отчет тестирования:

Отчет


Заключение

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

Предупреждение: статья предназначена исключительно для обучения. Торговля связана со значительными финансовыми рисками, а волатильность рынка может привести к убыткам. Перед использованием программы на реальном счете необходимо провести тщательное тестирование и настроить надежное управление рисками.

Реализовав эту стратегию пробоя линии тренда, вы сможете использовать направленные движения рынка и при необходимости дополнительно адаптировать систему под собственный стиль торговли. Успешной торговли!

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

Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (2)
linfo2
linfo2 | 1 окт. 2025 в 16:27
Большое спасибо за то, что поделились! Я действительно ценю, что вы поделились (всеми своими кодами) — надежный и хорошо комментированный код, отличный шаблон для дальнейшей разработки! Я сам пытался создать нечто подобное, но у меня, безусловно, не получилось так хорошо, как у вас.
Allan Munene Mutiiria
Allan Munene Mutiiria | 1 окт. 2025 в 22:05
linfo2 #:
Большое спасибо за то, что поделились! Я действительно ценю, что вы поделились (всеми своими кодами) — надёжный и хорошо комментированный код, отличный шаблон для дальнейшей разработки! Я сам пытался создать нечто подобное, но у меня получилось далеко не так хорошо, как у вас
Спасибо, что сочли это полезным. Не за что.
Автоматизация классических рыночных методов в MQL5 (Часть 3): Стадийный анализ Стэна Вайнштейна Автоматизация классических рыночных методов в MQL5 (Часть 3): Стадийный анализ Стэна Вайнштейна
В статье представлен полноценный торговый советник, построенный на методе стадийного анализа Стэна Вайнштейна. Советник классифицирует рынок по одной из четырёх стадий, анализируя наклон и положение 30-недельной скользящей средней, а также характер движения цены и динамику объёма. Затем он открывает длинные позиции только при прорывах в Стадию 2, а короткие — при пробоях вниз в Стадию 4. В статье объясняется каждая стадия, способы её программного определения и то, почему преимущество метода создаёт именно дисциплина — торговля только на подходящей стадии.
Возможности Мастера MQL5, которые вам нужно знать (Часть 95): Использование DSU и DBN в пользовательском классе сигнала Возможности Мастера MQL5, которые вам нужно знать (Часть 95): Использование DSU и DBN в пользовательском классе сигнала
В этой статье мы переключимся на пользовательский класс Мастер MQL5 (MQL5 Wizard), который анализирует сигналы входа. На этот раз наш пользовательский класс называется ‘CSignalDSUDBN’ и построен на сочетании алгоритма Disjoint Set Union (DSU) и сети Deep Belief Network (DBN). Как и в предыдущих статьях этой серии, нашу модель можно протестировать с использованием советников, собранных с помощью Мастера MQL5, которые можно настраивать с помощью различных трейлинг-стопов и классов управления капиталом.
Возможности Мастера MQL5, которые вам нужно знать (Часть 81): Использование паттернов Ишимоку и ADX Уайлдера в обучении на основе β-VAE-инференса Возможности Мастера MQL5, которые вам нужно знать (Часть 81): Использование паттернов Ишимоку и ADX Уайлдера в обучении на основе β-VAE-инференса
Эта статья продолжает часть 80, в которой мы рассмотрели сочетание индикаторов Ишимоку и ADX в рамках обучения с подкреплением. Теперь перейдем к обучению на основе инференса. Как уже отмечалось, Ишимоку и ADX дополняют друг друга. Однако мы вновь обратимся к выводам предыдущей статьи об использовании пайплайнов. Для обучения на основе инференса применяем β-вариант вариационного автокодировщика (Variational Auto Encoder, VAE). Также используем пользовательский класс сигнала, предназначенный для интеграции с Мастером MQL5.
Модульная архитектура индикаторов в MQL5 (Часть 1): Хватит копировать и вставлять — начните писать масштабируемый, переиспользуемый код Модульная архитектура индикаторов в MQL5 (Часть 1): Хватит копировать и вставлять — начните писать масштабируемый, переиспользуемый код
В статье разработан объектно-ориентированный каркас для индикаторов MQL5 путем преобразования примитивного примера в пригодные для постоянного использования модули. В ней формализуется частичный пересчет буфера в OnCalculate, логика переносится в классы, вынесенные в заголовочные файлы (CAppliedPrice, CSma), а также вводятся CSubIndiBase, CIndicatorBase и реестр для централизации требований. В результате получаются переносимые компоненты, изолированные входные параметры и аккуратно организованные буферы при минимуме шаблонного кода, что ускоряет сборку новых индикаторов и упрощает их обслуживание.