Автоматизация торговых стратегий в MQL5 (Часть 44): Обнаружение смены характера рынка (CHoCH) по пробоям свинговых экстремумов
Введение
В предыдущей статье (Часть 43) мы разработали на языке MetaQuotes Language 5 (MQL5) адаптивную стратегию на основе канала линейной регрессии. Она рассчитывала линии регрессии с полосами отклонения, активировалась только при достаточном наклоне, динамически продлевала или перестраивала канал при отклонениях цены, поддерживала обычный и обратный режимы, открывала позиции при пробое границ канала из внутренней области, закрывала их при пересечении средней линии, ограничивала число одновременно открытых позиций и отображала закрашенные зоны с подписями и стрелками. В Части 44 мы создадим систему обнаружения смены характера рынка (Change of Character, CHoCH) по пробоям свинговых максимумов и минимумов.
Система сканирует бары, определяет свинговые максимумы и минимумы и присваивает им обозначения HH/LH/LL/HL для определения тренда. Позиции открываются при пробоях, указывающих на разворот: покупки — выше максимумов при нисходящем тренде, продажи — ниже минимумов при восходящем тренде. Предусмотрены режимы обработки по барам и по тикам, фиксированные торговые уровни с настраиваемым соотношением риска и прибыли, ограничение числа одновременно открытых позиций, трейлинг-стоп, а также визуализация со значками, подписями, линиями пробоя и адаптивным размером шрифта. Рассмотрим следующие темы:
- Общие принципы стратегии Change of Character (CHoCH)
- Реализация на MQL5
- Тестирование в тестере стратегий
- Заключение
В результате мы получим рабочую стратегию на 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 — линиями со стрелками и текстом. Размер шрифта будет динамически изменяться вместе с масштабом графика. Ниже показана схема наших целей.

Реализация на 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.

Функция 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 потребуется та же логика с обратными условиями. Реализация показана ниже.
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. Остаётся добавить трейлинг-стоп, чтобы по возможности максимизировать прибыль, когда рынок движется в нашу пользу.
//+------------------------------------------------------------------+ //| Применение трейлинг-стопа по пунктам | //+------------------------------------------------------------------+ 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, которая обновляет все текстовые объекты. Таким образом, визуализация автоматически адаптируется к действиям пользователя. Результат компиляции и запуска показан ниже.

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

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

Заключение
В результате мы разработали на MQL5 систему обнаружения смены характера рынка (Change of Character, CHoCH), которая сканирует бары, определяет свинговые максимумы и минимумы и присваивает им метки для установления тренда и открывает позиции при пробоях, указывающих на разворот. Поддерживаются режимы обработки по барам и по тикам, фиксированные торговые уровни, Stop Loss и Take Profit, ограничение числа одновременно открытых позиций и необязательный трейлинг-стоп на основе пунктов. Система отображает свинги цветными пиктограммами и подписями, а пробои CHoCH — линиями со стрелками и текстом, динамически изменяет размер шрифта при смене масштаба графика и может выводить диагностические сообщения в журнал.
Отказ от ответственности: статья предназначена исключительно для образовательных целей. Торговля связана со значительными финансовыми рисками, а волатильность рынка может привести к убыткам. Перед использованием программы на реальном счёте необходимо провести тщательное тестирование в тестере стратегий и обеспечить надлежащее управление рисками.
Стратегия Change of Character позволяет обнаруживать пробои структуры рынка по точкам свинга, открывать по ним позиции и сопровождать их с помощью сигналов Price Action. Её можно дополнительно оптимизировать в ходе дальнейшей работы над стратегией. Удачной торговли!
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/20355
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
ARD-регрессия на MQL5: автоматический отбор признаков и случайные признаки Фурье
Нейросети в трейдинге: Долговременная память торговых сценариев (RAG-память)
Изучение Стандартной библиотеки MQL5 (Часть 4): Создание библиотеки пользовательских сигналов
Моделирование рынка: Position View (XII)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования