English Deutsch 日本語
preview
Автоматизация торговых стратегий на MQL5 (Часть 36): Торговля по зонам спроса и предложения на основе модели ретеста и импульса

Автоматизация торговых стратегий на MQL5 (Часть 36): Торговля по зонам спроса и предложения на основе модели ретеста и импульса

MetaTrader 5Трейдинг |
358 4
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

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

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

В результате у вас будет стратегия MQL5, готовая к дальнейшей настройке для торговли на ретестах зон спроса и предложения. Приступим!


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

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

Сетап зоны предложения:

СЕТАП ЗОНЫ ПРЕДЛОЖЕНИЯ

Сетап зоны спроса:

СЕТАП ЗОНЫ СПРОСА

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

СИСТЕМА НА ОСНОВЕ ЗОН СПРОСА И ПРЕДЛОЖЕНИЯ


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

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

//+------------------------------------------------------------------+
//|                                         Supply and Demand EA.mq5 |
//|                           Copyright 2025, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Forex Algo-Trader, Allan"
#property link "https://t.me/Forex_Algo_Trader"
#property version "1.00"
#property strict

#include <Trade/Trade.mqh>                         //--- Include Trade library for position management
CTrade obj_Trade;                                  //--- Instantiate trade object for order operations

Реализацию начнём с подключения торговой библиотеки директивой «#include <Trade/Trade.mqh>», которая предоставляет встроенные средства для выполнения торговых операций. Затем создадим объект «obj_Trade» класса CTrade, чтобы советник мог программно открывать позиции на покупку и продажу. Это позволит эффективно выполнять торговые операции без ручного вмешательства. Далее объявим перечисления, которые будут использоваться для классификации некоторых пользовательских настроек.

//+------------------------------------------------------------------+
//| Enum for trading tested zones                                    |
//+------------------------------------------------------------------+
enum TradeTestedZonesMode {                        // Define modes for trading tested zones
   NoRetrade,                                      // Trade zones only once
   LimitedRetrade,                                 // Trade zones up to a maximum number of times
   UnlimitedRetrade                                // Trade zones as long as they are valid
};

//+------------------------------------------------------------------+
//| Enum for broken zones validation                                 |
//+------------------------------------------------------------------+
enum BrokenZonesMode {                             // Define modes for broken zones validation
   AllowBroken,                                    // Zones can be marked as broken
   NoBroken                                        // Zones remain testable regardless of price break
};

//+------------------------------------------------------------------+
//| Enum for zone size restriction                                   |
//+------------------------------------------------------------------+
enum ZoneSizeMode {                                // Define modes for zone size restrictions
   NoRestriction,                                  // No restriction on zone size
   EnforceLimits                                   // Enforce minimum and maximum zone points
};

//+------------------------------------------------------------------+
//| Enum for trend confirmation                                      |
//+------------------------------------------------------------------+
enum TrendConfirmationMode {                       // Define modes for trend confirmation
   NoConfirmation,                                 // No trend confirmation required
   ConfirmTrend                                    // Confirm trend before trading on tap
};

Объявим несколько основных перечислений для настройки торгового поведения и подтверждения зон. Сначала создадим перечисление «TradeTestedZonesMode» с вариантами «NoRetrade» (открывать сделки по зоне только один раз), «LimitedRetrade» (торговать не более заданного количества раз) и «UnlimitedRetrade» (торговать, пока зона остаётся актуальной). Эти варианты определяют, сколько раз можно торговать одну зону. Затем объявим перечисление «BrokenZonesMode» с вариантами «AllowBroken» (помечать зону как пробитую, если цена выходит за её границы) и «NoBroken» (сохранять возможность тестирования зоны независимо от пробоя цены), которое определяет статус зоны после пробоя. Далее реализуем перечисление «ZoneSizeMode» с вариантами «NoRestriction» (без ограничений по размеру) и «EnforceLimits» (ограничивать размер зоны заданными пределами), чтобы отбирать зоны по размеру.

Наконец, добавим перечисление «TrendConfirmationMode» с вариантами «NoConfirmation» (без проверки тренда) и «ConfirmTrend» (требовать подтверждения тренда), что позволит при необходимости фильтровать сделки по направлению тренда. Эти перечисления позволяют гибко настраивать правила торговли и проверки зон. Теперь используем их при объявлении входных параметров.

//+------------------------------------------------------------------+
//| Input Parameters                                                 |
//+------------------------------------------------------------------+
input double tradeLotSize = 0.01;                   // Trade size in lots
input bool   enableTrading = true;                  // Enable automated trading
input bool   enableTrailingStop = true;             // Enable trailing stop
input double trailingStopPoints = 30;               // Trailing stop points
input double minProfitToTrail = 50;                 // Minimum trailing points
input int    uniqueMagicNumber = 12345;             // Magic Number
input int    consolidationBars = 5;                 // Consolidation range bars
input double maxConsolidationSpread = 30;           // Maximum allowed spread in points for consolidation
input double stopLossDistance = 200;                // Stop loss in points
input double takeProfitDistance = 400;              // Take profit in points
input double minMoveAwayPoints = 50;                // Minimum points price must move away before zone is ready
input bool   deleteBrokenZonesFromChart = false;    // Delete broken zones from chart
input bool   deleteExpiredZonesFromChart = false;   // Delete expired zones from chart
input int    zoneExtensionBars = 150;               // Number of bars to extend zones to the right
input bool   enableImpulseValidation = true;        // Enable impulse move validation
input int    impulseCheckBars = 3;                  // Number of bars to check for impulsive move
input double impulseMultiplier = 1.0;               // Multiplier for impulsive threshold
input TradeTestedZonesMode tradeTestedMode = NoRetrade; // Mode for trading tested zones
input int    maxTradesPerZone = 2;                  // Maximum trades per zone for LimitedRetrade
input BrokenZonesMode brokenZoneMode = AllowBroken; // Mode for broken zones validation
input color  demandZoneColor = clrBlue;             // Color for untested demand zones
input color  supplyZoneColor = clrRed;              // Color for untested supply zones
input color  testedDemandZoneColor = clrBlueViolet; // Color for tested demand zones
input color  testedSupplyZoneColor = clrOrange;     // Color for tested supply zones
input color  brokenZoneColor = clrDarkGray;         // Color for broken zones
input color  labelTextColor = clrBlack;             // Color for text labels
input ZoneSizeMode zoneSizeRestriction = NoRestriction; // Zone size restriction mode
input double minZonePoints = 50;                    // Minimum zone size in points
input double maxZonePoints = 300;                   // Maximum zone size in points
input TrendConfirmationMode trendConfirmation = NoConfirmation; // Trend confirmation mode
input int    trendLookbackBars = 10;                // Number of bars for trend confirmation
input double minTrendPoints = 1;                    // Minimum points for trend confirmation

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

//+------------------------------------------------------------------+
//| Structure for zone information                                   |
//+------------------------------------------------------------------+
struct SDZone {                                    //--- Define structure for supply/demand zones
   double   high;                                  //--- Store zone high price
   double   low;                                   //--- Store zone low price
   datetime startTime;                             //--- Store zone start time
   datetime endTime;                               //--- Store zone end time
   datetime breakoutTime;                          //--- Store breakout time
   bool     isDemand;                              //--- Indicate demand (true) or supply (false)
   bool     tested;                                //--- Track if zone was tested
   bool     broken;                                //--- Track if zone was broken
   bool     readyForTest;                          //--- Track if zone is ready for testing
   int      tradeCount;                            //--- Track number of trades on zone
   string   name;                                  //--- Store zone object name
};

//+------------------------------------------------------------------+
//| Global variables                                                 |
//+------------------------------------------------------------------+
SDZone zones[];                                    //--- Store active supply/demand zones
SDZone potentialZones[];                           //--- Store potential zones awaiting validation
int    maxZones = 50;                              //--- Set maximum number of zones to track

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   obj_Trade.SetExpertMagicNumber(uniqueMagicNumber); //--- Set magic number for trade identification
   return(INIT_SUCCEEDED);                        //--- Return initialization success
}

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   ObjectsDeleteAll(0, "SDZone_");                //--- Remove all zone objects from chart
   ChartRedraw(0);                                //--- Redraw chart to clear objects
}

Сначала создадим структуру «SDZone» для хранения сведений о зоне: верхней и нижней ценовых границ, времени начала, окончания и пробоя, признака спроса или предложения («isDemand»), признака того, что зона уже тестировалась («tested»), статуса пробоя («broken»), готовности к ретесту («readyForTest»), количества сделок («tradeCount») и имени объекта («name»). Затем объявим глобальные переменные: массив «zones» для действующих зон спроса и предложения, массив «potentialZones» для зон, ожидающих подтверждения, и переменную «maxZones» со значением 50, ограничивающую количество сопровождаемых зон. Это значение можно увеличить или уменьшить в зависимости от таймфрейма и настроек; здесь используется условно принятое значение по умолчанию.

В обработчике событий OnInit вызываем метод «SetExpertMagicNumber» объекта «obj_Trade» и передаём ему «uniqueMagicNumber» для идентификации сделок, после чего возвращаем INIT_SUCCEEDED, подтверждая успешную инициализацию. В функции OnDeinit используем ObjectsDeleteAll для удаления с графика всех объектов с префиксом «SDZone_», поскольку все объекты системы будут именоваться с этим префиксом, а затем вызываем ChartRedraw для перерисовки графика после удаления объектов. Теперь определим вспомогательные функции для обнаружения и сопровождения зон. Начнём с функции, которая поможет выводить сведения о зонах в журнал для отладки.

//+------------------------------------------------------------------+
//| Print zones for debugging                                        |
//+------------------------------------------------------------------+
void PrintZones(SDZone &arr[]) {
   Print("Current zones count: ", ArraySize(arr)); //--- Log total number of zones
   for (int i = 0; i < ArraySize(arr); i++) {     //--- Iterate through zones
      Print("Zone ", i, ": ", arr[i].name, " endTime: ", TimeToString(arr[i].endTime)); //--- Log zone details
   }
}

Для отслеживания состояния зон создадим функцию «PrintZones», которая принимает массив объектов «SDZone», выводит общее количество зон через Print и ArraySize, а затем перебирает массив и выводит индекс, имя и время окончания каждой зоны с помощью «Print» и TimeToString. Теперь перейдём к основной логике поиска зон.

//+------------------------------------------------------------------+
//| Detect supply and demand zones                                   |
//+------------------------------------------------------------------+
void DetectZones() {
   int startIndex = consolidationBars + 1;                 //--- Set start index for consolidation check
   if (iBars(_Symbol, _Period) < startIndex + 1) return;   //--- Exit if insufficient bars
   bool isConsolidated = true;                             //--- Assume consolidation
   double highPrice = iHigh(_Symbol, _Period, startIndex); //--- Initialize high price
   double lowPrice = iLow(_Symbol, _Period, startIndex);   //--- Initialize low price
   for (int i = startIndex - 1; i >= 2; i--) {             //--- Iterate through consolidation bars
      highPrice = MathMax(highPrice, iHigh(_Symbol, _Period, i)); //--- Update highest high
      lowPrice = MathMin(lowPrice, iLow(_Symbol, _Period, i)); //--- Update lowest low
      if (highPrice - lowPrice > maxConsolidationSpread * _Point) { //--- Check spread limit
         isConsolidated = false;                           //--- Mark as not consolidated
         break;                                            //--- Exit loop
      }
   }
   if (isConsolidated) {                                   //--- Confirm consolidation
      double closePrice = iClose(_Symbol, _Period, 1);     //--- Get last closed bar price
      double breakoutLow = iLow(_Symbol, _Period, 1);      //--- Get breakout bar low
      double breakoutHigh = iHigh(_Symbol, _Period, 1);    //--- Get breakout bar high
      bool isDemandZone = closePrice > highPrice && breakoutLow >= lowPrice; //--- Check demand zone
      bool isSupplyZone = closePrice < lowPrice && breakoutHigh <= highPrice; //--- Check supply zone
      if (isDemandZone || isSupplyZone) {                   //--- Validate zone type
         double zoneSize = (highPrice - lowPrice) / _Point; //--- Calculate zone size
         if (zoneSizeRestriction == EnforceLimits && (zoneSize < minZonePoints || zoneSize > maxZonePoints)) return; //--- Check size restrictions
         datetime lastClosedBarTime = iTime(_Symbol, _Period, 1); //--- Get last bar time
         bool overlaps = false;                             //--- Initialize overlap flag
         for (int j = 0; j < ArraySize(zones); j++) {       //--- Check existing zones
            if (lastClosedBarTime < zones[j].endTime) {     //--- Check time overlap
               double maxLow = MathMax(lowPrice, zones[j].low); //--- Find max low
               double minHigh = MathMin(highPrice, zones[j].high); //--- Find min high
               if (maxLow <= minHigh) {                     //--- Check price overlap
                  overlaps = true;                          //--- Mark as overlapping
                  break;                                    //--- Exit loop
               }
            }
         }
         bool duplicate = false;                        //--- Initialize duplicate flag
         for (int j = 0; j < ArraySize(zones); j++) {   //--- Check for duplicates
            if (lastClosedBarTime < zones[j].endTime) { //--- Check time
               if (MathAbs(zones[j].high - highPrice) < _Point && MathAbs(zones[j].low - lowPrice) < _Point) { //--- Check price match
                  duplicate = true;                     //--- Mark as duplicate
                  break;                                //--- Exit loop
               }
            }
         }
         if (overlaps || duplicate) return;             //--- Skip overlapping or duplicate zones
         if (enableImpulseValidation) {                 //--- Check impulse validation
            bool pot_overlaps = false;                  //--- Initialize potential overlap flag
            for (int j = 0; j < ArraySize(potentialZones); j++) { //--- Check potential zones
               if (lastClosedBarTime < potentialZones[j].endTime) { //--- Check time overlap
                  double maxLow = MathMax(lowPrice, potentialZones[j].low); //--- Find max low
                  double minHigh = MathMin(highPrice, potentialZones[j].high); //--- Find min high
                  if (maxLow <= minHigh) {              //--- Check price overlap
                     pot_overlaps = true;               //--- Mark as overlapping
                     break;                             //--- Exit loop
                  }
               }
            }
            bool pot_duplicate = false;           //--- Initialize potential duplicate flag
            for (int j = 0; j < ArraySize(potentialZones); j++) { //--- Check potential duplicates
               if (lastClosedBarTime < potentialZones[j].endTime) { //--- Check time
                  if (MathAbs(potentialZones[j].high - highPrice) < _Point && MathAbs(potentialZones[j].low - lowPrice) < _Point) { //--- Check price match
                     pot_duplicate = true;      //--- Mark as duplicate
                     break;                     //--- Exit loop
                  }
               }
            }
            if (pot_overlaps || pot_duplicate) return; //--- Skip overlapping or duplicate potential zones
            int potCount = ArraySize(potentialZones); //--- Get potential zones count
            ArrayResize(potentialZones, potCount + 1); //--- Resize potential zones array
            potentialZones[potCount].high = highPrice; //--- Set zone high
            potentialZones[potCount].low = lowPrice; //--- Set zone low
            potentialZones[potCount].startTime = iTime(_Symbol, _Period, startIndex); //--- Set start time
            potentialZones[potCount].endTime = TimeCurrent() + PeriodSeconds(_Period) * zoneExtensionBars; //--- Set end time
            potentialZones[potCount].breakoutTime = iTime(_Symbol, _Period, 1); //--- Set breakout time
            potentialZones[potCount].isDemand = isDemandZone; //--- Set zone type
            potentialZones[potCount].tested = false; //--- Set untested
            potentialZones[potCount].broken = false; //--- Set not broken
            potentialZones[potCount].readyForTest = false; //--- Set not ready
            potentialZones[potCount].tradeCount = 0; //--- Initialize trade count
            potentialZones[potCount].name = "PotentialZone_" + TimeToString(potentialZones[potCount].startTime, TIME_DATE|TIME_SECONDS); //--- Set zone name
            Print("Potential zone created: ", (isDemandZone ? "Demand" : "Supply"), " at ", lowPrice, " - ", highPrice, " endTime: ", TimeToString(potentialZones[potCount].endTime)); //--- Log potential zone
         } else {                                 //--- No impulse validation
            int zoneCount = ArraySize(zones);     //--- Get zones count
            if (zoneCount >= maxZones) {          //--- Check max zones limit
               ArrayRemove(zones, 0, 1);          //--- Remove oldest zone
               zoneCount--;                       //--- Decrease count
            }
            ArrayResize(zones, zoneCount + 1);    //--- Resize zones array
            zones[zoneCount].high = highPrice;    //--- Set zone high
            zones[zoneCount].low = lowPrice;      //--- Set zone low
            zones[zoneCount].startTime = iTime(_Symbol, _Period, startIndex); //--- Set start time
            zones[zoneCount].endTime = TimeCurrent() + PeriodSeconds(_Period) * zoneExtensionBars; //--- Set end time
            zones[zoneCount].breakoutTime = iTime(_Symbol, _Period, 1); //--- Set breakout time
            zones[zoneCount].isDemand = isDemandZone; //--- Set zone type
            zones[zoneCount].tested = false;      //--- Set untested
            zones[zoneCount].broken = false;      //--- Set not broken
            zones[zoneCount].readyForTest = false; //--- Set not ready
            zones[zoneCount].tradeCount = 0;      //--- Initialize trade count
            zones[zoneCount].name = "SDZone_" + TimeToString(zones[zoneCount].startTime, TIME_DATE|TIME_SECONDS); //--- Set zone name
            Print("Zone created: ", (isDemandZone ? "Demand" : "Supply"), " zone: ", zones[zoneCount].name, " at ", lowPrice, " - ", highPrice, " endTime: ", TimeToString(zones[zoneCount].endTime)); //--- Log zone creation
            PrintZones(zones);                    //--- Print zones for debugging
         }
      }
   }
}

Здесь реализуется логика поиска зон. В функции «DetectZones» задаём «startIndex» равным «consolidationBars + 1» и завершаем работу при недостаточном количестве баров, проверяя его через iBars. Сначала предполагаем наличие консолидации («isConsolidated = true»), инициализируем «highPrice» и «lowPrice» значениями iHigh и iLow для «startIndex», а затем перебираем бары в обратном порядке, обновляя границы с помощью MathMax и MathMin. Если диапазон превышает «maxConsolidationSpread * _Point», устанавливаем «isConsolidated» в false. При подтверждённой консолидации проверяем цену закрытия последнего бара («iClose»), его минимум («iLow») и максимум («iHigh»). Зона спроса определяется, если «closePrice > highPrice» и «breakoutLow >= lowPrice», а зона предложения — если «closePrice < lowPrice» и «breakoutHigh <= highPrice».

Для найденных зон проверяем ограничения по размеру через «zoneSizeRestriction», «minZonePoints» и «maxZonePoints», а также пересечения и дубликаты в массивах «zones» и «potentialZones» с помощью «MathMax» и «MathMin». Если «enableImpulseValidation» равно true, добавляем зону в «potentialZones» через ArrayResize, заполняя поля «high», «low», «startTime» (iTime), «endTime» («TimeCurrent + zoneExtensionBars») и «name» («PotentialZone»), а затем выводим сведения через «Print». Если проверка импульса отключена, сразу добавляем зону в «zones», при достижении «maxZones» удаляем самую старую и выводим сведения через «Print» и «PrintZones». Так мы реализуем базовую логику поиска и хранения зон спроса и предложения. Вызовем эту функцию в обработчике тиков OnTick.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick() {
   static datetime lastBarTime = 0;                      //--- Store last processed bar time
   datetime currentBarTime = iTime(_Symbol, _Period, 0); //--- Get current bar time
   bool isNewBar = (currentBarTime != lastBarTime);      //--- Check for new bar
   if (isNewBar) {                                       //--- Process new bar
      lastBarTime = currentBarTime;                      //--- Update last bar time
      DetectZones();                                     //--- Detect new zones
   }
}

В обработчике событий OnTick отслеживаем появление нового бара, сравнивая время текущего бара, полученное через iTime для текущего символа и периода со сдвигом 0, со статической переменной «lastBarTime». Если значения различаются, устанавливаем «isNewBar» в true и обновляем «lastBarTime». После появления нового бара вызываем функцию «DetectZones», которая ищет новые зоны спроса и предложения на основе паттернов консолидации. Теперь система способна определять зоны, как показано ниже.

ОБНАРУЖЕННЫЕ ПОТЕНЦИАЛЬНЫЕ ЗОНЫ

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

//+------------------------------------------------------------------+
//| Validate potential zones based on impulsive move                 |
//+------------------------------------------------------------------+
void ValidatePotentialZones() {
   datetime lastClosedBarTime = iTime(_Symbol, _Period, 1);   //--- Get last closed bar time
   for (int p = ArraySize(potentialZones) - 1; p >= 0; p--) { //--- Iterate potential zones backward
      if (lastClosedBarTime >= potentialZones[p].endTime) {   //--- Check for expired zone
         Print("Potential zone expired and removed from array: ", potentialZones[p].name, " endTime: ", TimeToString(potentialZones[p].endTime)); //--- Log expiration
         ArrayRemove(potentialZones, p, 1);                   //--- Remove expired zone
         continue;                                            //--- Skip to next
      }
      if (TimeCurrent() > potentialZones[p].breakoutTime + impulseCheckBars * PeriodSeconds(_Period)) { //--- Check impulse window
         bool isImpulsive = false;                            //--- Initialize impulsive flag
         int breakoutShift = iBarShift(_Symbol, _Period, potentialZones[p].breakoutTime, false); //--- Get breakout bar shift
         double range = potentialZones[p].high - potentialZones[p].low; //--- Calculate zone range
         double threshold = range * impulseMultiplier;        //--- Calculate impulse threshold
         for (int shift = 1; shift <= impulseCheckBars; shift++) { //--- Check bars after breakout
            if (shift + breakoutShift >= iBars(_Symbol, _Period)) continue; //--- Skip out-of-bounds
            double cl = iClose(_Symbol, _Period, shift);      //--- Get close price
            if (potentialZones[p].isDemand) {                 //--- Check demand zone
               if (cl >= potentialZones[p].high + threshold) { //--- Check bullish impulse
                  isImpulsive = true;                         //--- Set impulsive flag
                  break;                                      //--- Exit loop
               }
            } else {                                          //--- Check supply zone
               if (cl <= potentialZones[p].low - threshold) { //--- Check bearish impulse
                  isImpulsive = true;                         //--- Set impulsive flag
                  break;                                      //--- Exit loop
               }
            }
         }
         if (isImpulsive) {                                  //--- Process impulsive zone
            double zoneSize = (potentialZones[p].high - potentialZones[p].low) / _Point; //--- Calculate zone size
            if (zoneSizeRestriction == EnforceLimits && (zoneSize < minZonePoints || zoneSize > maxZonePoints)) { //--- Check size limits
               ArrayRemove(potentialZones, p, 1);            //--- Remove invalid zone
               continue;                                     //--- Skip to next
            }
            bool overlaps = false;                           //--- Initialize overlap flag
            for (int j = 0; j < ArraySize(zones); j++) {     //--- Check existing zones
               if (lastClosedBarTime < zones[j].endTime) {   //--- Check time overlap
                  double maxLow = MathMax(potentialZones[p].low, zones[j].low); //--- Find max low
                  double minHigh = MathMin(potentialZones[p].high, zones[j].high); //--- Find min high
                  if (maxLow <= minHigh) {                   //--- Check price overlap
                     overlaps = true;                        //--- Mark as overlapping
                     break;                                  //--- Exit loop
                  }
               }
            }
            bool duplicate = false;                          //--- Initialize duplicate flag
            for (int j = 0; j < ArraySize(zones); j++) {     //--- Check for duplicates
               if (lastClosedBarTime < zones[j].endTime) {   //--- Check time
                  if (MathAbs(zones[j].high - potentialZones[p].high) < _Point && MathAbs(zones[j].low - potentialZones[p].low) < _Point) { //--- Check price match
                     duplicate = true;                       //--- Mark as duplicate
                     break;                                  //--- Exit loop
                  }
               }
            }
            if (overlaps || duplicate) {                     //--- Check overlap or duplicate
               Print("Validated zone overlaps or duplicates, discarded: ", potentialZones[p].low, " - ", potentialZones[p].high); //--- Log discard
               ArrayRemove(potentialZones, p, 1);            //--- Remove zone
               continue;                                     //--- Skip to next
            }
            int zoneCount = ArraySize(zones);                //--- Get zones count
            if (zoneCount >= maxZones) {                     //--- Check max zones limit
               ArrayRemove(zones, 0, 1);                     //--- Remove oldest zone
               zoneCount--;                                  //--- Decrease count
            }
            ArrayResize(zones, zoneCount + 1);               //--- Resize zones array
            zones[zoneCount] = potentialZones[p];            //--- Copy potential zone
            zones[zoneCount].name = "SDZone_" + TimeToString(zones[zoneCount].startTime, TIME_DATE|TIME_SECONDS); //--- Set zone name
            zones[zoneCount].endTime = TimeCurrent() + PeriodSeconds(_Period) * zoneExtensionBars; //--- Update end time
            Print("Zone validated: ", (zones[zoneCount].isDemand ? "Demand" : "Supply"), " zone: ", zones[zoneCount].name, " at ", zones[zoneCount].low, " - ", zones[zoneCount].high, " endTime: ", TimeToString(zones[zoneCount].endTime)); //--- Log validation
            ArrayRemove(potentialZones, p, 1);               //--- Remove validated zone
            PrintZones(zones);                               //--- Print zones for debugging
         } else {                                            //--- Zone not impulsive
            Print("Potential zone not impulsive, discarded: ", potentialZones[p].low, " - ", potentialZones[p].high); //--- Log discard
            ArrayRemove(potentialZones, p, 1);               //--- Remove non-impulsive zone
         }
      }
   }
}

Создадим функцию для подтверждения потенциальных зон спроса и предложения. В функции «ValidatePotentialZones» перебираем массив «potentialZones» в обратном порядке. Если время последнего закрытого бара (iTime со сдвигом 1) превышает «endTime» зоны, удаляем истёкшую зону через ArrayRemove и выводим сообщение в журнал. Для зон, находящихся в пределах окна проверки импульса («TimeCurrent > breakoutTime + impulseCheckBars * PeriodSeconds»), рассчитываем диапазон зоны («high - low») и порог импульса («range * impulseMultiplier»). Затем проверяем бары после пробоя (iBarShift) и цены закрытия (iClose): для зоны спроса цена должна превысить верхнюю границу плюс порог, а для зоны предложения — опуститься ниже нижней границы минус порог. При выполнении условия устанавливаем «isImpulsive» в true.

Если импульс подтверждён, при режиме «EnforceLimits» проверяем размер зоны относительно «minZonePoints» и «maxZonePoints», затем ищем пересечения и дубликаты в массиве «zones» с помощью MathMax и MathMin. Если зона соответствует условиям, переносим её в «zones» через ArrayResize, обновляем имя на «SDZone_» и время окончания, выводим сведения функциями «Print» и «PrintZones», а затем удаляем её из «potentialZones». Зоны без подтверждённого импульса удаляем через ArrayRemove и также фиксируем это в журнале. Так система подтверждает зоны импульсными движениями и сохраняет только уникальные зоны, соответствующие заданным условиям. После вызова функции в обработчике тиков получим результат, показанный ниже.

ПОДТВЕРЖДЕНИЕ ЗОН СПРОСА И ПРЕДЛОЖЕНИЯ

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

//+------------------------------------------------------------------+
//| Update and draw zones                                            |
//+------------------------------------------------------------------+
void UpdateZones() {
   datetime lastClosedBarTime = iTime(_Symbol, _Period, 1); //--- Get last closed bar time
   for (int i = ArraySize(zones) - 1; i >= 0; i--) { //--- Iterate zones backward
      if (lastClosedBarTime >= zones[i].endTime) { //--- Check for expired zone
         Print("Zone expired and removed from array: ", zones[i].name, " endTime: ", TimeToString(zones[i].endTime)); //--- Log expiration
         if (deleteExpiredZonesFromChart) {    //--- Check if deleting expired
            ObjectDelete(0, zones[i].name);    //--- Delete zone rectangle
            ObjectDelete(0, zones[i].name + "Label"); //--- Delete zone label
         }
         ArrayRemove(zones, i, 1);             //--- Remove expired zone
         continue;                             //--- Skip to next
      }
      bool wasReady = zones[i].readyForTest;   //--- Store previous ready status
      if (!zones[i].readyForTest) {            //--- Check if not ready
         double currentClose = iClose(_Symbol, _Period, 1); //--- Get current close
         double zoneLevel = zones[i].isDemand ? zones[i].high : zones[i].low; //--- Get zone level
         double distance = zones[i].isDemand ? (currentClose - zoneLevel) : (zoneLevel - currentClose); //--- Calculate distance
         if (distance > minMoveAwayPoints * _Point) { //--- Check move away distance
            zones[i].readyForTest = true;      //--- Set ready for test
         }
      }
      if (!wasReady && zones[i].readyForTest) { //--- Check if newly ready
         Print("Zone ready for test: ", zones[i].name); //--- Log ready status
      }
      if (brokenZoneMode == AllowBroken && !zones[i].tested) { //--- Check if breakable
         double currentClose = iClose(_Symbol, _Period, 1); //--- Get current close
         bool wasBroken = zones[i].broken;     //--- Store previous broken status
         if (zones[i].isDemand) {              //--- Check demand zone
            if (currentClose < zones[i].low) { //--- Check if broken
               zones[i].broken = true;         //--- Mark as broken
            }
         } else {                              //--- Check supply zone
            if (currentClose > zones[i].high) { //--- Check if broken
               zones[i].broken = true;         //--- Mark as broken
            }
         }
         if (!wasBroken && zones[i].broken) {  //--- Check if newly broken
            Print("Zone broken in UpdateZones: ", zones[i].name); //--- Log broken zone
            ObjectSetInteger(0, zones[i].name, OBJPROP_COLOR, brokenZoneColor); //--- Update zone color
            string labelName = zones[i].name + "Label"; //--- Get label name
            string labelText = zones[i].isDemand ? "Demand Zone (Broken)" : "Supply Zone (Broken)"; //--- Set broken label
            ObjectSetString(0, labelName, OBJPROP_TEXT, labelText); //--- Update label text
            if (deleteBrokenZonesFromChart) {  //--- Check if deleting broken
               ObjectDelete(0, zones[i].name); //--- Delete zone rectangle
               ObjectDelete(0, labelName);     //--- Delete zone label
            }
         }
      }
      if (ObjectFind(0, zones[i].name) >= 0 || (!zones[i].broken || !deleteBrokenZonesFromChart)) { //--- Check if drawable
         color zoneColor;                        //--- Initialize zone color
         if (zones[i].tested) {                  //--- Check if tested
            zoneColor = zones[i].isDemand ? testedDemandZoneColor : testedSupplyZoneColor; //--- Set tested color
         } else if (zones[i].broken) {           //--- Check if broken
            zoneColor = brokenZoneColor;         //--- Set broken color
         } else {                                //--- Untested zone
            zoneColor = zones[i].isDemand ? demandZoneColor : supplyZoneColor; //--- Set untested color
         }
         ObjectCreate(0, zones[i].name, OBJ_RECTANGLE, 0, zones[i].startTime, zones[i].high, zones[i].endTime, zones[i].low); //--- Create zone rectangle
         ObjectSetInteger(0, zones[i].name, OBJPROP_COLOR, zoneColor); //--- Set zone color
         ObjectSetInteger(0, zones[i].name, OBJPROP_FILL, true); //--- Enable fill
         ObjectSetInteger(0, zones[i].name, OBJPROP_BACK, true); //--- Set to background
         string labelName = zones[i].name + "Label"; //--- Generate label name
         string labelText = zones[i].isDemand ? "Demand Zone" : "Supply Zone"; //--- Set base label
         if (zones[i].tested) labelText += " (Tested)"; //--- Append tested status
         else if (zones[i].broken) labelText += " (Broken)"; //--- Append broken status
         datetime labelTime = zones[i].startTime + (zones[i].endTime - zones[i].startTime) / 2; //--- Calculate label time
         double labelPrice = (zones[i].high + zones[i].low) / 2; //--- Calculate label price
         ObjectCreate(0, labelName, OBJ_TEXT, 0, labelTime, labelPrice); //--- Create label
         ObjectSetString(0, labelName, OBJPROP_TEXT, labelText); //--- Set label text
         ObjectSetInteger(0, labelName, OBJPROP_COLOR, labelTextColor); //--- Set label color
         ObjectSetInteger(0, labelName, OBJPROP_ANCHOR, ANCHOR_CENTER); //--- Set label anchor
      }
   }
   ChartRedraw(0);                                //--- Redraw chart
}

Теперь реализуем сопровождение и визуализацию зон. В функции «UpdateZones» перебираем массив «zones» в обратном порядке. Если время последнего закрытого бара (iTime со сдвигом 1) превышает «endTime» зоны, удаляем истёкшую зону через ArrayRemove и, если «deleteExpiredZonesFromChart» равно true, удаляем её графические объекты (OBJ_RECTANGLE и «Label»), после чего выводим сообщение в журнал. После этого истёкшие зоны больше не обрабатываются. Для зон, ещё не готовых к ретесту, рассчитываем расстояние от текущей цены закрытия (iClose) до верхней границы зоны спроса или нижней границы зоны предложения. Если расстояние превышает «minMoveAwayPoints * _Point», устанавливаем «readyForTest» в true и при первом изменении статуса выводим сообщение.

Если «brokenZoneMode» имеет значение «AllowBroken» и зона ещё не была отмечена как протестированная, помечаем её как пробитую, когда цена закрывается ниже нижней границы зоны спроса или выше верхней границы зоны предложения. Цвет меняем на «brokenZoneColor» через ObjectSetInteger, а подпись — на «“Demand Zone (Broken)” / “Supply Zone (Broken)”» через ObjectSetString. Если «deleteBrokenZonesFromChart» равно true, удаляем объекты и выводим сообщение в журнал. Для отображаемых зон выбираем цвет в зависимости от статуса: «demandZoneColor», «supplyZoneColor», «testedDemandZoneColor», «testedSupplyZoneColor» или «brokenZoneColor». Прямоугольники создаём через ObjectCreate («OBJ_RECTANGLE») по значениям «startTime», «high», «endTime» и «low», а центральные подписи — через «ObjectCreate» (OBJ_TEXT) с цветом «labelTextColor». Затем обновляем график функцией ChartRedraw. Так система обновляет состояния зон и динамически отображает их на графике. После вызова функции в обработчике тиков получим следующий результат.

ОТСЛЕЖИВАЕМЫЕ И ВИЗУАЛИЗИРОВАННЫЕ ЗОНЫ

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

//+------------------------------------------------------------------+
//| Trade on zones                                                   |
//+------------------------------------------------------------------+
void TradeOnZones(bool isNewBar) {
   static datetime lastTradeCheck = 0;                   //--- Store last trade check time
   datetime currentBarTime = iTime(_Symbol, _Period, 0); //--- Get current bar time
   if (!isNewBar || lastTradeCheck == currentBarTime) return; //--- Exit if not new bar or checked
   lastTradeCheck = currentBarTime;                      //--- Update last trade check
   double currentBid = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID), _Digits); //--- Get current bid
   double currentAsk = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK), _Digits); //--- Get current ask
   for (int i = 0; i < ArraySize(zones); i++) {          //--- Iterate through zones
      if (zones[i].broken) continue;                     //--- Skip broken zones
      if (tradeTestedMode == NoRetrade && zones[i].tested) continue; //--- Skip tested zones
      if (tradeTestedMode == LimitedRetrade && zones[i].tested && zones[i].tradeCount >= maxTradesPerZone) continue; //--- Skip max trades
      if (!zones[i].readyForTest) continue;              //--- Skip not ready zones
      double prevHigh = iHigh(_Symbol, _Period, 1);      //--- Get previous high
      double prevLow = iLow(_Symbol, _Period, 1);        //--- Get previous low
      double prevClose = iClose(_Symbol, _Period, 1);    //--- Get previous close
      bool tapped = false;                               //--- Initialize tap flag
      bool overlap = (prevLow <= zones[i].high && prevHigh >= zones[i].low); //--- Check candle overlap
      if (zones[i].isDemand) {                           //--- Check demand zone
         if (overlap && prevClose > zones[i].high) {     //--- Confirm demand tap
            tapped = true;                               //--- Set tapped flag
         }
      } else {                                           //--- Check supply zone
         if (overlap && prevClose < zones[i].low) {      //--- Confirm supply tap
            tapped = true;                               //--- Set tapped flag
         }
      }
      if (tapped) {                                      //--- Process tapped zone
         bool trendConfirmed = (trendConfirmation == NoConfirmation); //--- Assume no trend confirmation
         if (trendConfirmation == ConfirmTrend) {        //--- Check trend confirmation
            int oldShift = 2 + trendLookbackBars - 1;    //--- Calculate lookback shift
            if (oldShift >= iBars(_Symbol, _Period)) continue; //--- Skip if insufficient bars
            double oldClose = iClose(_Symbol, _Period, oldShift); //--- Get old close
            double recentClose = iClose(_Symbol, _Period, 2); //--- Get recent close
            double minChange = minTrendPoints * _Point; //--- Calculate min trend change
            if (zones[i].isDemand) {                    //--- Check demand trend
               trendConfirmed = (oldClose > recentClose + minChange); //--- Confirm downtrend
            } else {                                    //--- Check supply trend
               trendConfirmed = (oldClose < recentClose - minChange); //--- Confirm uptrend
            }
         }
         if (!trendConfirmed) continue;                 //--- Skip if trend not confirmed
         bool wasTested = zones[i].tested;              //--- Store previous tested status
         if (zones[i].isDemand) {                       //--- Handle demand trade
            double entryPrice = currentAsk;             //--- Set entry at ask
            double stopLossPrice = NormalizeDouble(zones[i].low - stopLossDistance * _Point, _Digits); //--- Set stop loss
            double takeProfitPrice = NormalizeDouble(entryPrice + takeProfitDistance * _Point, _Digits); //--- Set take profit
            obj_Trade.Buy(tradeLotSize, _Symbol, entryPrice, stopLossPrice, takeProfitPrice, "Buy at Demand Zone"); //--- Execute buy trade
            Print("Buy trade entered at Demand Zone: ", zones[i].name); //--- Log buy trade
         } else {                                       //--- Handle supply trade
            double entryPrice = currentBid;             //--- Set entry at bid
            double stopLossPrice = NormalizeDouble(zones[i].high + stopLossDistance * _Point, _Digits); //--- Set stop loss
            double takeProfitPrice = NormalizeDouble(entryPrice - takeProfitDistance * _Point, _Digits); //--- Set take profit
            obj_Trade.Sell(tradeLotSize, _Symbol, entryPrice, stopLossPrice, takeProfitPrice, "Sell at Supply Zone"); //--- Execute sell trade
            Print("Sell trade entered at Supply Zone: ", zones[i].name); //--- Log sell trade
         }
         zones[i].tested = true;                        //--- Mark zone as tested
         zones[i].tradeCount++;                         //--- Increment trade count
         if (!wasTested && zones[i].tested) {           //--- Check if newly tested
            Print("Zone tested: ", zones[i].name, ", Trade count: ", zones[i].tradeCount); //--- Log tested zone
         }
         color zoneColor = zones[i].isDemand ? testedDemandZoneColor : testedSupplyZoneColor; //--- Set tested color
         ObjectSetInteger(0, zones[i].name, OBJPROP_COLOR, zoneColor); //--- Update zone color
         string labelName = zones[i].name + "Label";                   //--- Get label name
         string labelText = zones[i].isDemand ? "Demand Zone (Tested)" : "Supply Zone (Tested)"; //--- Set tested label
         ObjectSetString(0, labelName, OBJPROP_TEXT, labelText);       //--- Update label text
      }
   }
   ChartRedraw(0);                                       //--- Redraw chart
}

Для реализации торговли на ретестах зон создадим функцию «TradeOnZones». Сначала отслеживаем новые бары с помощью статической переменной «lastTradeCheck». Если новый бар не появился или уже был проверен, завершаем работу; иначе обновляем «lastTradeCheck» через iTime и получаем цены Bid и Ask, нормализованные функциями SymbolInfoDouble и NormalizeDouble. Затем перебираем массив «zones», пропуская пробитые и неготовые зоны, а также зоны, по которым уже достигнут допустимый лимит сделок в соответствии с «tradeTestedMode» и «maxTradesPerZone». Далее проверяем максимум (iHigh), минимум («iLow») и цену закрытия (iClose) предыдущего бара на пересечение с зоной. Для зоны спроса касание подтверждается, если есть пересечение и «prevClose > high»; для зоны предложения — если есть пересечение и «prevClose < low».

При ретесте зоны, если «trendConfirmation» равно «ConfirmTrend», подтверждаем тренд сравнением более ранних и недавних цен закрытия («iClose») за «trendLookbackBars» с порогом «minTrendPoints * _Point». Если тренд не подтверждён, пропускаем сигнал. При подтверждённом касании зоны спроса открываем покупку по Ask: стоп-лосс устанавливаем ниже «low» на «stopLossDistance * _Point», а тейк-профит — выше цены входа на «takeProfitDistance * _Point», используя «obj_Trade.Buy», и выводим результат через Print. Для зоны предложения открываем продажу по Bid со стоп-лоссом выше «high» и тейк-профитом ниже цены входа через «obj_Trade.Sell». После сделки помечаем зону как прошедшую ретест, увеличиваем «tradeCount», при первом ретесте выводим сообщение, меняем цвет зоны на «testedDemandZoneColor» или «testedSupplyZoneColor» через ObjectSetInteger и обновляем подпись на «“Demand Zone (Tested)” / “Supply Zone (Tested)”» функцией ObjectSetString. В завершение перерисовываем график. После вызова функции получим следующий результат.

ПОДТВЕРЖДЁННЫЕ СИГНАЛЫ

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

//+------------------------------------------------------------------+
//| Apply trailing stop to open positions                            |
//+------------------------------------------------------------------+
void ApplyTrailingStop() {     
   double point = _Point;                               //--- Get point value
   for (int i = PositionsTotal() - 1; i >= 0; i--) {    //--- Iterate through positions
      if (PositionGetTicket(i) > 0) {                   //--- Check valid ticket
         if (PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == uniqueMagicNumber) { //--- Verify symbol and magic
            double sl = PositionGetDouble(POSITION_SL); //--- Get current stop loss
            double tp = PositionGetDouble(POSITION_TP); //--- Get current take profit
            double openPrice = PositionGetDouble(POSITION_PRICE_OPEN); //--- Get open price
            if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { //--- Check buy position
               double newSL = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID) - trailingStopPoints * point, _Digits); //--- Calculate new stop loss
               if (newSL > sl && SymbolInfoDouble(_Symbol, SYMBOL_BID) - openPrice > minProfitToTrail * point) { //--- Check trailing condition
                  obj_Trade.PositionModify(PositionGetInteger(POSITION_TICKET), newSL, tp); //--- Update stop loss
               }
            } else if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) { //--- Check sell position
               double newSL = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK) + trailingStopPoints * point, _Digits); //--- Calculate new stop loss
               if (newSL < sl && openPrice - SymbolInfoDouble(_Symbol, SYMBOL_ASK) > minProfitToTrail * point) { //--- Check trailing condition
                  obj_Trade.PositionModify(PositionGetInteger(POSITION_TICKET), newSL, tp); //--- Update stop loss
               }
            }
         }
      }
   }
}

Здесь реализуем трейлинг-стоп для динамического сопровождения открытых позиций. В функции «ApplyTrailingStop» получаем размер пункта через _Point и перебираем открытые позиции в обратном порядке с помощью PositionsTotal, проверяя тикет через PositionGetTicket, символ через PositionGetString и магический номер через PositionGetInteger, сопоставляя его с магическим номером советника.

Для позиций на покупку (POSITION_TYPE_BUY) рассчитываем новый стоп-лосс как цену Bid (SymbolInfoDouble с параметром SYMBOL_BID) минус «trailingStopPoints * point» и нормализуем его через NormalizeDouble. Стоп-лосс изменяем методом «obj_Trade.PositionModify», если новое значение выше текущего стоп-лосса («PositionGetDouble(POSITION_SL)») и прибыль превышает «minProfitToTrail * point». Для позиций на продажу новый стоп-лосс рассчитывается как цена Ask («SYMBOL_ASK») плюс «trailingStopPoints * point» и обновляется, если он ниже текущего стоп-лосса, а прибыль превышает установленный порог. Так стоп-лоссы следуют за благоприятным движением цены и помогают сохранять накопленную прибыль. Теперь функцию можно вызывать на каждом тике.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick() {
   if (enableTrailingStop) {                      //--- Check if trailing stop enabled
      ApplyTrailingStop();                        //--- Apply trailing stop to positions
   }
   static datetime lastBarTime = 0;               //--- Store last processed bar time
   datetime currentBarTime = iTime(_Symbol, _Period, 0); //--- Get current bar time
   bool isNewBar = (currentBarTime != lastBarTime); //--- Check for new bar
   if (isNewBar) {                                //--- Process new bar
      lastBarTime = currentBarTime;               //--- Update last bar time
      DetectZones();                              //--- Detect new zones
      ValidatePotentialZones();                   //--- Validate potential zones
      UpdateZones();                              //--- Update existing zones
   }
   if (enableTrading) {                           //--- Check if trading enabled
      TradeOnZones(isNewBar);                     //--- Execute trades on zones
   }
}

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

АКТИВАЦИЯ ТРЕЙЛИНГ-СТОПА

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

ЕДИНЫЙ ТЕСТ ЗОН СПРОСА И ПРЕДЛОЖЕНИЯ

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


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

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

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

ГРАФИК БЭКТЕСТА

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

ОТЧЁТ О БЭКТЕСТЕ


Заключение

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

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

Эта стратегия на основе зон спроса и предложения позволяет использовать торговые возможности при ретестах и готова к дальнейшей оптимизации. Успешной торговли!

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

Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (4)
jiyanazad
jiyanazad | 27 нояб. 2025 в 20:59
Привет,
Я скомпилировал его без ошибок. К сожалению, ни в бэктесте, ни на графике ничего не отображается. Не выводятся зоны спроса и предложения, не распознаются сигналы и не открываются сделки. Журнал отладки и журнал операций также пусты. У кого-нибудь это заработало? Может ли система автоматически проверить, полный ли код и актуален ли он?
bzaranyika
bzaranyika | 4 дек. 2025 в 19:04
Работа велась на 1-минутном таймфрейме
Allan Munene Mutiiria
Allan Munene Mutiiria | 5 дек. 2025 в 07:52
bzaranyika #:
Работа велась на 1-минутном таймфрейме
Спасибо за добрые отзывы
Olowogbade Sunday
Olowogbade Sunday | 6 янв. 2026 в 23:44
Спасибо за такую огромную помощь

Но с некоторыми котировками это не работает, может, это ошибка с моей стороны?
Если да, то, пожалуйста, объясните, в чём дело и каковы критерии, при которых код будет работать с любой валютой или торгуемым активом?
Моделирование рынка: Position View (X) Моделирование рынка: Position View (X)
Нам необходим способ обработки создаваемых графических объектов. Предложение, представленное в предыдущей статье, отлично подходит для некоторых сценариев. В данном случае нам потребуется нечто более сложное, учитывая специфику рассматриваемой проблемы. Поэтому мы не будем пытаться заменить существующие в MetaTrader 5 механизмы управления ZOrder и, конечно же, проверять, какой объект находится на переднем плане или скрыт другим объектом. Мы сделаем нечто совершенно другое. Здесь я покажу, какие изменения необходимо внести в код, чтобы задействовать часть того, что MetaTrader 5 уже делает за нас.
Анализ CSV-данных (Часть 6): Нормализация результатов по нескольким брокерам и сверка CSV между платформами Анализ CSV-данных (Часть 6): Нормализация результатов по нескольким брокерам и сверка CSV между платформами
В статье представлен фреймворк нормализации CSV-данных от нескольких брокеров. Подключаемый файл MQL5 дополняет экспорт брокерскими метаданными. Модуль Python устраняет расхождения схем — различия в разрядности котирования, соглашениях о пунктах, именовании символов, смещениях часовых поясов, моделях учёта комиссии и валютах номинирования счёта и прибыли — и формирует единый канонический набор данных. Сравнительные визуализации распределений проскальзывания и результатов после учёта издержек позволяют надёжно сопоставлять результаты стратегии между разными брокерскими средами без незаметного искажения данных.
Кооперация бионических алгоритмов — Co-Operation of Biology Related Algorithms (COBRA) Кооперация бионических алгоритмов — Co-Operation of Biology Related Algorithms (COBRA)
В статье реализована кооперативная метаэвристика COBRA во фреймворке C_AO: шесть популяционных методов (PSO, WPS, FFA, CSA, BA, FSS) работают в одной популяции при фиксированном бюджете обращений к целевой функции и экспериментально выясняем, на чём на самом деле держится кооперация. В комплекте — расширяемый класс для сборки собственных кооперативных портфелей из любых алгоритмов серии.
Возможности Мастера MQL5, которые вам нужно знать (Часть 82): Использование паттернов TRIX и WPR в обучении с подкреплением DQN Возможности Мастера MQL5, которые вам нужно знать (Часть 82): Использование паттернов TRIX и WPR в обучении с подкреплением DQN
В предыдущей статье мы рассмотрели сочетание Ишимоку и ADX в рамках обучения на основе инференса. В этой статье мы вновь обращаемся к обучению с подкреплением применительно к паре индикаторов, которую уже рассматривали в части 68: TRIX и Williams Percent Range. В этой статье используем алгоритм алгоритм DQN с квантильной регрессией. Как обычно, представляем его в виде пользовательского класса сигнала, предназначенного для использования с Мастером MQL5.