English Deutsch 日本語
preview
Автоматизация торговых стратегий на MQL5 (Часть 41): Стратегия Candle Range Theory (CRT) — накопление, манипуляция и распределение (AMD)

Автоматизация торговых стратегий на MQL5 (Часть 41): Стратегия Candle Range Theory (CRT) — накопление, манипуляция и распределение (AMD)

MetaTrader 5Трейдинг |
116 6
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

В предыдущей статье (часть 40) мы разработали на языке MetaQuotes Language 5 (MQL5) торговую систему по уровням коррекции Фибоначчи. Она рассчитывала уровни коррекции по диапазону дневной свечи или по массиву баров за заданную глубину истории, определяла бычий или медвежий сетап, открывала сделки при пересечении ценой заданных уровней и при необходимости закрывала позиции при новом расчёте уровней Фибоначчи. В части 41 мы разработаем торговую систему на основе стратегии Candle Range Theory (CRT), включающую фазы накопления, манипуляции и распределения (Accumulation, Manipulation, Distribution, AMD).

Система определяет диапазоны накопления на заданном таймфрейме, выявляет пробои с необязательной фильтрацией по глубине манипуляции и подтверждает разворот по закрытиям баров перед входом в сделку в фазе распределения. Она поддерживает динамический или статический расчёт Stop Loss и Take Profit на основе соотношения риска и прибыли, необязательный трейлинг-стоп и ограничение количества позиций в каждом направлении. Фазы отображаются на графике с помощью цветных прямоугольников, уровней и текстовых подписей. Рассмотрим следующие темы:

  1. Принцип работы стратегии Candle Range Theory (CRT)
  2. Реализация на MQL5
  3. Бэктестирование
  4. Заключение

К концу статьи у нас будет работоспособная стратегия MQL5 для торговли по стратегии Candle Range Theory с фазами AMD, готовая к дальнейшей настройке. Приступим!


Принцип работы стратегии Candle Range Theory (CRT)

Стратегия Candle Range Theory (CRT) — это стратегия Price Action, направленная на выявление ключевых фаз внутри диапазона свечи для поиска разворотных сделок с высокой вероятностью. Движение рынка разделяется на три основные фазы: накопление, когда цена консолидируется в заданном диапазоне, что часто указывает на накопление позиций институциональными участниками; манипуляцию, когда цена ненадолго выходит за границы диапазона, чтобы заманить трейдеров в ловушку и выбить слабые позиции; и распределение, когда после подтверждённого возврата в диапазон начинается основное направленное движение. Подход основан на предположении, что значительные пробои нередко оказываются ложными движениями для сбора ликвидности, после которых следует сильное движение в противоположную сторону.

В бычьем сетапе мы используем диапазон свечи, закрывшейся ростом, ожидаем пробой вниз в фазе манипуляции для снятия стоп-приказов ниже минимума и открываем позицию на покупку после подтверждённого возврата цены обратно в диапазон выше минимума, рассчитывая на восходящее распределение. В медвежьем сетапе диапазон задаётся свечой, закрывшейся снижением; затем ожидается пробой выше максимума в фазе манипуляции, а позиция на продажу открывается после возврата цены обратно в диапазон ниже максимума с расчётом на нисходящее распределение. Фильтр минимальной глубины манипуляции и подтверждение разворота по закрытиям баров помогает исключать слабые сетапы и выбирать более убедительные сигналы. Ниже показан пример сетапа CRT.

ПРИМЕР СЕТАПА CANDLE RANGE THEORY (CRT)

Мы зададим диапазоны накопления на выбранном пользователем таймфрейме. Система будет выявлять пробои и, если соответствующая настройка включена, проверять глубину манипуляции относительно процентного порога. Разворот будет подтверждаться заданным количеством закрывшихся баров на таймфрейме подтверждения. Количество позиций в каждом направлении ограничивается. Для Stop Loss и Take Profit применяется динамический или статический расчёт с учётом соотношения риска и прибыли. После достижения порога прибыли можно включить трейлинг-стоп. Все фазы отображаются на графике прямоугольниками, уровнями и подписями. Ниже представлена общая схема поставленных задач.

ПРИМЕР СТРУКТУРЫ CRT


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

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

//+------------------------------------------------------------------+
//|                                   CRT Candle Range Theory 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>
//+------------------------------------------------------------------+
//| Перечисления                                                     |
//+------------------------------------------------------------------+
enum SLTP_Method {                                                // Определяем перечисление методов расчёта SL/TP
   Dynamic_Method = 0,                                            // Динамический метод по экстремуму пробоя
   Static_Method  = 1                                             // Статический метод в фиксированных пунктах
};

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

//+------------------------------------------------------------------+
//| Входные параметры                                                |
//+------------------------------------------------------------------+
input ENUM_TIMEFRAMES RangeTF = PERIOD_H4;                        // Таймфрейм для определения диапазона
input double TradeVolume = 0.01;                                  // Размер лота
input double RR_Ratio = 1.3;                                      // Соотношение риска и прибыли
input SLTP_Method SLTP_Approach = Static_Method;                  // Метод расчёта SL/TP
input int SL_Points = 100;                                        // Расстояние SL в пунктах (для статического метода)
input TrailingTypeEnum TrailingType = Trailing_None;              // Тип трейлинг-стопа
input double Trailing_Stop_Points = 30.0;                         // Расстояние трейлинг-стопа в пунктах
input double Min_Profit_To_Trail_Points = 50.0;                   // Минимальная прибыль в пунктах для запуска трейлинга
input int UniqueID = 123456789;                                   // Уникальный идентификатор сделок
input int MaxPositionsDir = 1;                                    // Максимум позиций в каждом направлении
input ENUM_TIMEFRAMES ConfirmTF = PERIOD_CURRENT;                 // Таймфрейм подтверждения (по закрытиям баров)
input int ConfirmBars = 1;                                        // Количество закрывшихся баров для подтверждения разворота (0 — отключить)
input bool UseManipFilter = true;                                 // Использовать фильтр глубины манипуляции
input double MinManipPct = 5.0;                                   // Минимальная глубина манипуляции в процентах от диапазона (если фильтр включён)
input double DistribProjPct = 50.0;                               // Длина проекции фазы распределения в процентах от длительности диапазона

Сначала подключаем торговую библиотеку директивой #include <Trade\Trade.mqh>. Она предоставляет основные классы и функции для выполнения торговых операций. Затем определяем перечисления для пользовательских настроек. Перечисление SLTP_Method содержит Dynamic_Method для динамического расчёта Stop Loss и Take Profit по экстремуму пробоя и Static_Method для статического расчёта в фиксированных пунктах. В перечислении TrailingTypeEnum значение Trailing_None отключает трейлинг-стоп, а Trailing_Points включает сопровождение позиции по заданному количеству пунктов. Эти настройки обеспечивают гибкое управление риском.

Далее объявляем входные параметры, доступные в окне свойств советника. RangeTF задаёт таймфрейм для определения диапазона накопления, TradeVolume — объём каждой сделки, RR_Ratio — соотношение риска и прибыли, а SLTP_Approach выбирает способ расчёта Stop Loss и Take Profit с помощью ранее созданного перечисления. Назначение остальных параметров следует из их названий и комментариев. Эти настройки позволяют адаптировать систему к разным рыночным условиям и предпочтениям пользователя. После компиляции получаем следующий набор входных параметров.

НАБОР ВХОДНЫХ ПАРАМЕТРОВ

После этого объявим глобальные переменные, которые понадобятся во всей программе.

//+------------------------------------------------------------------+
//| Глобальные переменные                                            |
//+------------------------------------------------------------------+
CTrade obj_Trade;                                                 //--- Торговый объект
datetime prevRangeTime = 0;                                       //--- Время предыдущего диапазона
double rangeMax = 0.0;                                            //--- Максимум диапазона
double rangeMin = 0.0;                                            //--- Минимум диапазона
bool positiveDirection = false;                                   //--- Флаг бычьего сценария
bool rangeBreached = false;                                       //--- Флаг пробоя диапазона
double breachPoint = 0.0;                                         //--- Точка пробоя
string maxLevelObj = "RangeMaxLevel";                             //--- Имя объекта максимального уровня
string minLevelObj = "RangeMinLevel";                             //--- Имя объекта минимального уровня
string maxTextObj = "CRT_High_Text";                              //--- Текстовый объект максимума CRT
string minTextObj = "CRT_Low_Text";                               //--- Текстовый объект минимума CRT
bool tradedSetup = false;                                         //--- Флаг отработанного сетапа
datetime breachTime = 0;                                          //--- Время пробоя
datetime lastConfirmTime = 0;                                     //--- Время последнего подтверждения

Объявляем глобальные переменные для хранения состояния и управления логикой CRT. Объект obj_Trade класса CTrade выполняет торговые операции, включая открытие и изменение позиций. Переменные диапазона накопления: prevRangeTime хранит время предыдущей свечи диапазона, rangeMax и rangeMin — её максимальную и минимальную цены, а флаг positiveDirection показывает, закрылась ли свеча выше цены открытия (бычий сетап) или ниже неё (медвежий сетап). Флаг rangeBreached фиксирует факт пробоя, breachPoint хранит экстремальную цену фазы манипуляции, а tradedSetup предотвращает повторное открытие сделки по одному сетапу.

Строковые переменные maxLevelObj и minLevelObj содержат имена горизонтальных линий границ диапазона, а maxTextObj и minTextObj — имена текстовых подписей максимумов и минимумов CRT. Переменная breachTime хранит время начала фазы манипуляции, а lastConfirmTime — время последнего бара подтверждения, что позволяет корректно отслеживать события, зависящие от времени. Теперь в обработчике OnInit зададим магический номер сделок.

//+------------------------------------------------------------------+
//| Функция запуска советника                                        |
//+------------------------------------------------------------------+
int OnInit() {
   obj_Trade.SetExpertMagicNumber(UniqueID);                      //--- Задаём магический номер
   return(INIT_SUCCEEDED);                                        //--- Возвращаем код успешного выполнения
}

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

//+------------------------------------------------------------------+
//| Отображение горизонтального уровня                               |
//+------------------------------------------------------------------+
void RenderLevel(string objName, double levelVal, color levelClr, string levelDesc) {
   ObjectDelete(ChartID(), objName);                                //--- Удаляем объект
   ObjectCreate(ChartID(), objName, OBJ_HLINE, 0, 0, levelVal);     //--- Создаём горизонтальную линию
   ObjectSetInteger(ChartID(), objName, OBJPROP_COLOR, levelClr);   //--- Задаём цвет
   ObjectSetInteger(ChartID(), objName, OBJPROP_STYLE, STYLE_DOT);  //--- Задаём стиль
   ObjectSetString(ChartID(), objName, OBJPROP_TOOLTIP, levelDesc); //--- Задаём всплывающую подсказку
   ChartRedraw(ChartID());                                          //--- Перерисовываем график
}

//+------------------------------------------------------------------+
//| Отображение текстовой подписи                                    |
//+------------------------------------------------------------------+
void RenderText(string objName, datetime timeVal, double priceVal, string textStr, color textClr, int anchorVal) {
   ObjectDelete(ChartID(), objName);                                 //--- Удаляем объект
   ObjectCreate(ChartID(), objName, OBJ_TEXT, 0, timeVal, priceVal); //--- Создаём текстовый объект
   ObjectSetString(ChartID(), objName, OBJPROP_TEXT, textStr);       //--- Задаём текст
   ObjectSetInteger(ChartID(), objName, OBJPROP_COLOR, textClr);     //--- Задаём цвет
   ObjectSetInteger(ChartID(), objName, OBJPROP_ANCHOR, anchorVal);  //--- Задаём точку привязки
   ObjectSetInteger(ChartID(), objName, OBJPROP_FONTSIZE, 10);       //--- Задаём размер шрифта
   ChartRedraw(ChartID());                                           //--- Перерисовываем график
}

Функция RenderLevel рисует или обновляет горизонтальную линию ключевого ценового уровня, например максимума или минимума диапазона. Она принимает имя объекта, значение цены, цвет и описание. Сначала функция удаляет существующий объект с тем же именем с помощью ObjectDelete, чтобы избежать дубликатов. Затем ObjectCreate создаёт объект типа OBJ_HLINE на заданном ценовом уровне. Цвет задаётся через ObjectSetInteger и OBJPROP_COLOR, пунктирный стиль — через OBJPROP_STYLE и STYLE_DOT, а всплывающая подсказка — через OBJPROP_TOOLTIP. В конце ChartRedraw обновляет график.

Функция RenderText аналогичным образом создаёт или обновляет текстовые подписи, например обозначения фаз. Она принимает имя объекта, координаты времени и цены, текст, цвет и точку привязки. Сначала ObjectDelete удаляет прежний объект, затем ObjectCreate создаёт объект типа OBJ_TEXT в заданных координатах. Содержимое задаётся через ObjectSetString и OBJPROP_TEXT, цвет — через ObjectSetInteger и OBJPROP_COLOR, положение привязки — через OBJPROP_ANCHOR, а размер шрифта 10 — через OBJPROP_FONTSIZE. После ChartRedraw подпись появляется на графике. Теперь с помощью этих функций определим диапазон и отобразим его в обработчике OnTick.

//+------------------------------------------------------------------+
//| Функция обработки тиков                                          |
//+------------------------------------------------------------------+
void OnTick() {
   double currBid = SymbolInfoDouble(_Symbol, SYMBOL_BID);        //--- Получаем текущую цену Bid
   double currAsk = SymbolInfoDouble(_Symbol, SYMBOL_ASK);        //--- Получаем текущую цену Ask
   datetime currRangeTime = iTime(_Symbol, RangeTF, 0);           //--- Получаем время текущего диапазона
   if (currRangeTime != prevRangeTime) {                          //--- Проверяем формирование нового диапазона
      prevRangeTime = currRangeTime;                              //--- Обновляем время предыдущего диапазона
      double prevMax = iHigh(_Symbol, RangeTF, 1);                //--- Получаем максимум предыдущего бара
      double prevMin = iLow(_Symbol, RangeTF, 1);                 //--- Получаем минимум предыдущего бара
      double prevStart = iOpen(_Symbol, RangeTF, 1);              //--- Получаем цену открытия предыдущего бара
      double prevEnd = iClose(_Symbol, RangeTF, 1);               //--- Получаем цену закрытия предыдущего бара
      rangeMax = prevMax;                                         //--- Задаём максимум диапазона
      rangeMin = prevMin;                                         //--- Задаём минимум диапазона
      positiveDirection = (prevEnd > prevStart);                  //--- Определяем направление
      rangeBreached = false;                                      //--- Сбрасываем флаг пробоя
      breachPoint = positiveDirection ? rangeMin : rangeMax;      //--- Задаём точку пробоя
      tradedSetup = false;                                        //--- Сбрасываем флаг отработанного сетапа
      breachTime = 0;                                             //--- Сбрасываем время выхода за границу диапазона
      lastConfirmTime = 0;                                        //--- Сбрасываем время подтверждения
      RenderLevel(maxLevelObj, rangeMax, clrOrange, "Range Max"); //--- Отображаем максимальный уровень
      RenderLevel(minLevelObj, rangeMin, clrPurple, "Range Min"); //--- Отображаем минимальный уровень
      // Добавляем текстовые подписи текущих максимума и минимума CRT
      datetime labelTime = currRangeTime;                         //--- Задаём время подписи
      RenderText(maxTextObj, labelTime, rangeMax, "CRT High", clrOrange, ANCHOR_RIGHT_LOWER); //--- Отображаем подпись максимума
      RenderText(minTextObj, labelTime, rangeMin, "CRT Low", clrPurple, ANCHOR_RIGHT_UPPER);  //--- Отображаем подпись минимума
      // Рисуем заполненный фоновый прямоугольник фазы накопления (свечи диапазона)
      string rangeRectObj = "RangeRectangle_" + IntegerToString(currRangeTime);               //--- Имя прямоугольника диапазона
      datetime rangeStartTime = iTime(_Symbol, RangeTF, 1);                                   //--- Получаем время начала
      datetime rangeEndTime = currRangeTime;                                                  //--- Задаём время окончания
      ObjectCreate(ChartID(), rangeRectObj, OBJ_RECTANGLE, 0, rangeStartTime, rangeMax, rangeEndTime, rangeMin); //--- Создаём прямоугольник
      color rectClr = positiveDirection ? clrLightGreen : clrLightPink;                       //--- Задаём цвет прямоугольника
      ObjectSetInteger(ChartID(), rangeRectObj, OBJPROP_COLOR, rectClr);                      //--- Задаём цвет
      ObjectSetInteger(ChartID(), rangeRectObj, OBJPROP_FILL, true);                          //--- Включаем заливку
      ObjectSetInteger(ChartID(), rangeRectObj, OBJPROP_BACK, true);                          //--- Помещаем объект на задний план
      ObjectSetInteger(ChartID(), rangeRectObj, OBJPROP_STYLE, STYLE_SOLID);                  //--- Задаём стиль
      ChartRedraw(ChartID());                                                                 //--- Перерисовываем график
   }
}

В обработчике OnTick сначала получаем текущие цены Bid и Ask функцией SymbolInfoDouble с константами SYMBOL_BID и SYMBOL_ASK и сохраняем их в currBid и currAsk. Затем функцией iTime со сдвигом 0 получаем время последнего бара на таймфрейме RangeTF и записываем его в currRangeTime. Если оно отличается от prevRangeTime, сформировался новый бар диапазона. Тогда обновляем prevRangeTime и получаем максимум, минимум, цену открытия и цену закрытия предыдущего бара со сдвигом 1 с помощью iHigh, iLow, iOpen и iClose. В rangeMax и rangeMin сохраняются границы диапазона, а positiveDirection принимает true, если цена закрытия выше цены открытия. Затем сбрасываются rangeBreached и tradedSetup, breachPoint задаётся на уровне минимума для бычьего сценария или максимума для медвежьего сценария, а breachTime и lastConfirmTime обнуляются.

Для визуализации RenderLevel рисует максимальную границу оранжевым цветом с описанием Range Max, а минимальную — фиолетовым с описанием Range Min. Время подписи labelTime приравнивается currRangeTime, после чего RenderText размещает подписи «CRT High» и «CRT Low» возле соответствующих границ. Фаза накопления выделяется прямоугольником: уникальное имя создаётся из префикса RangeRectangle_ и времени текущего бара. Началом прямоугольника служит время предыдущего бара, концом — currRangeTime, а ценовыми границами — rangeMax и rangeMin. Для бычьего сценария используется светло-зелёный цвет, для медвежьего сценария — светло-розовый. Прямоугольник заполняется, помещается на задний план и отображается сплошной линией. После ChartRedraw изменения появляются на графике. Рекомендуется компилировать программу после каждого этапа разработки. Результат этого этапа показан ниже.

ПОСТРОЕНИЕ ДИАПАЗОНА CRT

На изображении видно, что диапазоны заданы корректно. Теперь можно перейти к определению пробоев и торговле по сформированным сетапам.

if (rangeMax == 0.0 || rangeMin == 0.0) return;                //--- Прекращаем выполнение, если диапазон не задан
bool justBreached = false;                                     //--- Инициализируем флаг нового пробоя
if (positiveDirection && currBid <= rangeMin) {                //--- Проверяем пробой в бычьем сценарии
   if (!rangeBreached) {                                       //--- Проверяем, что пробой ещё не зафиксирован
      rangeBreached = true;                                    //--- Фиксируем пробой
      justBreached = true;                                     //--- Устанавливаем флаг нового пробоя
      breachTime = TimeCurrent();                              //--- Записываем время выхода за границу диапазона
   }
   breachPoint = MathMin(breachPoint, currBid);                //--- Обновляем экстремальную точку пробоя
} else if (!positiveDirection && currBid >= rangeMax) {        //--- Проверяем пробой в медвежьем сценарии
   if (!rangeBreached) {                                       //--- Проверяем, что пробой ещё не зафиксирован
      rangeBreached = true;                                    //--- Фиксируем пробой
      justBreached = true;                                     //--- Устанавливаем флаг нового пробоя
      breachTime = TimeCurrent();                              //--- Записываем время выхода за границу диапазона
   }
   breachPoint = MathMax(breachPoint, currBid);                //--- Обновляем экстремальную точку пробоя
}
if (rangeBreached && !tradedSetup) {                           //--- Проверяем наличие пробоя и отсутствие сделки по сетапу
   // Проверяем подтверждение разворота по закрытиям баров
   bool reversalConfirmed = false;                              //--- Инициализируем флаг подтверждения
   if (ConfirmBars == 0) {                                      //--- Проверяем, отключено ли подтверждение
      reversalConfirmed = true;                                 //--- Подтверждаем разворот
   } else {                                                     //--- Иначе
      datetime currConfirmTime = iTime(_Symbol, ConfirmTF, 0);  //--- Получаем время бара подтверждения
      if (currConfirmTime != lastConfirmTime) {                 //--- Проверяем формирование нового бара подтверждения
         lastConfirmTime = currConfirmTime;                     //--- Обновляем время последнего подтверждения
         int confirmedCount = 0;                                //--- Инициализируем счётчик
         for (int i = 1; i <= ConfirmBars; i++) {               //--- Перебираем бары
            double confirmClose = iClose(_Symbol, ConfirmTF, i); //--- Получаем цену закрытия
            if (positiveDirection && confirmClose > rangeMin) { //--- Проверяем бычий сетап
               confirmedCount++;                                //--- Увеличиваем счётчик
            } else if (!positiveDirection && confirmClose < rangeMax) { //--- Проверяем медвежий сетап
               confirmedCount++;                                //--- Увеличиваем счётчик
            }
         }
         if (confirmedCount >= ConfirmBars) {                   //--- Проверяем количество подтверждений
            reversalConfirmed = true;                           //--- Подтверждаем разворот
         }
      }
   }
   // Рассчитываем глубину манипуляции для фильтра
   bool manipSufficient = true;                                 //--- Инициализируем признак достаточной глубины
   double rangeSize = rangeMax - rangeMin;                      //--- Рассчитываем размер диапазона
   double manipDepth = positiveDirection ? (rangeMin - breachPoint) : (breachPoint - rangeMax); //--- Рассчитываем глубину манипуляции
   double manipPct = (manipDepth / rangeSize) * 100.0;          //--- Рассчитываем процент глубины
   if (UseManipFilter) {                                        //--- Проверяем, включён ли фильтр
      if (manipPct < MinManipPct) {                             //--- Проверяем недостаточную глубину манипуляции
         manipSufficient = false;                               //--- Отмечаем недостаточную глубину манипуляции
      }
   }
   bool justEntered = false;                                    //--- Инициализируем флаг входа
   datetime entryTime = 0;                                      //--- Инициализируем время входа
   double entryPrice = 0.0;                                     //--- Инициализируем цену входа
   double gainTarget = 0.0;                                     //--- Инициализируем целевой уровень
   if (reversalConfirmed && manipSufficient) {                  //--- Проверяем подтверждение разворота и достаточную глубину манипуляции
      if (positiveDirection && currBid > rangeMin && ActivePositions(POSITION_TYPE_BUY) < MaxPositionsDir) { //--- Проверяем условия входа на покупку
         double lossStop;                                       //--- Инициализируем Stop Loss
         if (SLTP_Approach == Dynamic_Method) {                 //--- Проверяем выбор динамического метода
            lossStop = NormalizeDouble(breachPoint, _Digits);   //--- Задаём Stop Loss
            double riskDistance = currAsk - breachPoint;        //--- Рассчитываем риск
            gainTarget = NormalizeDouble(currAsk + riskDistance * RR_Ratio, _Digits); //--- Задаём Take Profit
         } else {                                               //--- Статический метод
            lossStop = NormalizeDouble(currAsk - SL_Points * _Point, _Digits); //--- Задаём Stop Loss
            gainTarget = NormalizeDouble(currAsk + SL_Points * RR_Ratio * _Point, _Digits); //--- Задаём Take Profit
         }
         if (obj_Trade.Buy(TradeVolume, _Symbol, currAsk, lossStop, gainTarget, "CRT Positive Entry")) { //--- Открываем позицию на покупку
            if (obj_Trade.ResultRetcode() == TRADE_RETCODE_DONE) { //--- Проверяем успешность торговой операции
               Print("Positive Signal: Range raided below min, reversed back in (confirmed). Entry at ", DoubleToString(currAsk, _Digits), 
                     " SL at ", DoubleToString(lossStop, _Digits), " TP at ", DoubleToString(gainTarget, _Digits)); //--- Выводим сведения о входе в журнал
               Print("Debug: Accumulation Range: ", DoubleToString(rangeSize / _Point, 0), " points. Manipulation Depth: ", DoubleToString(manipDepth / _Point, 0), " points (", DoubleToString(manipPct, 2), "% of range)"); //--- Выводим отладочные данные в журнал
               string markerName = "EntryMarker_" + IntegerToString(TimeCurrent()); //--- Имя маркера входа
               ObjectCreate(ChartID(), markerName, OBJ_ARROW, 0, TimeCurrent(), currBid); //--- Создаём маркер входа
               ObjectSetInteger(ChartID(), markerName, OBJPROP_ARROWCODE, 233); //--- Задаём код стрелки
               ObjectSetInteger(ChartID(), markerName, OBJPROP_COLOR, clrBlue); //--- Задаём цвет
               ObjectSetInteger(ChartID(), markerName, OBJPROP_ANCHOR, ANCHOR_BOTTOM); //--- Задаём точку привязки
               tradedSetup = true;                                 //--- Отмечаем сетап как отработанный
               justEntered = true;                                 //--- Фиксируем открытие позиции
               entryTime = TimeCurrent();                          //--- Записываем время входа
               entryPrice = currAsk;                               //--- Записываем цену входа
            }
         }
      } else if (!positiveDirection && currBid < rangeMax && ActivePositions(POSITION_TYPE_SELL) < MaxPositionsDir) { //--- Проверяем условия входа на продажу
         double lossStop;                                       //--- Инициализируем Stop Loss
         if (SLTP_Approach == Dynamic_Method) {                 //--- Проверяем выбор динамического метода
            lossStop = NormalizeDouble(breachPoint, _Digits);   //--- Задаём Stop Loss
            double riskDistance = breachPoint - currBid;        //--- Рассчитываем риск
            gainTarget = NormalizeDouble(currBid - riskDistance * RR_Ratio, _Digits); //--- Задаём Take Profit
         } else {                                               //--- Статический метод
            lossStop = NormalizeDouble(currBid + SL_Points * _Point, _Digits); //--- Задаём Stop Loss
            gainTarget = NormalizeDouble(currBid - SL_Points * RR_Ratio * _Point, _Digits); //--- Задаём Take Profit
         }
         if (obj_Trade.Sell(TradeVolume, _Symbol, currBid, lossStop, gainTarget, "CRT Negative Entry")) { //--- Открываем позицию на продажу
            if (obj_Trade.ResultRetcode() == TRADE_RETCODE_DONE) { //--- Проверяем успешность торговой операции
               Print("Negative Signal: Range raided above max, reversed back in (confirmed). Entry at ", DoubleToString(currBid, _Digits), 
                     " SL at ", DoubleToString(lossStop, _Digits), " TP at ", DoubleToString(gainTarget, _Digits)); //--- Выводим сведения о входе в журнал
               Print("Debug: Accumulation Range: ", DoubleToString(rangeSize / _Point, 0), " points. Manipulation Depth: ", DoubleToString(manipDepth / _Point, 0), " points (", DoubleToString(manipPct, 2), "% of range)"); //--- Выводим отладочные данные в журнал
               string markerName = "EntryMarker_" + IntegerToString(TimeCurrent()); //--- Имя маркера входа
               ObjectCreate(ChartID(), markerName, OBJ_ARROW, 0, TimeCurrent(), currAsk); //--- Создаём маркер входа
               ObjectSetInteger(ChartID(), markerName, OBJPROP_ARROWCODE, 234); //--- Задаём код стрелки
               ObjectSetInteger(ChartID(), markerName, OBJPROP_COLOR, clrRed); //--- Задаём цвет
               ObjectSetInteger(ChartID(), markerName, OBJPROP_ANCHOR, ANCHOR_TOP); //--- Задаём точку привязки
               tradedSetup = true;                                 //--- Отмечаем сетап как отработанный
               justEntered = true;                                 //--- Фиксируем открытие позиции
               entryTime = TimeCurrent();                          //--- Записываем время входа
               entryPrice = currBid;                               //--- Записываем цену входа
            }
         }
      }
   }
}

Продолжая обработчик OnTick, сначала проверяем корректность диапазона: если rangeMax или rangeMin равны нулю, выполнение прекращается, чтобы не обрабатывать недействительное состояние. Флаг justBreached инициализируется значением false. В бычьем сетапе, если текущая цена Bid находится на уровне или ниже rangeMin и пробой ещё не зафиксирован, rangeBreached и justBreached устанавливаются в true, а время выхода за границу диапазона записывается функцией TimeCurrent. Затем MathMin обновляет breachPoint, сохраняя меньшее из текущего значения и цены Bid. Для медвежьего сценария применяется зеркальная логика.

Если пробой произошёл, а сделка по этому сетапу ещё не открыта, система проверяет подтверждение разворота. Флаг reversalConfirmed изначально равен false. При ConfirmBars = 0 подтверждение считается полученным сразу. В противном случае функцией iTime со сдвигом 0 определяется время последнего бара на таймфрейме ConfirmTF. Если этот бар новый относительно lastConfirmTime, значение времени обновляется и подсчитываются подтверждающие закрытия: в цикле от сдвига 1 до ConfirmBars функция iClose получает цену закрытия каждого бара. Для бычьего сценария счётчик увеличивается, если закрытие находится выше минимума диапазона, а для медвежьего сценария — если ниже максимума. Когда количество таких баров достигает ConfirmBars, разворот считается подтверждённым. Далее проверяется достаточность манипуляции. Размер диапазона рассчитывается как разность максимума и минимума, глубина манипуляции — как расстояние от границы диапазона до breachPoint, а процент глубины — как отношение этой величины к размеру диапазона, умноженное на 100. Если фильтр UseManipFilter включён и полученный процент меньше MinManipPct, условие манипуляции считается невыполненным.

Затем подготавливаются и инициализируются переменные входа. Если разворот подтверждён и глубина манипуляции достаточна, проверяются условия покупки для бычьего сценария. Цена Bid должна быть выше минимума диапазона, а количество позиций на покупку, возвращаемое функцией ActivePositions для POSITION_TYPE_BUY, — меньше MaxPositionsDir. После этого рассчитываются уровни сделки. ActivePositions — вспомогательная функция, которая возвращает количество активных позиций указанного типа. Её реализация приведена ниже.

//+------------------------------------------------------------------+
//| Подсчёт активных позиций по типу                                 |
//+------------------------------------------------------------------+
int ActivePositions(ENUM_POSITION_TYPE posType) {
   int total = 0;                                                 //--- Инициализируем общее количество
   for (int pos = PositionsTotal() - 1; pos >= 0; pos--) {        //--- Перебираем позиции
      if (PositionGetSymbol(pos) == _Symbol && PositionGetInteger(POSITION_MAGIC) == UniqueID && PositionGetInteger(POSITION_TYPE) == posType) { //--- Проверяем параметры позиции
         total++;                                                 //--- Увеличиваем количество
      }
   }
   return total;                                                  //--- Возвращаем общее количество
}

Функция проста, а её действия подробно описаны в комментариях. Возвращаясь к основной логике, советник пытается открыть покупку методом obj_Trade.Buy, передавая объём, символ, цену Ask, Stop Loss, Take Profit и комментарий. Если ResultRetcode равен TRADE_RETCODE_DONE, в журнал выводятся сведения о сигнале и отладочные данные по диапазону и глубине манипуляции. Затем ObjectCreate создаёт стрелку входа типа OBJ_ARROW на текущем времени и цене Bid; ей задаются код 233, синий цвет и нижняя точка привязки. Флаги tradedSetup и justEntered устанавливаются в true, после чего сохраняются время и цена входа. MQL5 поддерживает различные коды стрелок шрифта Wingdings, из которых можно выбрать подходящий.

СИМВОЛЫ WINGDINGS В MQL5

Для медвежьего сценария используется зеркальная логика. Цена Bid должна находиться ниже максимума диапазона, а количество позиций на продажу — быть меньше установленного лимита. При динамическом методе Stop Loss задаётся по нормализованному значению breachPoint, риск рассчитывается как расстояние от точки пробоя до цены Bid, а Take Profit — вычитанием произведения риска и RR_Ratio. При статическом методе Stop Loss располагается выше Bid на SL_Points пунктов, а Take Profit — ниже на расстоянии, определяемом тем же соотношением. Продажа открывается методом obj_Trade.Sell по цене Bid. При успехе советник выводит сведения в журнал, рисует красную стрелку с кодом 234 и верхней точкой привязки, а также обновляет флаги и данные входа. После компиляции видим результат, показанный ниже.

ПОДТВЕРЖДЕНИЯ ВХОДА

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

// Если позиция только что открыта, рисуем прямоугольники манипуляции и распределения, а также подписи фаз
if (justEntered) {                                             //--- Проверяем факт открытия позиции
   string setupSuffix = IntegerToString(prevRangeTime);        //--- Формируем суффикс сетапа
   // Обозначаем диапазон как фазу накопления (только для завершённых конфигураций)
   string accumTextUnique = "AccumText_" + setupSuffix;        //--- Имя подписи накопления
   double accumPrice = (rangeMax + rangeMin) / 2;              //--- Цена размещения подписи накопления
   datetime labelTime = prevRangeTime;                         //--- Время размещения подписи
   RenderText(accumTextUnique, labelTime, accumPrice, "Accumulation", clrBlue, ANCHOR_RIGHT); //--- Отображаем подпись накопления
   // Определяем экстремум манипуляции по максимумам и минимумам свечей между currRangeTime и entryTime
   int startBar = iBarShift(_Symbol, PERIOD_CURRENT, prevRangeTime); //--- Начальный бар
   int endBar = iBarShift(_Symbol, PERIOD_CURRENT, entryTime); //--- Конечный бар
   if (startBar < 0 || endBar < 0) return;                     //--- Прекращаем выполнение при недействительных индексах
   if (startBar < endBar) { int temp = startBar; startBar = endBar; endBar = temp; } //--- При необходимости меняем индексы местами
   int barCount = startBar - endBar + 1;                       //--- Рассчитываем количество баров
   double manipExtreme;                                        //--- Инициализируем экстремум манипуляции
   double manipStartPrice = positiveDirection ? rangeMin : rangeMax; //--- Начальная цена манипуляции
   if (positiveDirection) {                                    //--- Проверяем бычий сетап
      int lowestBar = iLowest(_Symbol, PERIOD_CURRENT, MODE_LOW, barCount, endBar); //--- Находим бар с минимальным значением
      manipExtreme = iLow(_Symbol, PERIOD_CURRENT, lowestBar); //--- Задаём экстремальную цену
   } else {                                                    //--- Медвежий сценарий
      int highestBar = iHighest(_Symbol, PERIOD_CURRENT, MODE_HIGH, barCount, endBar); //--- Находим бар с максимальным значением
      manipExtreme = iHigh(_Symbol, PERIOD_CURRENT, highestBar); //--- Задаём экстремальную цену
   }
   // Рисуем незаполненный прямоугольник манипуляции от окончания CRT до времени сигнала
   string manipRectObj = "ManipRectangle_" + setupSuffix;         //--- Имя прямоугольника манипуляции
   double topPrice = MathMax(manipStartPrice, manipExtreme);      //--- Верхняя ценовая граница
   double bottomPrice = MathMin(manipStartPrice, manipExtreme);   //--- Нижняя ценовая граница
   ObjectCreate(ChartID(), manipRectObj, OBJ_RECTANGLE, 0, prevRangeTime, topPrice, entryTime, bottomPrice); //--- Создаём прямоугольник
   ObjectSetInteger(ChartID(), manipRectObj, OBJPROP_COLOR, clrBlue); //--- Задаём цвет
   ObjectSetInteger(ChartID(), manipRectObj, OBJPROP_FILL, false); //--- Отключаем заливку
   ObjectSetInteger(ChartID(), manipRectObj, OBJPROP_BACK, true);  //--- Помещаем объект на задний план
   ObjectSetInteger(ChartID(), manipRectObj, OBJPROP_STYLE, STYLE_DOT); //--- Задаём стиль
   ObjectSetInteger(ChartID(), manipRectObj, OBJPROP_WIDTH, 2);    //--- Задаём толщину линии
   ChartRedraw(ChartID());                                         //--- Перерисовываем график
   // Добавляем подпись манипуляции в момент пробоя
   string manipTextUnique = "ManipText_" + setupSuffix;        //--- Имя подписи манипуляции
   int anchorManip = positiveDirection ? ANCHOR_RIGHT_UPPER : ANCHOR_RIGHT_LOWER; //--- Точка привязки подписи манипуляции
   RenderText(manipTextUnique, breachTime, manipExtreme, "Manipulation", clrBlue, anchorManip); //--- Отображаем подпись манипуляции
   // Добавляем подпись и прямоугольник распределения
   string distribTextUnique = "DistribText_" + setupSuffix;    //--- Имя подписи распределения
   color distribClr = positiveDirection ? clrGreen : clrRed;   //--- Цвет фазы распределения
   int anchor = positiveDirection ? ANCHOR_LEFT_LOWER : ANCHOR_LEFT_UPPER; //--- Точка привязки подписи распределения
   RenderText(distribTextUnique, entryTime, entryPrice, "Distribution", distribClr, anchor); //--- Отображаем подпись распределения
   // Рисуем незаполненный прямоугольник фазы распределения с длительностью в процентах от диапазона
   string distribRectObj = "DistribRectangle_" + setupSuffix;  //--- Имя прямоугольника распределения
   datetime rangeStartTime = iTime(_Symbol, RangeTF, 1);       //--- Время начала диапазона
   datetime rangeEndTime = prevRangeTime;                      //--- Время окончания диапазона
   long duration = rangeEndTime - rangeStartTime;              //--- Рассчитываем длительность диапазона
   double projFactor = MathMax(DistribProjPct / 100.0, 0.01);  //--- Рассчитываем коэффициент проекции
   datetime projEndTime = entryTime + (datetime)(duration * projFactor); //--- Рассчитываем время окончания проекции
   double topDistrib = MathMax(entryPrice, gainTarget);        //--- Верхняя граница распределения
   double bottomDistrib = MathMin(entryPrice, gainTarget);     //--- Нижняя граница распределения
   ObjectCreate(ChartID(), distribRectObj, OBJ_RECTANGLE, 0, entryTime, topDistrib, projEndTime, bottomDistrib); //--- Создаём прямоугольник
   ObjectSetInteger(ChartID(), distribRectObj, OBJPROP_COLOR, distribClr); //--- Задаём цвет
   ObjectSetInteger(ChartID(), distribRectObj, OBJPROP_FILL, false); //--- Отключаем заливку
   ObjectSetInteger(ChartID(), distribRectObj, OBJPROP_BACK, true); //--- Помещаем объект на задний план
   ObjectSetInteger(ChartID(), distribRectObj, OBJPROP_STYLE, STYLE_SOLID); //--- Задаём стиль
   ObjectSetInteger(ChartID(), distribRectObj, OBJPROP_WIDTH, 2); //--- Задаём толщину линии
   ChartRedraw(ChartID());                                     //--- Перерисовываем график
}

Если сделка только что открыта, то есть justEntered равен true, отображаются остальные фазы. Из prevRangeTime с помощью IntegerToString формируется уникальный суффикс имён объектов. Для подписи накопления к префиксу AccumText_ добавляется этот суффикс, цена подписи рассчитывается как среднее между максимумом и минимумом диапазона, а временем служит prevRangeTime. Функция RenderText размещает синюю подпись Accumulation с правой привязкой. Для определения экстремума манипуляции время начала prevRangeTime и время входа entryTime преобразуются функцией iBarShift в индексы баров. При недействительных индексах выполнение прекращается. При необходимости индексы меняются местами, затем рассчитывается количество баров. Начальной ценой манипуляции служит минимум диапазона для бычьего сценария или максимум для медвежьего сценария. В бычьем сетапе функция iLowest с MODE_LOW находит бар с наименьшим минимумом, после чего iLow возвращает его цену. В медвежьем сетапе экстремум определяется функциями iHighest, MODE_HIGH и iHigh.

Затем создаётся прямоугольник манипуляции. К префиксу ManipRectangle_ добавляется уникальный суффикс, а верхняя и нижняя ценовые границы определяются функциями MathMax и MathMin по начальной цене и найденному экстремуму. ObjectCreate создаёт объект OBJ_RECTANGLE от времени предыдущего диапазона до времени входа. Прямоугольнику задаются синий цвет, отсутствие заливки, размещение на заднем плане, пунктирный стиль и толщина линии 2. Затем создаётся подпись Manipulation: её имя формируется из ManipText_ и суффикса, для бычьего сценария выбирается верхняя правая привязка, для медвежьего сценария — нижняя правая, а подпись размещается на времени пробоя и цене экстремума. Для фазы распределения создаётся имя DistribText_, выбирается зелёный цвет для бычьего сценария или красный для медвежьего сценария и соответствующая левая точка привязки. Подпись Distribution размещается на времени и цене входа. В завершение аналогичным способом рисуется прямоугольник распределения и обновляется график. Получаем следующий результат.

ФАЗЫ НАКОПЛЕНИЯ, МАНИПУЛЯЦИИ И РАСПРЕДЕЛЕНИЯ

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

//+------------------------------------------------------------------+
//| Применение трейлинг-стопа по пунктам                             |
//+------------------------------------------------------------------+
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) == UniqueID) { //--- Проверяем символ и магический номер
            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_Points * point, _Digits); //--- Рассчитываем новый Stop Loss
               if (newSL > sl && SymbolInfoDouble(_Symbol, SYMBOL_BID) - openPrice > Min_Profit_To_Trail_Points * point) { //--- Проверяем условия переноса Stop Loss
                  obj_Trade.PositionModify(ticket, newSL, tp);       //--- Изменяем параметры позиции
               }
            } else if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) { //--- Проверяем позицию на продажу
               double newSL = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK) + Trailing_Stop_Points * point, _Digits); //--- Рассчитываем новый Stop Loss
               if (newSL < sl && openPrice - SymbolInfoDouble(_Symbol, SYMBOL_ASK) > Min_Profit_To_Trail_Points * point) { //--- Проверяем условия переноса Stop Loss
                  obj_Trade.PositionModify(ticket, newSL, tp);       //--- Изменяем параметры позиции
               }
            }
         }
      }
   }
}

//--- Вызываем функцию на каждом тике в обработчике OnTick

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

Функция ApplyPointsTrailing управляет переносом Stop Loss по пунктам, если соответствующий режим включён. В переменную point записывается размер пункта символа из _Point. Затем в обратном порядке перебираются все открытые позиции, полученные через PositionsTotal, что позволяет избежать проблем с индексами при изменении позиций. Корректность каждого тикета проверяется функцией PositionGetTicket. Для позиций текущего символа и с нужным магическим номером получаем Stop Loss, Take Profit, цену открытия и тикет через PositionGetString, PositionGetInteger и PositionGetDouble. Для позиции на покупку рассчитывается новый Stop Loss: из текущей цены Bid вычитается Trailing_Stop_Points, умноженное на point, после чего результат нормализуется по числу знаков символа. Если новый Stop Loss выше текущего, а прибыль в пунктах превышает порог Min_Profit_To_Trail_Points, позиция изменяется методом obj_Trade.PositionModify с сохранением прежнего Take Profit.

Для позиций на продажу выполняется аналогичный расчёт. Если новый Stop Loss находится ниже текущего, а прибыль, определяемая как разность цены открытия и Ask, достигла минимального порога, параметры позиции обновляются. В OnTick функция ApplyPointsTrailing вызывается на каждом тике, если TrailingType равен Trailing_Points и имеются открытые позиции. Теперь необходимо удалить созданные графические объекты при деинициализации советника.

//+------------------------------------------------------------------+
//| Функция завершения работы советника                              |
//+------------------------------------------------------------------+
void OnDeinit(const int code) {
   ObjectDelete(ChartID(), maxLevelObj);                          //--- Удаляем максимальный уровень
   ObjectDelete(ChartID(), minLevelObj);                          //--- Удаляем минимальный уровень
   ObjectDelete(ChartID(), maxTextObj);                           //--- Удаляем подпись максимума
   ObjectDelete(ChartID(), minTextObj);                           //--- Удаляем подпись минимума
   // Удаляем динамические прямоугольники и подписи
   ObjectsDeleteAll(ChartID(), "RangeRectangle_", OBJ_RECTANGLE); //--- Удаляем прямоугольники диапазонов
   ObjectsDeleteAll(ChartID(), "ManipRectangle_", OBJ_RECTANGLE); //--- Удаляем прямоугольники манипуляции
   ObjectsDeleteAll(ChartID(), "DistribRectangle_", OBJ_RECTANGLE); //--- Удаляем прямоугольники распределения
   ObjectsDeleteAll(ChartID(), "AccumText_", OBJ_TEXT);           //--- Удаляем подписи накопления
   ObjectsDeleteAll(ChartID(), "ManipText_", OBJ_TEXT);           //--- Удаляем подписи манипуляции
   ObjectsDeleteAll(ChartID(), "DistribText_", OBJ_TEXT);         //--- Удаляем подписи распределения
}

Обработчик OnDeinit выполняется при удалении советника с графика или завершении его работы. Сначала функцией ObjectDelete с текущим идентификатором графика ChartID по отдельности удаляются статические объекты: горизонтальная линия максимума maxLevelObj, линия минимума minLevelObj, текстовая подпись максимума CRT maxTextObj и подпись минимума minTextObj.

Для удаления динамических объектов используется ObjectsDeleteAll. С графика удаляются прямоугольники с префиксами RangeRectangle_, ManipRectangle_ и DistribRectangle_ типа OBJ_RECTANGLE, соответствующие фазам накопления, манипуляции и распределения. Затем удаляются текстовые объекты типа OBJ_TEXT с префиксами AccumText_, ManipText_ и DistribText_. Это обеспечивает полную очистку графика без оставшихся элементов. Итог работы с включённым трейлинг-стопом показан ниже.

ИТОГОВЫЙ РЕЗУЛЬТАТ С ВКЛЮЧЁННЫМ ТРЕЙЛИНГ-СТОПОМ

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

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

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

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

ГРАФИК БЭКТЕСТИРОВАНИЯ

Отчёт о бэктестировании:

ОТЧЁТ О БЭКТЕСТИРОВАНИИ


Заключение

В заключение мы разработали на MQL5 торговую систему на основе стратегии Candle Range Theory (CRT). Она определяет диапазоны накопления на заданном таймфрейме, выявляет пробои с фильтрацией по глубине манипуляции, подтверждает развороты по закрытиям баров и открывает сделки в фазе распределения. Stop Loss и Take Profit рассчитываются динамически или статически с учётом соотношения риска и прибыли.

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

Стратегия Candle Range Theory с фазами накопления, манипуляции и распределения (AMD) позволяет использовать разворотные торговые возможности и может стать основой для дальнейшей оптимизации под ваш стиль торговли. Успешной торговли!

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

Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (6)
Stanislav Korotky
Stanislav Korotky | 21 нояб. 2025 в 14:11
Вы не объяснили, как определяется начало каждой новой фазы накопления.
Allan Munene Mutiiria
Allan Munene Mutiiria | 22 нояб. 2025 в 06:18
Christian Gomez #:
Спасибо, выглядит интересно.
Спасибо
Allan Munene Mutiiria
Allan Munene Mutiiria | 22 нояб. 2025 в 06:24
Stanislav Korotky #:
Вы не объяснили, как определяется начало каждой новой фазы накопления.

Речь идет о диапазонах свечей, представленных на графике.

      double prevMax = iHigh(_Symbol, RangeTF, 1);                //--- Получить предыдущее максимальное значение
      double prevMin = iLow(_Symbol, RangeTF, 1);                 //--- Получить предыдущее минимальное значение
      double prevStart = iOpen(_Symbol, RangeTF, 1);              //--- Получить предыдущую открытую позицию
      double prevEnd = iClose(_Symbol, RangeTF, 1);               //--- Получить предыдущую цену закрытия
      rangeMax = prevMax;                                         //--- Установить максимальное значение диапазона
Stanislav Korotky
Stanislav Korotky | 22 нояб. 2025 в 18:21
Allan Munene Mutiiria #:

Вот так выглядят диапазоны свечей.

Это не отвечает на вопрос — как определить начало фазы накопления (каждой из них, ведь эта фаза повторяется снова и снова на разных участках графика). Речь идет о времени, а не о ценовом диапазоне. Речь также не идет о визуализации.

Serhii Pashchenko
Serhii Pashchenko | 5 авг. 2026 в 09:17
На мой взгляд, вопрос о начале новой фазы накопления действительно остаётся без ответа.

Получение High и Low предыдущей закрытой свечи определяет только ценовые границы очередного диапазона:

iHigh(_Symbol, RangeTF, 1);
iLow(_Symbol, RangeTF, 1);

Но это ещё не определяет, когда должен начинаться новый цикл CRT во времени.

Если на каждом новом баре предыдущая свеча автоматически становится новым диапазоном, тогда range постоянно сдвигается. Возникает несколько вопросов:

— фиксируется ли диапазон после начала манипуляции;
— когда предыдущий setup считается завершённым или отменённым;
— можно ли создавать новый range, пока старый ещё находится в состоянии Manipulation или Distribution;
— что является reset-условием: вход в позицию, достижение TP/SL, возврат в диапазон, истечение заданного количества баров или появление новой опорной свечи;
— допускается ли несколько одновременно активных CRT-диапазонов.

Для однозначной автоматизации здесь нужна state machine, например:

WAITING_FOR_RANGE
→ RANGE_ACTIVE
→ MANIPULATION_DETECTED
→ REVERSAL_CONFIRMED
→ DISTRIBUTION / TRADE_ACTIVE
→ COMPLETED или INVALIDATED
→ WAITING_FOR_NEW_RANGE

Без явного правила перехода из COMPLETED/INVALIDATED к новому RANGE_ACTIVE стратегия может выглядеть понятно визуально, но её программное поведение и результаты тестирования будут зависеть от неописанных предположений.

Поэтому ключевой вопрос именно такой: какое конкретное событие завершает текущий CRT-цикл и разрешает создать следующий диапазон?
Изучение Стандартной библиотеки MQL5 (Часть 3): Советник на основе канала стандартного отклонения Изучение Стандартной библиотеки MQL5 (Часть 3): Советник на основе канала стандартного отклонения
В этом обсуждении мы будем разрабатывать советник, используя классы CTrade и CChartObjectStdDevChannel, и применим несколько фильтров для повышения потенциальной прибыльности. На этом этапе мы применим на практике то, что обсуждали ранее. Кроме того, я представлю еще один простой подход, который поможет вам лучше понять стандартную библиотеку MQL5 и лежащую в ее основе кодовую базу. Присоединяйтесь, чтобы посмотреть, как эти идеи работают на практике.
Возможности Мастера MQL5, которые вам нужно знать (Часть 84): Использование паттернов стохастического осциллятора и FrAMA – Окончание Возможности Мастера MQL5, которые вам нужно знать (Часть 84): Использование паттернов стохастического осциллятора и FrAMA – Окончание
Стохастический осциллятор и фрактальная адаптивная скользящая средняя образуют пару взаимодополняющих индикаторов, которую можно использовать в советнике MQL5. Эту пару индикаторов мы представили в предыдущей статье, а теперь завершим ее рассмотрение, разобрав оставшиеся пять сигнальных паттернов. Как и прежде, для создания советника и проверки потенциала этой пары индикаторов используем Мастер MQL5.
От начального до среднего уровня: Очереди, списки и деревья (IV) От начального до среднего уровня: Очереди, списки и деревья (IV)
В данной статье мы завершим раздел, посвященный реализации и объяснению связного списка. Однако представленная здесь реализация не включает в себя одну деталь, которую можно реализовать в связном списке. Обсудим это позже, в другой статье.
Моделирование рынка: Position View (XI) Моделирование рынка: Position View (XI)
В этой статье я покажу вам, уважаемый читатель, как можновыбирать объекты, которые мы создаём на графике модифицировать индикатор позиции, чтобы он мог выполнять гораздо больше функций, чем это было предусмотрено изначально. Мы рассмотрим, как реализовать возможность перемещения цен и создания ценовых линий непосредственно на графике. Многим это покажется сложным для решения. Однако вы увидите, что мы сделаем это с минимальными усилиями. Вам нужно всего лишь немного подумать.