English Deutsch 日本語
preview
Автоматизация торговых стратегий в MQL5 (Часть 44): Обнаружение смены характера рынка (CHoCH) по пробоям свинговых экстремумов

Автоматизация торговых стратегий в MQL5 (Часть 44): Обнаружение смены характера рынка (CHoCH) по пробоям свинговых экстремумов

MetaTrader 5Торговые системы |
63 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

В предыдущей статье (Часть 43) мы разработали на языке MetaQuotes Language 5 (MQL5) адаптивную стратегию на основе канала линейной регрессии. Она рассчитывала линии регрессии с полосами отклонения, активировалась только при достаточном наклоне, динамически продлевала или перестраивала канал при отклонениях цены, поддерживала обычный и обратный режимы, открывала позиции при пробое границ канала из внутренней области, закрывала их при пересечении средней линии, ограничивала число одновременно открытых позиций и отображала закрашенные зоны с подписями и стрелками. В Части 44 мы создадим систему обнаружения смены характера рынка (Change of Character, CHoCH) по пробоям свинговых максимумов и минимумов.

Система сканирует бары, определяет свинговые максимумы и минимумы и присваивает им обозначения HH/LH/LL/HL для определения тренда. Позиции открываются при пробоях, указывающих на разворот: покупки — выше максимумов при нисходящем тренде, продажи — ниже минимумов при восходящем тренде. Предусмотрены режимы обработки по барам и по тикам, фиксированные торговые уровни с настраиваемым соотношением риска и прибыли, ограничение числа одновременно открытых позиций, трейлинг-стоп, а также визуализация со значками, подписями, линиями пробоя и адаптивным размером шрифта. Рассмотрим следующие темы:

  1. Общие принципы стратегии Change of Character (CHoCH)
  2. Реализация на MQL5
  3. Тестирование в тестере стратегий
  4. Заключение

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


Общие принципы стратегии Change of Character (CHoCH)

Change of Character (CHoCH) — это концепция Price Action, указывающая на возможный разворот тренда, когда цена пробивает недавний свинговый максимум или минимум вопреки сложившемуся направлению рынка. Мы определяем свинговые максимумы — точки, расположенные выше окружающих баров, — и свинговые минимумы, расположенные ниже окружающих баров. Затем сравниваем их с предыдущим свингом: максимумы обозначаются как HH (более высокий максимум) или LH (более низкий максимум), а минимумы — как LL (более низкий минимум) или HL (более высокий минимум).

Последовательность HH/HL указывает на восходящий тренд, а LH/LL — на нисходящий. CHoCH возникает, когда при нисходящем тренде цена закрывается выше последнего свингового максимума — это бычий разворот, — либо при восходящем тренде закрывается ниже последнего свингового минимума — это медвежий разворот. Это указывает на смену структуры, при которой контроль переходит к покупателям или продавцам. При нисходящем тренде, определяемом по LH или LL, бычий CHoCH подтверждается закрытием цены выше последнего свингового максимума: открывается позиция на покупку, а Stop Loss размещается ниже уровня пробоя. При восходящем тренде, определяемом по HH или HL, медвежий CHoCH подтверждается закрытием ниже последнего свингового минимума: открывается позиция на продажу со Stop Loss выше уровня и Take Profit ниже цены входа.

Мы будем сканировать бары при формировании каждой новой свечи, определять точки свинга и присваивать им метки HH/LH/LL/HL, устанавливать текущее направление тренда по последовательности меток, открывать покупки CHoCH при пробое максимумов в нисходящем тренде и продажи при пробое минимумов в восходящем тренде. Также ограничим общее количество открытых позиций, будем использовать фиксированные уровни Stop Loss и Take Profit в пунктах с настраиваемым соотношением риска и прибыли (R:R), добавим необязательный трейлинг-стоп на основе пунктов после достижения минимальной прибыли и визуализируем свинги цветными пиктограммами и подписями, а пробои CHoCH — линиями со стрелками и текстом. Размер шрифта будет динамически изменяться вместе с масштабом графика. Ниже показана схема наших целей.

СХЕМА CHoCH


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

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

//+------------------------------------------------------------------+
//|                                                     CHoCH EA.mq5 |
//|                           Copyright 2025, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, Allan Munene Mutiiria."
#property link      "https://t.me/Forex_Algo_Trader"
#property version   "1.00"

#include <Trade/Trade.mqh>

//+------------------------------------------------------------------+
//| Глобальные переменные                                            |
//+------------------------------------------------------------------+
CTrade obj_Trade;                                                 //--- Торговый объект
int   object_code     = 174;                                      //--- Код объекта
int   current_font_size = 10;                                     //--- Текущий размер шрифта
long  magic_number    = 123456789;                                //--- Магический номер

//+------------------------------------------------------------------+
//| Перечисления                                                     |
//+------------------------------------------------------------------+
enum TradeMode {                                                  // Определение перечисления торговых режимов
   PerBar,                                                        // По барам
   PerTick                                                        // По тикам
};

enum TrailingTypeEnum {                                           // Определение перечисления типов трейлинг-стопа
   Trailing_None   = 0,                                           // Нет
   Trailing_Points = 2                                            // По пунктам
};

//+------------------------------------------------------------------+
//| Входные параметры                                                |
//+------------------------------------------------------------------+
input group "EA GENERAL SETTINGS"
input double inpLot             = 0.01;                           // Размер лота
input int    sl_pts             = 300;                            // Stop Loss в пунктах
input int    tp_pts             = 300;                            // Take Profit в пунктах
input double r2r_ratio          = 
1 ;                              // Соотношение риска и прибыли
input int    totalTrades        = 1;                              // Максимальное число открытых позиций
input color  def_clr_up         = clrBlue;                        // Цвет свингового максимума
input color  def_clr_down       = clrRed;                         // Цвет свингового минимума
input int    ext_bars           = 5;                              // Глубина сканирования CHoCH в барах
input bool   prt                = true;                           // Вывод сообщений
input int    width              = 2;                              // Толщина
input TradeMode trade_mode      = PerBar;                         // Торговый режим
input TrailingTypeEnum TrailingType = Trailing_None;              // Тип трейлинг-стопа
input double Trailing_Stop_Pips = 30.0;                           // Трейлинг-стоп в пипсах (для типа по пунктам)
input double Min_Profit_To_Trail_Pips = 50.0;                     // Минимальная прибыль для запуска трейлинг-стопа в пипсах

Сначала подключим торговую библиотеку директивой «#include <Trade/Trade.mqh>», которая предоставляет класс CTrade для выполнения торговых операций и работы с позициями. Затем объявим несколько глобальных переменных: obj_Trade — экземпляр CTrade для управления позициями; object_code со значением 174 — код символа Wingdings, используемого при визуализации; current_font_size со стартовым значением 10 — текущий размер динамически масштабируемого текста; magic_number со значением 123456789 — уникальный идентификатор позиций советника. При необходимости эти значения можно вынести во входные параметры. Далее определим два перечисления. TradeMode содержит режим PerBar для обнаружения пробоев по закрытиям баров и PerTick для обнаружения пробоев в реальном времени на каждом тике. TrailingTypeEnum содержит Trailing_None для отключения сопровождения и Trailing_Points для включения трейлинг-стопа по пунктам.

Входные параметры объединены в группу «EA GENERAL SETTINGS», чтобы упорядочить их отображение в окне свойств. inpLot задаёт размер лота; sl_pts и tp_pts — расстояния Stop Loss и Take Profit в пунктах; r2r_ratio — множитель соотношения риска и прибыли; в текущей реализации он используется в расчёте Stop Loss, хотя при желании эту схему можно изменить; totalTrades ограничивает число одновременно открытых позиций; def_clr_up и def_clr_down задают цвета свинговых максимумов — синий — и минимумов — красный; ext_bars определяет глубину сканирования CHoCH; prt включает вывод сообщений в журнал; width задаёт толщину линий; trade_mode выбирает обработку по барам или тикам; TrailingType задаёт тип трейлинг-стопа; Trailing_Stop_Pips определяет расстояние сопровождения, а Min_Profit_To_Trail_Pips — минимальную прибыль, после которой оно активируется. В названиях переменных используется слово Pips, однако в данной реализации расчёт ведётся через _Point; далее для единообразия будем говорить «в пунктах».

//+------------------------------------------------------------------+
//| Функция обновления размеров шрифта                               |
//+------------------------------------------------------------------+
void UpdateFontSizes() {
   long scale = 0;                                                //--- Инициализация масштаба
   if (ChartGetInteger(0, CHART_SCALE, 0, scale)) {               //--- Получение масштаба
      current_font_size = (int)(7 + scale * 0.7);                 //--- Расчёт размера шрифта
      if (current_font_size < 6) current_font_size = 6;           //--- Минимальный размер шрифта
      if (current_font_size > 15) current_font_size = 15;         //--- Максимальный размер шрифта
      for (int i = ObjectsTotal(0, -1, -1) - 1; i >= 0; i--) {    //--- Перебор объектов
         string name = ObjectName(0, i, -1, -1);                  //--- Получение имени
         long type = ObjectGetInteger(0, name, OBJPROP_TYPE);     //--- Получение типа
         if (type == OBJ_TEXT) {                                  //--- Проверка текстового объекта
            ObjectSetInteger(0, name, OBJPROP_FONTSIZE, current_font_size); //--- Установка размера шрифта
         }
      }
      ChartRedraw(0);                                             //--- Перерисовка графика
   }
}

//+------------------------------------------------------------------+
//| Функция максимума                                                |
//+------------------------------------------------------------------+
double high(int index) {
   return (iHigh(_Symbol,_Period,index));                         //--- Возврат максимума
}

//+------------------------------------------------------------------+
//| Функция минимума                                                 |
//+------------------------------------------------------------------+
double low(int index) {
   return (iLow(_Symbol,_Period,index));                          //--- Возврат минимума
}

//+------------------------------------------------------------------+
//| Функция цены закрытия                                            |
//+------------------------------------------------------------------+
double close(int index) {
   return (iClose(_Symbol,_Period,index));                        //--- Возврат цены закрытия
}

//+------------------------------------------------------------------+
//| Функция времени                                                  |
//+------------------------------------------------------------------+
datetime time(int index) {
   return (iTime(_Symbol,_Period,index));                         //--- Возврат времени
}

//+------------------------------------------------------------------+
//| Отрисовка точки свинга                                           |
//+------------------------------------------------------------------+
void drawSwingPoint(string objName,datetime time,double price,int arrCode,
   color clr,int direction,string label) {
   UpdateFontSizes();                                             //--- Обновление размеров шрифта
   if (ObjectFind(0,objName) < 0) {                               //--- Проверка отсутствия объекта
      // Отрисовка пиктограммы как OBJ_TEXT с Wingdings
      string iconName = objName + "_icon";                        //--- Имя пиктограммы
      ObjectCreate(0,iconName,OBJ_TEXT,0,time,price);             //--- Создание пиктограммы
      ObjectSetString(0,iconName,OBJPROP_FONT,"Wingdings");       //--- Установка шрифта
      ObjectSetInteger(0,iconName,OBJPROP_FONTSIZE,current_font_size); //--- Установка размера
      ObjectSetString(0,iconName,OBJPROP_TEXT,CharToString((uchar)arrCode)); //--- Установка текста
      ObjectSetInteger(0,iconName,OBJPROP_COLOR,clr);             //--- Установка цвета
      if (direction == 1){
         ObjectSetInteger(0,iconName,OBJPROP_ANCHOR,ANCHOR_RIGHT_UPPER);   //--- Установка точки привязки
      }
      else if (direction == -1){
         ObjectSetInteger(0,iconName,OBJPROP_ANCHOR,ANCHOR_RIGHT_LOWER);   //--- Установка точки привязки
      }
      // Отрисовка текстовой подписи
      string txt = label;                                         //--- Установка текста
      string objNameDescr = objName + txt;                        //--- Имя подписи
      ObjectCreate(0,objNameDescr,OBJ_TEXT,0,time,price);         //--- Создание подписи
      ObjectSetString(0,objNameDescr,OBJPROP_FONT,"Arial");       //--- Установка шрифта
      ObjectSetInteger(0,objNameDescr,OBJPROP_COLOR,clr);         //--- Установка цвета
      ObjectSetInteger(0,objNameDescr,OBJPROP_FONTSIZE,current_font_size); //--- Установка размера
      ObjectSetString(0,objNameDescr,OBJPROP_TEXT,txt);           //--- Установка текста
      if (direction == 1){
         ObjectSetInteger(0,objNameDescr,OBJPROP_ANCHOR,ANCHOR_LEFT_UPPER); //--- Установка точки привязки
      }
      else if (direction == -1){
         ObjectSetInteger(0,objNameDescr,OBJPROP_ANCHOR,ANCHOR_LEFT_LOWER); //--- Установка точки привязки
      }
   }
   ChartRedraw(0);                                                //--- Перерисовка графика
}

//+------------------------------------------------------------------+
//| Отрисовка уровня пробоя                                          |
//+------------------------------------------------------------------+
void drawBreakLevel(string objName,datetime time1,double price1,
   datetime time2,double price2,color clr,int direction) {
   UpdateFontSizes();                                              //--- Обновление размеров шрифта
   if (ObjectFind(0,objName) < 0) {                                //--- Проверка отсутствия объекта
      ObjectCreate(0,objName,OBJ_ARROWED_LINE,0,time1,price1,time2,price2); //--- Создание линии со стрелкой
      ObjectSetInteger(0,objName,OBJPROP_TIME,0,time1);            //--- Установка времени 1
      ObjectSetDouble(0,objName,OBJPROP_PRICE,0,price1);           //--- Установка цены 1
      ObjectSetInteger(0,objName,OBJPROP_TIME,1,time2);            //--- Установка времени 2
      ObjectSetDouble(0,objName,OBJPROP_PRICE,1,price2);           //--- Установка цены 2
      ObjectSetInteger(0,objName,OBJPROP_COLOR,clr);               //--- Установка цвета
      ObjectSetInteger(0,objName,OBJPROP_WIDTH,width);             //--- Установка толщины
      string txt = "CHoCH";                                        //--- Установка текста
      string objNameDescr = objName + txt;                         //--- Имя подписи
      ObjectCreate(0,objNameDescr,OBJ_TEXT,0,time2,price2);        //--- Создание подписи
      ObjectSetInteger(0,objNameDescr,OBJPROP_COLOR,clr);          //--- Установка цвета
      ObjectSetInteger(0,objNameDescr,OBJPROP_FONTSIZE,current_font_size); //--- Установка размера
      if (direction > 0) {                                         //--- Проверка положительного направления
         ObjectSetInteger(0,objNameDescr,OBJPROP_ANCHOR,ANCHOR_RIGHT_UPPER); //--- Установка точки привязки
         ObjectSetString(0,objNameDescr,OBJPROP_TEXT, " " + txt);  //--- Установка текста
      }
      if (direction < 0) {                                         //--- Проверка отрицательного направления
         ObjectSetInteger(0,objNameDescr,OBJPROP_ANCHOR,ANCHOR_RIGHT_LOWER); //--- Установка точки привязки
         ObjectSetString(0,objNameDescr,OBJPROP_TEXT, " " + txt);  //--- Установка текста
      }
   }
   ChartRedraw(0);                                                 //--- Перерисовка графика
}

Сначала реализуем вспомогательную функцию UpdateFontSizes, которая динамически изменяет размер текста в зависимости от текущего масштаба графика, чтобы подписи оставались читаемыми при увеличении и уменьшении масштаба. Переменная scale инициализируется нулём, после чего функция ChartGetInteger с параметром CHART_SCALE получает масштаб графика. При успешном выполнении current_font_size рассчитывается как 7 плюс 70% значения масштаба и ограничивается диапазоном от 6 до 15. Затем программа в обратном порядке перебирает все объекты графика с помощью ObjectsTotal, передавая -1 для всех окон и типов, получает имя каждого объекта через ObjectName и его тип через ObjectGetInteger и OBJPROP_TYPE. Для всех объектов OBJ_TEXT размер шрифта устанавливается равным current_font_size с помощью ObjectSetInteger и OBJPROP_FONTSIZE, после чего график перерисовывается функцией ChartRedraw.

Далее создадим простые функции-обёртки для быстрого доступа к данным баров: high возвращает максимум по заданному индексу через iHigh для текущего символа и периода; low возвращает минимум через iLow; close — цену закрытия через iClose; time — время открытия бара через iTime. Это упрощает код обнаружения и отрисовки свингов и устраняет повторяющиеся вызовы. Функция drawSwingPoint визуализирует обнаруженные свинговые максимумы и минимумы. Сначала вызывается UpdateFontSizes, затем через ObjectFind проверяется наличие объекта с переданным именем. Если его нет, создаётся пиктограмма Wingdings в виде OBJ_TEXT с суффиксом _icon в заданных координатах времени и цены. Для неё задаются шрифт Wingdings, размер current_font_size, символ из arrCode через CharToString, цвет clr и точка привязки: справа сверху для минимумов при direction = 1 или справа снизу для максимумов при direction = -1.

Затем создаётся текстовая подпись с переданным значением label, например HH или LL, как ещё один объект OBJ_TEXT с соответствующим суффиксом. Для неё используются шрифт Arial, тот же цвет и размер, текст метки и точка привязки слева сверху или слева снизу в зависимости от направления. После этого график перерисовывается. Если вы впервые сталкиваетесь с кодами Wingdings, ниже приведена таблица кодов символов MQL5.

КОДЫ WINGDINGS В MQL5

Функция drawBreakLevel предназначена для обозначения пробоев CHoCH. Сначала вызывается UpdateFontSizes. Если объект ещё не существует, создаётся линия OBJ_ARROWED_LINE от точки time1/price1 до time2/price2. Координаты обеих точек явно задаются через OBJPROP_TIME и OBJPROP_PRICE, после чего устанавливаются переданные цвет и толщина width. В точке time2/price2 создаётся текстовая подпись CHoCH как объект OBJ_TEXT с тем же цветом и размером current_font_size. При положительном направлении она привязывается справа сверху, при отрицательном — справа снизу; перед текстом добавляется пробел для визуального отступа. Имея в распоряжении эти функции, мы готовы перейти к основной реализации. Сначала зададим магический номер при инициализации и обновим подписи.

//+------------------------------------------------------------------+
//| Функция инициализации советника                                  |
//+------------------------------------------------------------------+
int OnInit() {
   obj_Trade.SetExpertMagicNumber(magic_number);                  //--- Установка магического номера
   UpdateFontSizes();                                             //--- Обновление размеров шрифта
   return(INIT_SUCCEEDED);                                        //--- Возврат успешного результата
}

Обработчик OnInit выполняется один раз при загрузке программы или её присоединении к графику. Сначала magic_number передаётся объекту obj_Trade через SetExpertMagicNumber, чтобы все позиции советника получали уникальный идентификатор. Затем вызывается UpdateFontSizes для первоначальной настройки размеров текста по текущему масштабу графика. Возврат INIT_SUCCEEDED подтверждает успешную инициализацию. Инициализация выполняется достаточно просто. Следующим шагом определим тренд по свинговым точкам.

//+------------------------------------------------------------------+
//| Функция обработки тиков советника                                |
//+------------------------------------------------------------------+
void OnTick() {
   static bool isNewBar = false;                                  //--- Флаг нового бара
   int currBars = iBars(_Symbol,_Period);                         //--- Получение текущего числа баров
   static int prevBars = currBars;                                //--- Предыдущее число баров
   if (prevBars == currBars) {                                    //--- Проверка отсутствия нового бара
      isNewBar = false;                                           //--- Сброс флага нового бара
   } else if (prevBars != currBars) {                             //--- Проверка нового бара
      isNewBar = true;                                            //--- Установка флага нового бара
      prevBars = currBars;                                        //--- Обновление предыдущего значения
   }
   const int length = ext_bars;                                   //--- Установка длины
   const int limit = ext_bars;                                    //--- Установка предела
   int right_index, left_index;                                   //--- Индексы
   bool isSwingHigh = true, isSwingLow = true;                    //--- Флаги свингов
   static double current_swing_high = -1.0, current_swing_low = -1.0; //--- Текущие свинги
   static datetime swing_high_time = 0, swing_low_time = 0;       //--- Время свингов
   static int current_trend = 0;                                  //--- Текущий тренд (1 — вверх, -1 — вниз, 0 — не определён)
   int curr_bar = limit;                                          //--- Установка текущего бара
   if (isNewBar) {                                                //--- Проверка нового бара
      UpdateFontSizes();                                          //--- Обновление размеров шрифта
      for (int j=1; j<=length; j++) {                             //--- Перебор длины
         right_index = curr_bar - j;                              //--- Расчёт правого индекса
         left_index = curr_bar + j;                               //--- Расчёт левого индекса
         if ( (high(curr_bar) <= high(right_index)) || (high(curr_bar) < high(left_index)) ) { //--- Проверка, что максимум не свинговый
            isSwingHigh = false;                                  //--- Сброс флага свингового максимума
         }
         if ( (low(curr_bar) >= low(right_index)) || (low(curr_bar) > low(left_index)) ) { //--- Проверка, что минимум не свинговый
            isSwingLow = false;                                   //--- Сброс флага свингового минимума
         }
      }
      if (isSwingHigh) {                                          //--- Проверка свингового максимума
         double new_high = high(curr_bar);                        //--- Получение нового максимума
         string label = "H";                                      //--- Инициализация метки
         color clr = def_clr_up;                                  //--- Установка цвета
         if (current_swing_high > 0) {                            //--- Проверка существующего максимума
            if (new_high > current_swing_high) {                  //--- Проверка более высокого максимума
               label = "HH";                                      //--- Установка HH
               current_trend = 1;                                 //--- Установка восходящего тренда
            } else {                                              //--- Более низкий
               label = "LH";                                      //--- Установка LH
               clr = def_clr_down;                                //--- Установка цвета нисходящего движения
               current_trend = -1;                                //--- Установка нисходящего тренда
            }
         }
         if (prt) {                                               //--- Проверка вывода сообщений
            Print("SWING HIGH @ BAR INDEX ",curr_bar," of High: ",new_high, " Label: ",label); //--- Вывод максимума в журнал
         }
         drawSwingPoint(TimeToString(time(curr_bar)),time(curr_bar),new_high,object_code,clr,-1,label); //--- Отрисовка максимума
         current_swing_high = new_high;                           //--- Обновление максимума
         swing_high_time = time(curr_bar);                        //--- Обновление времени
      }
      if (isSwingLow) {                                           //--- Проверка свингового минимума
         double new_low = low(curr_bar);                          //--- Получение нового минимума
         string label = "L";                                      //--- Инициализация метки
         color clr = def_clr_down;                                //--- Установка цвета
         if (current_swing_low > 0) {                             //--- Проверка существующего минимума
            if (new_low < current_swing_low) {                    //--- Проверка более низкого минимума
               label = "LL";                                      //--- Установка LL
               current_trend = -1;                                //--- Установка нисходящего тренда
            } else {                                              //--- Более высокий
               label = "HL";                                      //--- Установка HL
               clr = def_clr_up;                                  //--- Установка цвета восходящего движения
               current_trend = 1;                                 //--- Установка восходящего тренда
            }
         }
         if (prt) {                                               //--- Проверка вывода сообщений
            Print("SWING LOW @ BAR INDEX ",curr_bar," of Low: ",new_low, " Label: ",label); //--- Вывод минимума в журнал
         }
         drawSwingPoint(TimeToString(time(curr_bar)),time(curr_bar),new_low,object_code,clr,1,label); //--- Отрисовка минимума
         current_swing_low = new_low;                             //--- Обновление минимума
         swing_low_time = time(curr_bar);                         //--- Обновление времени
      }
   }
}

В функции OnTick статические переменные isNewBar и prevBars используются для обнаружения нового бара. Текущее количество баров для символа и периода получается через iBars и сохраняется в currBars, после чего сравнивается с prevBars. Если значение не изменилось, isNewBar устанавливается в false; если увеличилось — в true, а prevBars обновляется. Благодаря этому ресурсоёмкое сканирование свингов выполняется только один раз после формирования нового завершённого бара. Переменным length и limit присваивается входное значение ext_bars, объявляются индексы для проверки баров слева и справа, а логические флаги isSwingHigh и isSwingLow инициализируются значением true. Статические переменные хранят последние current_swing_high и current_swing_low, время соответствующих свингов и current_trend: 1 — восходящий тренд, -1 — нисходящий, 0 — направление не определено. В качестве проверяемого бара curr_bar используется limit — обычно самый ранний бар в окне сканирования.

Если isNewBar равен true, сначала вызывается UpdateFontSizes, а затем цикл от 1 до length проверяет, является ли curr_bar подтверждённой свинговой точкой. Для максимума его значение должно превышать максимумы баров справа — более новых — и слева — более старых; если хотя бы одно условие не выполнено, isSwingHigh становится false. Аналогично для минимума: его значение должно быть ниже окружающих минимумов, иначе isSwingLow становится false. Если свинговый максимум подтверждён, его цена сохраняется в new_high, исходная метка задаётся как H, а цвет — def_clr_up. При наличии предыдущего current_swing_high выполняется сравнение: более высокий максимум получает метку HH и устанавливает current_trend = 1; более низкий получает метку LH, цвет def_clr_down и current_trend = -1. Если prt включён, сведения о свинге выводятся в журнал. Затем drawSwingPoint получает строку времени бара, время, цену, object_code, цвет, direction = -1 для максимума и метку. После этого обновляются current_swing_high и swing_high_time. Для свингового минимума выполняется зеркальная логика. Результат компиляции и запуска показан ниже.

ОБНАРУЖЕНИЕ ТОЧЕК СВИНГА

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

double Ask = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits); //--- Получение Ask
double Bid = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits); //--- Получение Bid
bool buy_break = (trade_mode == PerTick) ? (Bid > current_swing_high) : (Bid > current_swing_high && close(1) > current_swing_high); //--- Проверка пробоя для покупки
bool sell_break = (trade_mode == PerTick) ? (Ask < current_swing_low) : (Ask < current_swing_low && close(1) < current_swing_low); //--- Проверка пробоя для продажи
if (current_trend == -1 && current_swing_high > 0 && buy_break) { //--- Проверка бычьего CHoCH
   if (prt) {                                                   //--- Проверка вывода сообщений
      Print("CHoCH UP NOW");                                    //--- Вывод бычьего CHoCH в журнал
   }
   int swing_H_index = 0;                                       //--- Инициализация индекса
   for (int i=0; i<=length*2+1000; i++) {                       //--- Цикл поиска
      double high_sel = high(i);                                //--- Получение максимума
      if (high_sel == current_swing_high) {                     //--- Проверка совпадения
         swing_H_index = i;                                     //--- Установка индекса
         if (prt) {                                             //--- Проверка вывода сообщений
            Print("BREAK HIGH @ BAR ",swing_H_index);           //--- Вывод пробоя в журнал
         }
         break;                                                 //--- Выход из цикла
      }
   }
   drawBreakLevel(TimeToString(time(0)),swing_high_time,current_swing_high, 
   time(0+1),current_swing_high,def_clr_up,-1);                 //--- Отрисовка уровня пробоя
   current_swing_high = -1.0;                                   //--- Сброс максимума
   //--- Открытие покупки
   double trade_lots = NormalizeDouble(inpLot, 2);              //--- Нормализация лота
   double SL_Buy = NormalizeDouble(Bid-sl_pts*r2r_ratio*_Point,_Digits); //--- Расчёт SL покупки
   double TP_Buy = NormalizeDouble(Bid+tp_pts*_Point,_Digits);  //--- Расчёт TP покупки
   if (PositionsTotal() < totalTrades) {                        //--- Проверка лимита позиций
      obj_Trade.Buy(trade_lots,_Symbol,Ask,SL_Buy,TP_Buy,"CHoCH Up BUY"); //--- Открытие покупки
   }
   return;                                                      //--- Возврат
}

Здесь реализуются обнаружение пробоя и открытие позиции при бычьем CHoCH. Текущая цена Ask получается функцией SymbolInfoDouble с параметром SYMBOL_ASK и нормализуется до количества знаков символа; аналогично цена Bid получается через SYMBOL_BID. Условие buy_break зависит от trade_mode: в режиме PerTick проверяется, превышает ли Bid значение current_swing_high; в режиме PerBar дополнительно требуется, чтобы цена закрытия предыдущего бара close(1) также находилась выше уровня, подтверждая пробой по закрытию. Условие sell_break формируется зеркально. Если current_trend равен -1, существует корректный current_swing_high выше нуля и buy_break равен true, обнаруживается бычий CHoCH. При включённом prt в журнал выводится CHoCH UP NOW. Затем программа ищет точный индекс бара этого свингового максимума, перебирая до удвоенной глубины сканирования плюс 1000 баров и сравнивая high(i) с current_swing_high. При совпадении индекс сохраняется в swing_H_index, при необходимости выводится в журнал, после чего цикл завершается.

Уровень пробоя отрисовывается функцией drawBreakLevel: от времени swing_high_time на цене current_swing_high до точки на один бар правее на той же цене, с цветом def_clr_up и direction = -1. Затем current_swing_high сбрасывается в -1.0 для поиска следующего свинга. Размер лота нормализуется до двух знаков и сохраняется в trade_lots. Stop Loss для покупки рассчитывается как Bid минус sl_pts × r2r_ratio × _Point, а Take Profit — как Bid плюс tp_pts × _Point; оба значения нормализуются. Схему уровней можно изменить по своему усмотрению: использовать расчёт по соотношению риска и прибыли либо фиксированные расстояния. Входные параметры позволяют выбрать подходящую конфигурацию. Если общее количество позиций меньше totalTrades, функция obj_Trade.Buy открывает позицию на покупку с trade_lots, текущим символом, ценой Ask, рассчитанными SL и TP и комментарием CHoCH Up BUY. После этого выполняется ранний return, чтобы не продолжать обработку текущего тика. Результат работы системы показан ниже.

БЫЧИЙ CHoCH

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

else if (current_trend == 1 && current_swing_low > 0 && sell_break) { //--- Проверка медвежьего CHoCH
   if (prt) {                                                   //--- Проверка вывода сообщений
      Print("CHoCH DOWN NOW");                                  //--- Вывод медвежьего CHoCH в журнал
   }
   int swing_L_index = 0;                                       //--- Инициализация индекса
   for (int i=0; i<=length*2+1000; i++) {                       //--- Цикл поиска
      double low_sel = low(i);                                  //--- Получение минимума
      if (low_sel == current_swing_low) {                       //--- Проверка совпадения
         swing_L_index = i;                                     //--- Установка индекса
         if (prt) {                                             //--- Проверка вывода сообщений
            Print("BREAK LOW @ BAR ",swing_L_index);            //--- Вывод пробоя в журнал
         }
         break;                                                 //--- Выход из цикла
      }
   }
   drawBreakLevel(TimeToString(time(0)),swing_low_time,current_swing_low, 
   time(0+1),current_swing_low,def_clr_down,1);                 //--- Отрисовка уровня пробоя
   current_swing_low = -1.0;                                    //--- Сброс минимума
   //--- Открытие продажи
   double trade_lots = NormalizeDouble(inpLot, 2);              //--- Нормализация лота
   double SL_Sell = NormalizeDouble(Ask+sl_pts*r2r_ratio*_Point,_Digits); //--- Расчёт SL продажи
   double TP_Sell = NormalizeDouble(Ask-tp_pts*_Point,_Digits); //--- Расчёт TP продажи
   if (PositionsTotal() < totalTrades) {                        //--- Проверка лимита позиций
      obj_Trade.Sell(trade_lots,_Symbol,Bid,SL_Sell,TP_Sell,"CHoCH Down SELL"); //--- Открытие продажи
   }
   return;                                                      //--- Возврат
}

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

МЕДВЕЖИЙ CHoCH

Теперь система определяет все сетапы CHoCH. Остаётся добавить трейлинг-стоп, чтобы по возможности максимизировать прибыль, когда рынок движется в нашу пользу.

//+------------------------------------------------------------------+
//| Применение трейлинг-стопа по пунктам                             |
//+------------------------------------------------------------------+
void ApplyPointsTrailing() {
   double point = _Point;                                            //--- Получение значения пункта
   for (int i = PositionsTotal() - 1; i >= 0; i--) {                 //--- Обратный перебор позиций
      if (PositionGetTicket(i) > 0) {                                //--- Проверка корректности тикета
         if (PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == magic_number) { //--- Проверка символа и магического номера
            double sl = PositionGetDouble(POSITION_SL);              //--- Получение SL
            double tp = PositionGetDouble(POSITION_TP);              //--- Получение TP
            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); //--- Расчёт нового SL
               if (newSL > sl && SymbolInfoDouble(_Symbol, SYMBOL_BID) - openPrice > Min_Profit_To_Trail_Pips * point) { //--- Проверка условий
                  obj_Trade.PositionModify(ticket, newSL, tp);       //--- Изменение позиции
               }
            } else if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) { //--- Проверка позиции на продажу
               double newSL = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK) + Trailing_Stop_Pips * point, _Digits); //--- Расчёт нового SL
               if (newSL < sl && openPrice - SymbolInfoDouble(_Symbol, SYMBOL_ASK) > Min_Profit_To_Trail_Pips * point) { //--- Проверка условий
                  obj_Trade.PositionModify(ticket, newSL, tp);       //--- Изменение позиции
               }
            }
         }
      }
   }
}

//--- Вызов функции в обработчике тиков
// Трейлинг-стоп по пунктам может работать на каждом тике
if (TrailingType == Trailing_Points && PositionsTotal() > 0) {      //--- Проверка трейлинг-стопа
   ApplyPointsTrailing();                                           //--- Применение трейлинг-стопа
}

//--- Добавлено для ручного изменения масштаба графика
//+------------------------------------------------------------------+
//| Функция обработки событий графика                                |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) {
   if (id == CHARTEVENT_CHART_CHANGE) {                             //--- Проверка изменения графика
      UpdateFontSizes();                                            //--- Обновление размеров шрифта
   }
}

Для сопровождения реализуем функцию ApplyPointsTrailing, которая при включении трейлинг-стопа по пунктам динамически изменяет Stop Loss по мере благоприятного движения цены. Значение пункта символа сохраняется в point из _Point. Затем в обратном порядке перебираются все открытые позиции через PositionsTotal, а корректность каждого тикета проверяется функцией PositionGetTicket. Для позиций текущего символа с magic_number получаем Stop Loss через PositionGetDouble и POSITION_SL, Take Profit через POSITION_TP, цену открытия через POSITION_PRICE_OPEN и тикет через POSITION_TICKET. Для позиции на покупку новый Stop Loss рассчитывается как текущая цена Bid минус Trailing_Stop_Pips × point и нормализуется до количества знаков символа. Если новый уровень лучше текущего Stop Loss, а прибыль превышает Min_Profit_To_Trail_Pips × point, позиция модифицируется через obj_Trade.PositionModify. Для позиции на продажу применяется зеркальная логика.

В обработчике тиков, если TrailingType равен Trailing_Points и PositionsTotal показывает наличие позиций, вызывается ApplyPointsTrailing. Благодаря этому уровни сопровождения обновляются на каждом тике и защищают прибыль в реальном времени. Функция OnChartEvent обрабатывает события графика, включая изменение масштаба: если id равен CHARTEVENT_CHART_CHANGE, вызывается UpdateFontSizes, которая обновляет все текстовые объекты. Таким образом, визуализация автоматически адаптируется к действиям пользователя. Результат компиляции и запуска показан ниже.

CHoCH GIF

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


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

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

График результатов тестирования:

GRAPH

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

REPORT


Заключение

В результате мы разработали на MQL5 систему обнаружения смены характера рынка (Change of Character, CHoCH), которая сканирует бары, определяет свинговые максимумы и минимумы и присваивает им метки для установления тренда и открывает позиции при пробоях, указывающих на разворот. Поддерживаются режимы обработки по барам и по тикам, фиксированные торговые уровни, Stop Loss и Take Profit, ограничение числа одновременно открытых позиций и необязательный трейлинг-стоп на основе пунктов. Система отображает свинги цветными пиктограммами и подписями, а пробои CHoCH — линиями со стрелками и текстом, динамически изменяет размер шрифта при смене масштаба графика и может выводить диагностические сообщения в журнал.

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

Стратегия Change of Character позволяет обнаруживать пробои структуры рынка по точкам свинга, открывать по ним позиции и сопровождать их с помощью сигналов Price Action. Её можно дополнительно оптимизировать в ходе дальнейшей работы над стратегией. Удачной торговли!

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

Прикрепленные файлы |
CHoCH_EA.mq5 (46.57 KB)
ARD-регрессия на MQL5: автоматический отбор признаков и случайные признаки Фурье ARD-регрессия на MQL5: автоматический отбор признаков и случайные признаки Фурье
Статья посвящена ARD-регрессии как байесовскому методу отбора признаков и применению случайных признаков Фурье (Random Fourier Features) для учета нелинейностей в рамках линейной модели. К статье прилагаются классы ARDRegression и CRBFSampler. Проводится сравнительный анализ точности и производительности реализации на MQL5 с эталонным классом из библиотеки scikit-learn.
Нейросети в трейдинге: Долговременная память торговых сценариев (RAG-память) Нейросети в трейдинге: Долговременная память торговых сценариев (RAG-память)
Статья продолжает адаптацию фреймворка VLADriver-RAG к задачам алгоритмической торговли и описывает переход от отдельной сценарной ячейки к ограниченной двухуровневой памяти. Рассматриваются добавление и консолидация завершённого опыта, многоэтапный GPU-поиск глобального Top-K и формирование RAG-токенов с параметрами действия, релевантностью и средним результатом. Память сохраняет опыт вне весов сети и возвращает Актёру компактный исторический контекст при предсказуемом времени чтения.
Изучение Стандартной библиотеки MQL5 (Часть 4): Создание библиотеки пользовательских сигналов Изучение Стандартной библиотеки MQL5 (Часть 4): Создание библиотеки пользовательских сигналов
Сегодня мы используем стандартную библиотеку MQL5 для создания пользовательских классов сигналов и позволяем мастеру MQL5 собрать для нас профессиональный советник. Такой подход упрощает разработку, позволяя даже начинающим программистам создавать надежные советники без глубоких знаний в области программирования, сосредоточившись на настройке параметров и оптимизации стратегии. Давайте шаг за шагом разберем этот процесс.
Моделирование рынка: Position View (XII) Моделирование рынка: Position View (XII)
В этой статье вы научитесь создавать визуальный сигнал на своей торговой платформе, чтобы прямо на графике определять, является ли позиция длинной или короткой, без необходимости обращаться к терминалу. Кроме того, в тексте рассматривается реализация функциональности, которая улучшает отображение при перемещении линий тейк-профита и стоп-лосса, скрывая горизонтальную линию, следующую за курсором мыши, во время перемещения этих линий во избежание путаницы. Чтение предлагает практические знания для настройки систем моделирования рынка.