English Deutsch 日本語
preview
Автоматизация торговых стратегий на MQL5 (Часть 37): Поиск дивергенций и конвергенций RSI с визуализацией на графике

Автоматизация торговых стратегий на MQL5 (Часть 37): Поиск дивергенций и конвергенций RSI с визуализацией на графике

MetaTrader 5Трейдинг |
98 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

В нашей предыдущей статье (Часть 36), мы разработали торговую систему на основе зон спроса и предложения на языке MetaQuotes Language 5 (MQL5), которая определяла зоны спроса и предложения по диапазонам консолидации, подтверждала их импульсными движениями и открывала позиции при повторном тесте зон с подтверждением тренда и настраиваемыми параметрами риска. В Части 37 мы разработаем систему поиска обычных дивергенций RSI с визуализацией на графике. Система определяет обычные бычьи и медвежьи дивергенции между свинговыми точками цены и значениями индекса относительной силы (RSI), открывает позиции по сигналам с возможностью настройки параметров риск-менеджмента и добавляет на график визуальные элементы для более удобного анализа. Мы рассмотрим следующие темы:

  1. Принцип работы стратегии обычных дивергенций RSI
  2. Реализация на MQL5
  3. Бэктестирование
  4. Заключение

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


Принцип работы стратегии обычных дивергенций RSI

Стратегия обычных дивергенций RSI направлена на поиск возможных разворотов тренда по расхождениям между поведением цены и осциллятором индекса относительной силы (RSI), который измеряет силу ценового импульса. При бычьей дивергенции цена формирует более низкий минимум, а RSI — более высокий минимум, что указывает на ослабление нисходящего импульса и вероятный разворот вверх. При медвежьей дивергенции цена формирует более высокий максимум, а RSI — более низкий максимум, что указывает на ослабление восходящего импульса и возможный разворот вниз.

Для поиска этих моделей используются подтверждённые свинговые точки, определяемые по заданному числу баров. Чтобы считать дивергенцию «чистой», вводится допуск: промежуточные значения RSI не должны пересекать линию между двумя опорными точками. При обнаружении бычьей дивергенции открываем длинную позицию на закрытии подтверждающего бара, а при медвежьей дивергенции открываем короткую позицию на закрытии подтверждающего бара. Для управления риском используются заранее заданные уровни Stop Loss и Take Profit, а также динамический трейлинг-стоп. Ниже показаны возможные варианты формирования дивергенций.

Пример формирования бычьей дивергенции:

ОБЫЧНАЯ БЫЧЬЯ ДИВЕРГЕНЦИЯ

Пример формирования медвежьей дивергенции:

ОБЫЧНАЯ МЕДВЕЖЬЯ ДИВЕРГЕНЦИЯ

Мы будем искать свинговые максимумы и минимумы с подтверждением по числу соседних баров, проверять дивергенции в заданном диапазоне баров и с учётом допуска, автоматически открывать позиции с настраиваемым объёмом и параметрами риска, а также добавим цветные линии и подписи для удобного отслеживания. В итоге получим систему для торговли по дивергенциям. Ниже представлена её схема.

СХЕМА СТРАТЕГИИ


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

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

//+------------------------------------------------------------------+
//|                        RSI Regular Divergence Convergence 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>

//+------------------------------------------------------------------+
//| Входные параметры |
//+------------------------------------------------------------------+
input group "RSI Settings"
input int RSI_Period = 14;                          // Период RSI
input ENUM_APPLIED_PRICE RSI_Applied = PRICE_CLOSE; // Применяемая цена RSI

input group "Swing Settings"
input int Swing_Strength = 5;                       // Количество баров для подтверждения свингового максимума/минимума
input int Min_Bars_Between = 5;                     // Минимальное количество баров между свинговыми точками для дивергенции
input int Max_Bars_Between = 50;                    // Максимальное количество баров между свинговыми точками для дивергенции
input double Tolerance = 0.1;                       // Допуск для проверки «чистой» дивергенции

input group "Trade Settings"
input double Lot_Size = 0.01;                       // Фиксированный размер лота
input int Magic_Number = 123456789;                 // Магический номер
input double SL_Pips = 300.0;                       // Stop Loss в пунктах (pips); 0 — отключить
input double TP_Pips = 300.0;                       // Take Profit в пунктах (pips); 0 — отключить

input group "Trailing Stop Settings"
input bool Enable_Trailing_Stop = true;             // Включить трейлинг-стоп
input double Trailing_Stop_Pips = 30.0;             // Трейлинг-стоп в пунктах (pips)
input double Min_Profit_To_Trail_Pips = 50.0;       // Минимальная прибыль в пунктах (pips) для запуска трейлинг-стопа

input group "Visualization"
input color Bull_Color = clrGreen;                  // Цвет бычьей дивергенции
input color Bear_Color = clrRed;                    // Цвет медвежьей дивергенции
input color Swing_High_Color = clrRed;              // Цвет подписей свинговых максимумов
input color Swing_Low_Color = clrGreen;             // Цвет подписей свинговых минимумов
input int Line_Width = 2;                           // Толщина линии дивергенции
input ENUM_LINE_STYLE Line_Style = STYLE_SOLID;     // Стиль линии дивергенции
input int Font_Size = 8;                            // Размер шрифта подписей свинговых точек

//+------------------------------------------------------------------+
//| Дескрипторы индикаторов и торговый объект |
//+------------------------------------------------------------------+
int RSI_Handle = INVALID_HANDLE;                   //--- Дескриптор индикатора RSI
CTrade obj_Trade;                                  //--- Торговый объект для управления позициями

Начнём с подключения торговой библиотеки директивой «#include<Trade\Trade.mqh>», которая предоставляет встроенные средства для управления позициями и ордерами. Затем объявим входные параметры, сгруппированные по категориям, для удобной настройки. В группе «RSI Settings» параметр «RSI_Period» равен 14 и задаёт период расчёта RSI, а «RSI_Applied» имеет значение PRICE_CLOSE чтобы рассчитывать индикатор по ценам закрытия. В группе «Swing Settings» параметр «Swing_Strength» равен 5 и задаёт количество баров для подтверждения свинговых максимумов и минимумов. «Min_Bars_Between» и «Max_Bars_Between» ограничивают расстояние между свингами диапазоном от 5 до 50 баров, а «Tolerance» со значением 0,1 задаёт небольшой допуск для проверки чистой дивергенции.

В группе «Trade Settings» параметр «Lot_Size» равен 0,01 и задаёт фиксированный объём позиции, «Magic_Number» со значением 123456789 служит идентификатором сделок, а «SL_Pips» и «TP_Pips» со значением 300,0 задают расстояния Stop Loss и Take Profit; при значении 0 соответствующий уровень отключается. В группе «Trailing Stop Settings» параметр «Enable_Trailing_Stop» включает трейлинг-стоп, «Trailing_Stop_Pips» задаёт расстояние 30 пунктов, а «Min_Profit_To_Trail_Pips» — минимальную прибыль 50 пунктов для его запуска. В группе «Visualization» задаются цвета линий и подписей, ширина и стиль линий дивергенции, а также размер шрифта.

Наконец, объявим глобальные переменные: «RSI_Handle», инициализированную значением «INVALID_HANDLE» для хранения дескриптора RSI, и «obj_Trade» — экземпляр класса CTrade для выполнения торговых операций. Далее объявим и инициализируем глобальные переменные свинговых точек.

//+------------------------------------------------------------------+
//| Переменные свинговых точек |
//+------------------------------------------------------------------+
double Last_High_Price = 0.0;                      //--- Цена последнего свингового максимума
datetime Last_High_Time = 0;                       //--- Время последнего свингового максимума
double Prev_High_Price = 0.0;                      //--- Цена предыдущего свингового максимума
datetime Prev_High_Time = 0;                       //--- Время предыдущего свингового максимума
double Last_Low_Price = 0.0;                       //--- Цена последнего свингового минимума
datetime Last_Low_Time = 0;                        //--- Время последнего свингового минимума
double Prev_Low_Price = 0.0;                       //--- Цена предыдущего свингового минимума
datetime Prev_Low_Time = 0;                        //--- Время предыдущего свингового минимума
double Last_High_RSI = 0.0;                        //--- Значение RSI последнего свингового максимума
double Prev_High_RSI = 0.0;                        //--- Значение RSI предыдущего свингового максимума
double Last_Low_RSI = 0.0;                         //--- Значение RSI последнего свингового минимума
double Prev_Low_RSI = 0.0;                         //--- Значение RSI предыдущего свингового минимума

В разделе «Переменные свинговых точек» объявим глобальные переменные для хранения последней и предыдущей свинговой точки отдельно для максимумов и минимумов. Переменные «Last_High_Price», «Last_High_Time», «Prev_High_Price» и «Prev_High_Time» хранят цены и время свинговых максимумов; аналогичные переменные с «Low» — данные свинговых минимумов. Значения RSI в этих точках сохраняются в «Last_High_RSI», «Prev_High_RSI», «Last_Low_RSI» и «Prev_Low_RSI». Все переменные инициализируются нулём и затем динамически обновляются при поиске дивергенций. Теперь инициализируем советник и индикатор RSI, а также найдём подокно RSI для последующей отрисовки объектов.

//+------------------------------------------------------------------+
//| Функция инициализации советника |
//+------------------------------------------------------------------+
int OnInit() {
   RSI_Handle = iRSI(_Symbol, _Period, RSI_Period, RSI_Applied); //--- Создаём дескриптор RSI
   if (RSI_Handle == INVALID_HANDLE) {             //--- Проверяем ошибку создания RSI
      Print("Failed to create RSI indicator");     //--- Выводим ошибку в журнал
      return(INIT_FAILED);                         //--- Возвращаем ошибку инициализации
   }
   long chart_id = ChartID();                      //--- Получаем идентификатор текущего графика
   string rsi_name = "RSI(" + IntegerToString(RSI_Period) + ")"; //--- Формируем имя индикатора RSI
   int rsi_subwin = ChartWindowFind(chart_id, rsi_name); //--- Находим подокно RSI
   if (rsi_subwin == -1) {                         //--- Проверяем отсутствие подокна RSI
      if (!ChartIndicatorAdd(chart_id, 1, RSI_Handle)) { //--- Добавляем RSI в подокно графика
         Print("Failed to add RSI indicator to chart"); //--- Выводим ошибку в журнал
      }
   }
   obj_Trade.SetExpertMagicNumber(Magic_Number);   //--- Задаём магический номер торгового объекта
   Print("RSI Divergence EA initialized");         //--- Выводим сообщение об успешной инициализации
   return(INIT_SUCCEEDED);                         //--- Возвращаем успешную инициализацию
}

В обработчике OnInit сначала создадим дескриптор RSI с помощью iRSI, передав текущий символ, таймфрейм, период RSI и тип применяемой цены. Затем проверим, не равен ли «RSI_Handle» значению «INVALID_HANDLE». При ошибке выведем сообщение через «Print» и вернём «INIT_FAILED». После этого получим идентификатор текущего графика с помощью ChartID и сформируем имя индикатора RSI, объединив строку «RSI(» с периодом, преобразованным функцией IntegerToString .

Подокно RSI найдём с помощью ChartWindowFind по идентификатору графика и имени. Если подокно не найдено («rsi_subwin» == -1), добавим индикатор в подокно 1 через ChartIndicatorAdd. При ошибке выведем соответствующее сообщение. Затем вызовем «obj_Trade.SetExpertMagicNumber(Magic_Number)», чтобы присвоить позициям и торговым операциям советника уникальный идентификатор. В завершение выведем сообщение об успешной инициализации и вернём «INIT_SUCCEEDED». После запуска видим следующий результат.

ИНИЦИАЛИЗАЦИЯ

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

//+------------------------------------------------------------------+
//| Проверка свингового максимума |
//+------------------------------------------------------------------+
bool CheckSwingHigh(int bar, double& highs[]) {
   if (bar < Swing_Strength || bar + Swing_Strength >= ArraySize(highs)) return false; //--- Возвращаем false, если индекс бара выходит за допустимые границы
   double current = highs[bar];                    //--- Получаем текущий максимум
   for (int i = 1; i <= Swing_Strength; i++) {     //--- Перебираем соседние бары
      if (highs[bar - i] >= current || highs[bar + i] >= current) return false; //--- Возвращаем false, если это не свинговый максимум
   }
   return true;                                    //--- Возвращаем true для свингового максимума
}

//+------------------------------------------------------------------+
//| Проверка свингового минимума |
//+------------------------------------------------------------------+
bool CheckSwingLow(int bar, double& lows[]) {
   if (bar < Swing_Strength || bar + Swing_Strength >= ArraySize(lows)) return false; //--- Возвращаем false, если индекс бара выходит за допустимые границы
   double current = lows[bar];                     //--- Получаем текущий минимум
   for (int i = 1; i <= Swing_Strength; i++) {     //--- Перебираем соседние бары
      if (lows[bar - i] <= current || lows[bar + i] <= current) return false; //--- Возвращаем false, если это не свинговый минимум
   }
   return true;                                    //--- Возвращаем true для свингового минимума
}

Функция «CheckSwingHigh» принимает индекс бара и ссылку на массив максимумов и определяет, является ли указанный бар свинговым максимумом. Сначала она проверяет границы массива с учётом «Swing_Strength» через ArraySize чтобы избежать ошибок индексации. Затем получает текущий максимум и сравнивает его с соседними барами слева и справа в пределах «Swing_Strength». Если хотя бы один соседний максимум не ниже текущего, возвращается false; иначе — true.

Аналогично создадим функцию «CheckSwingLow» для минимумов. Она проверяет допустимость индекса и убеждается, что минимумы соседних баров выше текущего значения. True возвращается только для подтверждённого свингового минимума. Далее создадим функцию проверки чистой дивергенции RSI.

//+------------------------------------------------------------------+
//| Проверка чистой дивергенции |
//+------------------------------------------------------------------+
bool CleanDivergence(double rsi1, double rsi2, int shift1, int shift2, double& rsi_data[], bool bearish) {
   if (shift1 <= shift2) return false;             //--- Возвращаем false при некорректных сдвигах
   for (int b = shift2 + 1; b < shift1; b++) {    //--- Перебираем бары между сдвигами
      double interp_factor = (double)(b - shift2) / (shift1 - shift2); //--- Рассчитываем коэффициент интерполяции
      double interp_rsi = rsi2 + interp_factor * (rsi1 - rsi2); //--- Рассчитываем интерполированное значение RSI
      if (bearish) {                               //--- Проверяем медвежью дивергенцию
         if (rsi_data[b] > interp_rsi + Tolerance) return false; //--- Возвращаем false, если RSI превышает линию с учётом допуска
      } else {                                     //--- Проверяем бычью дивергенцию
         if (rsi_data[b] < interp_rsi - Tolerance) return false; //--- Возвращаем false, если RSI опускается ниже линии с учётом допуска
      }
   }
   return true;                                    //--- Возвращаем true, если дивергенция чистая
}

Функция «CleanDivergence» проверяет, что промежуточные значения RSI не пересекают линию между двумя свинговыми точками, то есть дивергенция остаётся чистой. Она принимает значения RSI в точках «rsi1» и «rsi2», соответствующие сдвиги «shift1» и «shift2», ссылку на массив «rsi_data» и логический параметр «bearish», определяющий тип дивергенции.

Сначала проверим корректность сдвигов, затем переберём промежуточные бары от «shift2 + 1» до «shift1 - 1». Для каждого бара рассчитываются коэффициент интерполяции «interp_factor» и линейно интерполированное значение «interp_rsi». При медвежьей дивергенции функция возвращает false, если «rsi_data[b]» выше «interp_rsi + Tolerance»; при бычьей — если значение ниже «interp_rsi - Tolerance». Если нарушений нет, возвращается true. Теперь необходимые функции готовы, и эту логику можно использовать в обработчике OnTick. Начнём с медвежьей дивергенции.

//+------------------------------------------------------------------+
//| Обработчик тиков советника |
//+------------------------------------------------------------------+
void OnTick() {
   static datetime last_time = 0;                //--- Сохраняем время последнего обработанного бара
   datetime current_time = iTime(_Symbol, _Period, 0); //--- Получаем время текущего бара
   if (current_time == last_time) return;        //--- Выходим, если новый бар не появился
   last_time = current_time;                     //--- Обновляем последнее время
   int data_size = 200;                          //--- Задаём количество анализируемых баров
   double high_data[], low_data[], rsi_data[];   //--- Объявляем массивы максимумов, минимумов и RSI
   datetime time_data[];                         //--- Объявляем массив времени
   CopyHigh(_Symbol, _Period, 0, data_size, high_data); //--- Копируем максимумы
   CopyLow(_Symbol, _Period, 0, data_size, low_data); //--- Копируем минимумы
   CopyTime(_Symbol, _Period, 0, data_size, time_data); //--- Копируем значения времени
   CopyBuffer(RSI_Handle, 0, 0, data_size, rsi_data); //--- Копируем значения RSI
   ArraySetAsSeries(high_data, true);            //--- Преобразуем массив максимумов в таймсерию
   ArraySetAsSeries(low_data, true);             //--- Преобразуем массив минимумов в таймсерию
   ArraySetAsSeries(time_data, true);            //--- Преобразуем массив времени в таймсерию
   ArraySetAsSeries(rsi_data, true);             //--- Преобразуем массив RSI в таймсерию
   long chart_id = ChartID();                    //--- Получаем идентификатор текущего графика
   // Находим два последних свинговых максимума
   int last_high_bar = -1, prev_high_bar = -1;   //--- Инициализируем индексы свинговых максимумов
   for (int b = 1; b < data_size - Swing_Strength; b++) { //--- Перебираем бары
      if (CheckSwingHigh(b, high_data)) {        //--- Проверяем свинговый максимум
         if (last_high_bar == -1) {              //--- Проверяем первый свинговый максимум
            last_high_bar = b;                   //--- Задаём индекс последнего максимума
         } else {                                //--- Найден второй свинговый максимум
            prev_high_bar = b;                   //--- Задаём индекс предыдущего максимума
            break;                               //--- Выходим из цикла
         }
      }
   }
   if (last_high_bar > 0 && time_data[last_high_bar] > Last_High_Time) { //--- Проверяем новый свинговый максимум
      Prev_High_Price = Last_High_Price;          //--- Обновляем цену предыдущего максимума
      Prev_High_Time = Last_High_Time;            //--- Обновляем время предыдущего максимума
      Last_High_Price = high_data[last_high_bar]; //--- Задаём цену последнего максимума
      Last_High_Time = time_data[last_high_bar];  //--- Задаём время последнего максимума
      Prev_High_RSI = Last_High_RSI;              //--- Обновляем RSI предыдущего максимума
      Last_High_RSI = rsi_data[last_high_bar];    //--- Задаём RSI последнего максимума
      string high_type = "H";                     //--- Задаём тип максимума по умолчанию
      if (Prev_High_Price > 0.0) {                //--- Проверяем наличие предыдущего максимума
         high_type = (Last_High_Price > Prev_High_Price) ? "HH" : "LH"; //--- Задаём тип максимума
      }
      bool higher_high = Last_High_Price > Prev_High_Price; //--- Проверяем наличие более высокого максимума
      bool lower_rsi_high = Last_High_RSI < Prev_High_RSI; //--- Проверяем наличие более низкого максимума RSI
      int bars_diff = prev_high_bar - last_high_bar; //--- Рассчитываем количество баров между максимумами
      bool bear_div = false;                     //--- Инициализируем флаг медвежьей дивергенции
      if (Prev_High_Price > 0.0 && higher_high && lower_rsi_high && bars_diff >= Min_Bars_Between && bars_diff <= Max_Bars_Between) { //--- Проверяем условия медвежьей дивергенции
         if (CleanDivergence(Prev_High_RSI, Last_High_RSI, prev_high_bar, last_high_bar, rsi_data, true)) { //--- Проверяем чистую дивергенцию
            bear_div = true;                    //--- Устанавливаем флаг медвежьей дивергенции
            // Рисуем линии дивергенции
            string line_name = "DivLine_Bear_" + TimeToString(Last_High_Time); //--- Задаём имя линии дивергенции
            ObjectCreate(chart_id, line_name, OBJ_TREND, 0, Prev_High_Time, Prev_High_Price, Last_High_Time, Last_High_Price); //--- Создаём линию ценовой дивергенции
            ObjectSetInteger(chart_id, line_name, OBJPROP_COLOR, Bear_Color); //--- Задаём цвет линии
            ObjectSetInteger(chart_id, line_name, OBJPROP_WIDTH, Line_Width); //--- Задаём ширину линии
            ObjectSetInteger(chart_id, line_name, OBJPROP_STYLE, Line_Style); //--- Задаём стиль линии
            ObjectSetInteger(chart_id, line_name, OBJPROP_RAY, false); //--- Отключаем луч
            ObjectSetInteger(chart_id, line_name, OBJPROP_BACK, false); //--- Выводим на передний план
            int rsi_window = ChartWindowFind(chart_id, "RSI(" + IntegerToString(RSI_Period) + ")"); //--- Находим подокно RSI
            if (rsi_window != -1) {              //--- Проверяем, найдено ли подокно RSI
               string rsi_line = "DivLine_RSI_Bear_" + TimeToString(Last_High_Time); //--- Задаём имя линии дивергенции RSI
               ObjectCreate(chart_id, rsi_line, OBJ_TREND, rsi_window, Prev_High_Time, Prev_High_RSI, Last_High_Time, Last_High_RSI); //--- Создаём линию дивергенции RSI
               ObjectSetInteger(chart_id, rsi_line, OBJPROP_COLOR, Bear_Color); //--- Задаём цвет линии
               ObjectSetInteger(chart_id, rsi_line, OBJPROP_WIDTH, Line_Width); //--- Задаём ширину линии
               ObjectSetInteger(chart_id, rsi_line, OBJPROP_STYLE, Line_Style); //--- Задаём стиль линии
               ObjectSetInteger(chart_id, rsi_line, OBJPROP_RAY, false); //--- Отключаем луч
               ObjectSetInteger(chart_id, rsi_line, OBJPROP_BACK, false); //--- Выводим на передний план
            }
         }
      }
      // Рисуем подпись свинговой точки
      string swing_name = "SwingHigh_" + TimeToString(Last_High_Time); //--- Задаём имя подписи свингового максимума
      if (ObjectFind(chart_id, swing_name) < 0) { //--- Проверяем наличие подписи
         ObjectCreate(chart_id, swing_name, OBJ_TEXT, 0, Last_High_Time, Last_High_Price); //--- Создаём подпись свингового максимума
         ObjectSetString(chart_id, swing_name, OBJPROP_TEXT, " " + high_type + (bear_div ? " Bear Div" : "")); //--- Задаём текст подписи
         ObjectSetInteger(chart_id, swing_name, OBJPROP_COLOR, Swing_High_Color); //--- Задаём цвет подписи
         ObjectSetInteger(chart_id, swing_name, OBJPROP_ANCHOR, ANCHOR_LEFT_LOWER); //--- Задаём точку привязки подписи
         ObjectSetInteger(chart_id, swing_name, OBJPROP_FONTSIZE, Font_Size); //--- Задаём размер шрифта подписи
      }
      ChartRedraw(chart_id);                    //--- Перерисовываем график
   }
}

В обработчике OnTick используем статическую переменную «last_time» для хранения времени последнего обработанного бара. Время текущего бара получаем через iTime и завершаем функцию, если оно не изменилось. Затем обновляем «last_time». Задаём количество анализируемых баров: параметр «data_size» равен 200, но при необходимости это значение можно изменить. Объявляем массивы «high_data», «low_data», «rsi_data» и «time_data» и заполняем их с помощью CopyHigh, «CopyLow», «CopyTime» и CopyBuffer, получая недавние максимумы, минимумы, временные метки и значения RSI. Затем преобразуем массивы в таймсерии с помощью ArraySetAsSeries и получаем «chart_id» через «ChartID» для последующей отрисовки объектов.

Для поиска последних свинговых максимумов переменным «last_high_bar» и «prev_high_bar» присваиваем -1, после чего перебираем бары и вызываем «CheckSwingHigh», пока не найдём два последних подтверждённых максимума. При появлении нового максимума обновляем предыдущие значения и записываем новые «Last_High_Price», «Last_High_Time» и «Last_High_RSI». Тип максимума хранится в «high_type»: «H» по умолчанию, «HH» для более высокого максимума и «LH» для более низкого.

Затем проверяем условия медвежьей дивергенции: предыдущий максимум должен существовать, цена должна сформировать более высокий максимум, RSI — более низкий максимум, а расстояние между точками должно находиться в пределах «Min_Bars_Between» и «Max_Bars_Between». После этого вызываем «CleanDivergence» в медвежьем режиме. Если дивергенция чистая, устанавливаем «bear_div» в true и создаём на основном графике линию «DivLine_Bear_» с помощью ObjectCreate соединяя предыдущий и последний максимумы. Цвет, ширина, стиль, отключение луча и вывод на передний план задаются через ObjectSetInteger .

Подокно RSI находим через «ChartWindowFind». Если оно найдено, создаём в нём аналогичную линию «DivLine_RSI_Bear_», соединяющую значения RSI. Затем проверяем наличие подписи «SwingHigh_» через «ObjectFind». Если объекта нет, создаём его в точке максимума, задаём текст с «high_type» и отметкой «Bear Div», цвет «Swing_High_Color», привязку «ANCHOR_LEFT_LOWER» и размер шрифта «Font_Size». После этого обновляем график функцией ChartRedraw. После компиляции видим следующий результат.

МЕДВЕЖЬЯ ДИВЕРГЕНЦИЯ

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

//+------------------------------------------------------------------+
//| Открытие позиции на покупку |
//+------------------------------------------------------------------+
void OpenBuy() {
   double ask_price = SymbolInfoDouble(_Symbol, SYMBOL_ASK); //--- Получаем текущую цену Ask
   double sl = (SL_Pips > 0) ? NormalizeDouble(ask_price - SL_Pips * _Point, _Digits) : 0; //--- Рассчитываем Stop Loss, если он включён
   double tp = (TP_Pips > 0) ? NormalizeDouble(ask_price + TP_Pips * _Point, _Digits) : 0; //--- Рассчитываем Take Profit, если он включён
   if (obj_Trade.PositionOpen(_Symbol, ORDER_TYPE_BUY, Lot_Size, 0, sl, tp)) { //--- Пытаемся открыть позицию на покупку
      Print("Buy trade opened on bullish divergence"); //--- Записываем открытие покупки в журнал
   } else {                                        //--- Обрабатываем ошибку открытия
      Print("Failed to open Buy: ", obj_Trade.ResultRetcodeDescription()); //--- Выводим ошибку в журнал
   }
}

//+------------------------------------------------------------------+
//| Открытие позиции на продажу |
//+------------------------------------------------------------------+
void OpenSell() {
   double bid_price = SymbolInfoDouble(_Symbol, SYMBOL_BID); //--- Получаем текущую цену Bid
   double sl = (SL_Pips > 0) ? NormalizeDouble(bid_price + SL_Pips * _Point, _Digits) : 0; //--- Рассчитываем Stop Loss, если он включён
   double tp = (TP_Pips > 0) ? NormalizeDouble(bid_price - TP_Pips * _Point, _Digits) : 0; //--- Рассчитываем Take Profit, если он включён
   if (obj_Trade.PositionOpen(_Symbol, ORDER_TYPE_SELL, Lot_Size, 0, sl, tp)) { //--- Пытаемся открыть позицию на продажу
      Print("Sell trade opened on bearish divergence"); //--- Записываем открытие продажи в журнал
   } else {                                        //--- Обрабатываем ошибку открытия
      Print("Failed to open Sell: ", obj_Trade.ResultRetcodeDescription()); //--- Выводим ошибку в журнал
   }
}

//+------------------------------------------------------------------+
//| Закрытие всех позиций |
//+------------------------------------------------------------------+
void CloseAll() {
   for (int p = PositionsTotal() - 1; p >= 0; p--) { //--- Перебираем позиции в обратном порядке
      if (PositionGetTicket(p) > 0 && PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == Magic_Number) { //--- Проверяем параметры позиции
         obj_Trade.PositionClose(PositionGetTicket(p)); //--- Закрываем позицию
      }
   }
}

Поскольку советник будет открывать позиции обоих направлений, создадим все необходимые функции. «OpenBuy» открывает длинную позицию при бычьей дивергенции. Хотя логика бычьего сигнала будет добавлена позже, функция уже понадобится. Сначала получаем текущую цену Ask через SymbolInfoDouble . Если «SL_Pips» больше нуля, Stop Loss рассчитывается как «ask_price - SL_Pips * _Point» и нормализуется по количеству знаков символа функцией NormalizeDouble; иначе устанавливается 0. Аналогично Take Profit рассчитывается как «ask_price + TP_Pips * _Point» или отключается нулевым значением. Позиция на покупку открывается через «obj_Trade.PositionOpen» с типом ORDER_TYPE_BUY, объёмом «Lot_Size» и рассчитанными SL и TP. Результат операции выводится через «Print», а при ошибке — описание из «obj_Trade.ResultRetcodeDescription».

Функция «OpenSell» аналогичным образом открывает короткую позицию по медвежьему сигналу. Она получает цену Bid, рассчитывает Stop Loss как «bid_price + SL_Pips * _Point», Take Profit как «bid_price - TP_Pips * _Point» и вызывает «obj_Trade.PositionOpen» с типом «ORDER_TYPE_SELL». Результат также записывается в журнал.

Наконец, реализуем функцию «CloseAll», которая закрывает существующие позиции перед открытием новых. Для этого перебираем позиции в обратном порядке от «PositionsTotal()- 1» до нуля. Для каждой позиции проверяем действительность тикета, соответствие текущему символу через «PositionGetString(POSITION_SYMBOL)» и магическому номеру «Magic_Number». Подходящие позиции закрываются через «obj_Trade.PositionClose». При появлении сигнала сначала вызывается «CloseAll», а затем функция открытия позиции соответствующего направления. Для медвежьей дивергенции добавим следующий код.

// Обнаружена медвежья дивергенция — сигнал на продажу
CloseAll();                         //--- Закрываем все открытые позиции
OpenSell();                         //--- Открываем позицию на продажу

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

КОРОТКАЯ ПОЗИЦИЯ

Теперь советник может торговать по сигналу медвежьей дивергенции. Добавим аналогичную логику для бычьей дивергенции.

// Находим два последних свинговых минимума
int last_low_bar = -1, prev_low_bar = -1; //--- Инициализируем индексы свинговых минимумов
for (int b = 1; b < data_size - Swing_Strength; b++) { //--- Перебираем бары
   if (CheckSwingLow(b, low_data)) {      //--- Проверяем свинговый минимум
      if (last_low_bar == -1) {           //--- Проверяем первый свинговый минимум
         last_low_bar = b;                //--- Задаём индекс последнего минимума
      } else {                            //--- Найден второй свинговый минимум
         prev_low_bar = b;                //--- Задаём индекс предыдущего минимума
         break;                           //--- Выходим из цикла
      }
   }
}
if (last_low_bar > 0 && time_data[last_low_bar] > Last_Low_Time) { //--- Проверяем новый свинговый минимум
   Prev_Low_Price = Last_Low_Price;       //--- Обновляем цену предыдущего минимума
   Prev_Low_Time = Last_Low_Time;         //--- Обновляем время предыдущего минимума
   Last_Low_Price = low_data[last_low_bar]; //--- Задаём цену последнего минимума
   Last_Low_Time = time_data[last_low_bar]; //--- Задаём время последнего минимума
   Prev_Low_RSI = Last_Low_RSI;           //--- Обновляем RSI предыдущего минимума
   Last_Low_RSI = rsi_data[last_low_bar]; //--- Задаём RSI последнего минимума
   string low_type = "L";                 //--- Задаём тип минимума по умолчанию
   if (Prev_Low_Price > 0.0) {            //--- Проверяем наличие предыдущего минимума
      low_type = (Last_Low_Price < Prev_Low_Price) ? "LL" : "HL"; //--- Задаём тип минимума
   }
   bool lower_low = Last_Low_Price < Prev_Low_Price; //--- Проверяем наличие более низкого минимума
   bool higher_rsi_low = Last_Low_RSI > Prev_Low_RSI; //--- Проверяем наличие более высокого минимума RSI
   int bars_diff = prev_low_bar - last_low_bar; //--- Рассчитываем количество баров между минимумами
   bool bull_div = false;                 //--- Инициализируем флаг бычьей дивергенции
   if (Prev_Low_Price > 0.0 && lower_low && higher_rsi_low && bars_diff >= Min_Bars_Between && bars_diff <= Max_Bars_Between) { //--- Проверяем условия бычьей дивергенции
      if (CleanDivergence(Prev_Low_RSI, Last_Low_RSI, prev_low_bar, last_low_bar, rsi_data, false)) { //--- Проверяем чистую дивергенцию
         bull_div = true;                 //--- Устанавливаем флаг бычьей дивергенции
         // Обнаружена бычья дивергенция — сигнал на покупку
         CloseAll();                      //--- Закрываем все открытые позиции
         OpenBuy();                       //--- Открываем позицию на покупку
         // Рисуем линии дивергенции
         string line_name = "DivLine_Bull_" + TimeToString(Last_Low_Time); //--- Задаём имя линии дивергенции
         ObjectCreate(chart_id, line_name, OBJ_TREND, 0, Prev_Low_Time, Prev_Low_Price, Last_Low_Time, Last_Low_Price); //--- Создаём линию ценовой дивергенции
         ObjectSetInteger(chart_id, line_name, OBJPROP_COLOR, Bull_Color); //--- Задаём цвет линии
         ObjectSetInteger(chart_id, line_name, OBJPROP_WIDTH, Line_Width); //--- Задаём ширину линии
         ObjectSetInteger(chart_id, line_name, OBJPROP_STYLE, Line_Style); //--- Задаём стиль линии
         ObjectSetInteger(chart_id, line_name, OBJPROP_RAY, false); //--- Отключаем луч
         ObjectSetInteger(chart_id, line_name, OBJPROP_BACK, false); //--- Выводим на передний план
         int rsi_window = ChartWindowFind(chart_id, "RSI(" + IntegerToString(RSI_Period) + ")"); //--- Находим подокно RSI
         if (rsi_window != -1) {          //--- Проверяем, найдено ли подокно RSI
            string rsi_line = "DivLine_RSI_Bull_" + TimeToString(Last_Low_Time); //--- Задаём имя линии дивергенции RSI
            ObjectCreate(chart_id, rsi_line, OBJ_TREND, rsi_window, Prev_Low_Time, Prev_Low_RSI, Last_Low_Time, Last_Low_RSI); //--- Создаём линию дивергенции RSI
            ObjectSetInteger(chart_id, rsi_line, OBJPROP_COLOR, Bull_Color); //--- Задаём цвет линии
            ObjectSetInteger(chart_id, rsi_line, OBJPROP_WIDTH, Line_Width); //--- Задаём ширину линии
            ObjectSetInteger(chart_id, rsi_line, OBJPROP_STYLE, Line_Style); //--- Задаём стиль линии
            ObjectSetInteger(chart_id, rsi_line, OBJPROP_RAY, false); //--- Отключаем луч
            ObjectSetInteger(chart_id, rsi_line, OBJPROP_BACK, false); //--- Выводим на передний план
         }
      }
   }
   // Рисуем подпись свинговой точки
   string swing_name = "SwingLow_" + TimeToString(Last_Low_Time); //--- Задаём имя подписи свингового минимума
   if (ObjectFind(chart_id, swing_name) < 0) { //--- Проверяем наличие подписи
      ObjectCreate(chart_id, swing_name, OBJ_TEXT, 0, Last_Low_Time, Last_Low_Price); //--- Создаём подпись свингового минимума
      ObjectSetString(chart_id, swing_name, OBJPROP_TEXT, " " + low_type + (bull_div ? " Bull Div" : "")); //--- Задаём текст подписи
      ObjectSetInteger(chart_id, swing_name, OBJPROP_COLOR, Swing_Low_Color); //--- Задаём цвет подписи
      ObjectSetInteger(chart_id, swing_name, OBJPROP_ANCHOR, ANCHOR_LEFT_UPPER); //--- Задаём точку привязки подписи
      ObjectSetInteger(chart_id, swing_name, OBJPROP_FONTSIZE, Font_Size); //--- Задаём размер шрифта подписи
   }
   ChartRedraw(chart_id);                 //--- Перерисовываем график
}

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

ПОЗИЦИЯ ПО БЫЧЬЕЙ ДИВЕРГЕНЦИИ

Как видно на изображении, советник также открывает позиции по бычьим дивергенциям. Далее добавим трейлинг-стоп для сопровождения прибыли. Для этого вынесем логику в отдельную функцию — так код останется модульным.

//+------------------------------------------------------------------+
//| Применение трейлинг-стопа к позициям                                 |
//+------------------------------------------------------------------+
void ApplyTrailingStop() {
   double point = _Point;                         //--- Получаем размер пункта инструмента
   for (int i = PositionsTotal() - 1; i >= 0; i--) { //--- Перебираем позиции в обратном порядке
      if (PositionGetTicket(i) > 0) {             //--- Проверяем корректность тикета
         if (PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == Magic_Number) { //--- Проверяем символ и магический номер
            double sl = PositionGetDouble(POSITION_SL); //--- Получаем текущий Stop Loss
            double tp = PositionGetDouble(POSITION_TP); //--- Получаем текущий Take Profit
            double openPrice = PositionGetDouble(POSITION_PRICE_OPEN); //--- Получаем цену открытия
            ulong ticket = PositionGetInteger(POSITION_TICKET); //--- Получаем тикет позиции
            if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { //--- Проверяем позицию на покупку
               double newSL = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID) - Trailing_Stop_Pips * point, _Digits); //--- Рассчитываем новый Stop Loss
               if (newSL > sl && SymbolInfoDouble(_Symbol, SYMBOL_BID) - openPrice > Min_Profit_To_Trail_Pips * point) { //--- Проверяем условия трейлинг-стопа
                  obj_Trade.PositionModify(ticket, newSL, tp); //--- Изменяем позицию, устанавливая новый Stop Loss
               }
            } else if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) { //--- Проверяем позицию на продажу
               double newSL = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK) + Trailing_Stop_Pips * point, _Digits); //--- Рассчитываем новый Stop Loss
               if (newSL < sl && openPrice - SymbolInfoDouble(_Symbol, SYMBOL_ASK) > Min_Profit_To_Trail_Pips * point) { //--- Проверяем условия трейлинг-стопа
                  obj_Trade.PositionModify(ticket, newSL, tp); //--- Изменяем позицию, устанавливая новый Stop Loss
               }
            }
         }
      }
   }
}

Функция «ApplyTrailingStop» динамически изменяет Stop Loss открытых позиций после достижения заданного порога прибыли, фиксируя результат по мере движения рынка. Сначала присваиваем переменной «point» значение «_Point», затем перебираем все позиции в обратном порядке от «PositionsTotal() - 1» до нуля, чтобы безопасно изменять их без смещения индексов.

Для каждой позиции функция PositionGetTicket с параметром i должна вернуть действительный тикет больше нуля. Затем проверяем символ через PositionGetString с параметром «POSITION_SYMBOL» и сравниваем его с _Symbol, а также магический номер через «PositionGetInteger(POSITION_MAGIC)». Затем получаем текущие значения «sl», «tp», цену открытия «openPrice» и тикет позиции.

Если тип позиции, полученный через PositionGetInteger с параметром «POSITION_TYPE», равен POSITION_TYPE_BUY, рассчитываем «newSL» как текущую цену Bid минус «Trailing_Stop_Pips * point» и нормализуем значение. Если новый Stop Loss выше текущего, а прибыль превышает «Min_Profit_To_Trail_Pips * point», позиция изменяется через «obj_Trade.PositionModify» с сохранением Take Profit.

Для позиции на продажу, когда «POSITION_TYPE» равен POSITION_TYPE_SELL, новый Stop Loss рассчитывается как цена Ask плюс «Trailing_Stop_Pips * point». Если он ниже текущего Stop Loss, а прибыль превышает минимальный порог, позиция изменяется через «obj_Trade.PositionModify». Так трейлинг-стоп применяется только к подходящим позициям. Теперь вызовем функцию в обработчике тиков.

if (Enable_Trailing_Stop && PositionsTotal() > 0) {               //--- Проверяем, включён ли трейлинг-стоп
   ApplyTrailingStop();                   //--- Применяем трейлинг-стоп к позициям
}

Если есть открытые позиции и трейлинг-стоп включён, вызываем функцию на каждом тике. Получаем следующий результат.

ТРЕЙЛИНГ-СТОП

Основная логика завершена. Остаётся удалить графические объекты при снятии советника с графика, чтобы не оставлять лишние элементы.

//+------------------------------------------------------------------+
//| Функция деинициализации советника |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   if (RSI_Handle != INVALID_HANDLE) IndicatorRelease(RSI_Handle); //--- Освобождаем дескриптор RSI, если он действителен
   ObjectsDeleteAll(0, "DivLine_");                //--- Удаляем все линии дивергенции
   ObjectsDeleteAll(0, "SwingHigh_");              //--- Удаляем все объекты свинговых максимумов
   ObjectsDeleteAll(0, "SwingLow_");               //--- Удаляем все объекты свинговых минимумов
   Print("RSI Divergence EA deinitialized");       //--- Записываем деинициализацию в журнал
}

В обработчике OnDeinit проверяем, что «RSI_Handle» не равен INVALID_HANDLE, после чего освобождаем ресурсы индикатора вызовом IndicatorRelease с аргументом «RSI_Handle». Затем удаляем графические объекты через ObjectsDeleteAll с префиксами «DivLine_», «SwingHigh_» и «SwingLow_». В завершение выводим подтверждающее сообщение через Print о завершении работы советника. На этом реализация завершена; далее перейдём к бэктестированию программы.


Бэктестирование

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

График бэктеста:

ГРАФИК

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

ОТЧЁТ


Заключение

В результате мы разработали систему поиска обычных дивергенций RSI на MQL5. Она определяет бычьи и медвежьи дивергенции по свинговым максимумам и минимумам с подтверждением силы, проверяет чистоту сигналов в заданном диапазоне баров с учётом допуска и открывает позиции фиксированным объёмом с необязательными Stop Loss, Take Profit и трейлинг-стопом.

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

Теперь у вас есть рабочая основа для эффективной торговли по сигналам обычных дивергенций RSI, готовая к дальнейшей оптимизации. Успешной торговли!

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

Прикрепленные файлы |
Возможности Мастера MQL5, которые вам нужно знать (Часть 83): Использование паттернов стохастического осциллятора и FrAMA — поведенческие архетипы Возможности Мастера MQL5, которые вам нужно знать (Часть 83): Использование паттернов стохастического осциллятора и FrAMA — поведенческие архетипы
Стохастический осциллятор и фрактальная адаптивная скользящая средняя образуют еще одну пару взаимодополняющих индикаторов, которую можно использовать в советнике MQL5. Стохастический осциллятор позволяет точно выявлять изменения моментума, а FrAMA подтверждает преобладающие тренды. Как и прежде, для изучения этой пары индикаторов используем Мастер MQL5, чтобы создать советник и проверить ее потенциал.
Как построить мультивалютную netting-сетку с точным CatBoost и swap/carry-слоем в MQL5 Как построить мультивалютную netting-сетку с точным CatBoost и swap/carry-слоем в MQL5
В статье разбирается мультивалютный netting-советник для MetaTrader 5 с валютным графом из 28 пар, набором из 19 M1‑признаков и двумя моделями CatBoost, выполняемыми в MQL5 без упрощений. Показано, как прогнозы управляют сеткой лимитных заявок, режимными фильтрами, инвентарём и swap/carry при учёте брокерских ограничений, а также как обеспечить воспроизводимость через self‑test и проверку совпадения Python и MQL5.
Изучение Стандартной библиотеки MQL5 (Часть 1): Интеграция и связывание компонентов Изучение Стандартной библиотеки MQL5 (Часть 1): Интеграция и связывание компонентов
Сегодня мы делаем важный шаг к тому, чтобы помочь каждому разработчику понять, как читать структуры классов и быстро создавать советники с помощью стандартной библиотеки MQL5. Библиотека богата и расширяема, но при этом может показаться, что вам вручили сложный набор инструментов без руководства. Здесь мы делимся и обсуждаем альтернативный алгоритм интеграции — краткий, повторяемый рабочий сценарий, демонстрирующий, как надежно связывать классы в реальных проектах.
Кооперация бионических алгоритмов — Co-Operation of Biology Related Algorithms (COBRA) Кооперация бионических алгоритмов — Co-Operation of Biology Related Algorithms (COBRA)
В статье реализована кооперативная метаэвристика COBRA во фреймворке C_AO: шесть популяционных методов (PSO, WPS, FFA, CSA, BA, FSS) работают в одной популяции при фиксированном бюджете обращений к целевой функции и экспериментально выясняем, на чём на самом деле держится кооперация. В комплекте — расширяемый класс для сборки собственных кооперативных портфелей из любых алгоритмов серии.