Автоматизация торговых стратегий на MQL5 (Часть 36): Торговля по зонам спроса и предложения на основе модели ретеста и импульса
Введение
В нашей предыдущей статье (Часть 35) мы разработали систему на основе брейкер-блоков на языке MetaQuotes Language 5 (MQL5), которая определяла диапазоны консолидации, подтверждала брейкер-блоки по свинговым точкам и открывала позиции на их ретестах с настраиваемыми параметрами риска и визуальным отображением на графике. В Части 36 мы разработаем торговую систему на основе зон спроса и предложения с моделью ретеста и импульса. Эта модель определяет зоны спроса и предложения по консолидации, подтверждает их импульсными движениями и открывает позиции на ретестах с подтверждением тренда и динамической визуализацией на графике. Мы рассмотрим следующие темы:
- Принцип работы стратегии на основе зон спроса и предложения
- Реализация на MQL5
- Бэктестирование
- Заключение
В результате у вас будет стратегия 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
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Моделирование рынка: Position View (X)
Анализ CSV-данных (Часть 6): Нормализация результатов по нескольким брокерам и сверка CSV между платформами
Кооперация бионических алгоритмов — Co-Operation of Biology Related Algorithms (COBRA)
Возможности Мастера MQL5, которые вам нужно знать (Часть 82): Использование паттернов TRIX и WPR в обучении с подкреплением DQN
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Я скомпилировал его без ошибок. К сожалению, ни в бэктесте, ни на графике ничего не отображается. Не выводятся зоны спроса и предложения, не распознаются сигналы и не открываются сделки. Журнал отладки и журнал операций также пусты. У кого-нибудь это заработало? Может ли система автоматически проверить, полный ли код и актуален ли он?
Работа велась на 1-минутном таймфрейме