Свинговые экстремумы и откаты в MQL 5 (Часть 4): Динамическая глубина отката на основе моделей волатильности
Содержание
Введение
Ранее мы разработали систему на основе рыночной структуры, способную обнаруживать корректные свинги, проверять рыночную структуру и выявлять откаты для входа в сделки. Однако большинство трейдеров, использующих системы на основе рыночной структуры, рано или поздно сталкиваются с одной и той же проблемой: советник отмечает корректные свинги, обнаруживает откат и входит в сделку, но затем раз за разом получает убыток по стоп-лоссу на движениях, которые прежде приносили прибыль. Система не может объяснить, почему. Коррекция на 40 пунктов в одной сессии приводит к открытию позиции на покупку, а в другой — сразу же к убытку. Логика кажется обоснованной, но результаты нестабильны. Настоящая проблема заключается не в самом входе, а в том, что система рассматривает все откаты как равнозначные. У системы нет средств для описания глубины отката, понимания того, насколько глубокой должна быть коррекция цены при текущей волатильности, и возможности отличить здоровую коррекцию от начинающегося разрушения рыночной структуры.
Решение — перестать спрашивать: «Произошел ли откат цены?» — и начать задавать вопрос: «Насколько глубоким был откат относительно того, что действительно оправдано текущей волатильностью рынка?» В этой системе используется модель коррекции, нормализованная по волатильности. Модель измеряет глубину каждой коррекции как долю предшествующего импульса и настраивает пороговые значения допустимой глубины в соответствии с режимом волатильности, определяемым по скользящему ATR. Откат на 40 пунктов в условиях низкого ATR считается опасно глубоким. Тот же откат на 40 пунктов в фазе расширения с высоким ATR классифицируется как здоровый и пригодный для торговли. Каждому откату присваивается оценка качества, сжатие выявляется до начала расширения, а размер стоп-лоссов увеличивается или уменьшается в зависимости от рыночного режима — так советник больше не борется с рыночными условиями, которые он не способен распознать.
Обзор системы
Советник определяет максимумы и минимумы свингов с помощью окна ретроспективного анализа. Каждый кандидат проходит несколько проверок, включая пробой структуры (новый максимум или минимум), смещение, оцениваемое по размеру свечи, свип ликвидности и удержание во времени. Подтверждённые свинги служат основой для определения структуры рынка и разметки зон ликвидности. Затем автомат состояний определяет фазу рынка — накопление, расширение, распределение или разворот — на основе последовательности свингов и неудачных свипов.

Уровень 1 считывает исходную структуру цены; уровень 2 подтверждает свинги с помощью четырёх независимых проверок; уровень 3 размечает пулы ликвидности, которые в дальнейшем служат магнитами для цены.
Как выполняется разметка архитектуры рынка:
- Шаг 1: Обнаружение кандидатов в свинги → Проверка по критериям пробоя структуры, смещения, свипа ликвидности и удержания во времени.
- Шаг 2: Автомат состояний определяет фазу рынка — накопление, расширение, распределение или разворот.
- Шаг 3: Отслеживание зон ликвидности — скоплений максимумов и минимумов; после свипа ликвидности зоны помечаются как снятые.
Второй уровень моделирует откаты в рамках каждого импульсного участка. Он измеряет глубину коррекции, нормирует её относительно ATR и корректирует допустимые диапазоны в соответствии с текущим режимом волатильности. Каждому откату присваивается оценка качества, которая учитывает глубину, перекрытие свечей, затухание импульса и сжатие волатильности. Сделка открывается только в том случае, если откат получает высокую оценку качества, соответствует состоянию рынка, остаётся в пределах адаптивных ограничений глубины и подтверждается либо свипом ликвидности, либо сильной свечой смещения. Стоп-лоссы размещаются динамически относительно ближайших уровней свингов с дополнительным отступом на основе ATR.

Ни одна сделка не открывается без отката, получившего оценку качества и укладывающегося в адаптивные ограничения глубины, а каждый вход требует подтверждения свипом ликвидности или свечой смещения — советник ждёт, пока структура подтвердится.
Как работают фильтрация по качеству и логика исполнения сделок:
- Шаг 1: Измерение каждого импульсного участка → Нормирование глубины отката относительно ATR → Присвоение оценки качества.
- Шаг 2: Адаптивные пороговые значения глубины подстраиваются под режим волатильности — применяется бонус за сжатие.
- Шаг 3: Для входа требуется подтверждение свипом ликвидности или смещением — стоп-лосс привязан к структуре свингов.
Начало работы
//+------------------------------------------------------------------+ //| Dynmc Pllbck Dpth.mq5 | //| Copyright 2025, MetaQuotes Ltd. | //| https://www.mql5.com/en/users/johnhlomohang/ | //+------------------------------------------------------------------+ #property copyright "Copyright 2025, MetaQuotes Ltd." #property link "https://www.mql5.com/en/users/johnhlomohang/" #property version "1.00" #include <Trade/Trade.mqh> CTrade trade; //+------------------------------------------------------------------+ //| Input Parameters | //+------------------------------------------------------------------+ input int SwingLookback = 5; // Bars for swing detection input double DisplacementFactor = 1.5; // Impulse strength factor input int StructureHoldBars = 3; // Bars structure must hold input int ATR_Period = 14; // ATR calculation period input int ATR_AvgPeriod = 50; // ATR rolling average period input double RiskPercent = 1.0; // Risk per trade (%) input int StopLossPoints = 2000; // Fallback SL in points input double RiskRewardRatio = 2.0; // Risk:Reward ratio input double ATR_SL_Multiplier = 1.5; // ATR multiplier for dynamic SL input double PullbackMinDepth = 0.25; // Minimum pullback depth input double PullbackMaxDepth = 0.65; // Maximum pullback depth (structure valid) input bool RequireCompression = true; // Require ATR compression before entry input bool Visualize = true; // Show visualization //+------------------------------------------------------------------+ //| Enumerations | //+------------------------------------------------------------------+ enum MarketState { ACCUMULATION, EXPANSION, DISTRIBUTION, REVERSAL }; enum VolatilityRegime { LOW_VOL, NORMAL_VOL, HIGH_VOL, EXTREME_VOL }; enum PullbackRegime { PULLBACK_AGGRESSIVE, // 0.00-0.25 : Aggressive continuation / barely pulled back PULLBACK_HEALTHY, // 0.25-0.50 : Ideal trend pullback PULLBACK_DEEP, // 0.50-0.75 : Deep, still within structure PULLBACK_WEAK, // 0.75-1.00 : Structural weakness PULLBACK_INVALID // > 1.00 : Structure failure };
Для начала мы определим базовую конфигурацию и систему классификации, которые будут лежать в основе нашей модели отката с учетом волатильности. Входные параметры позволяют нам настраивать обнаружение свингов, силу смещения, проверку структуры, расчеты ATR, управление рисками, ограничения глубины отката и поведение динамического стоп-лосса. Кроме того, мы вводим три перечисления, которые помогают разделить рыночное поведение на осмысленные категории. MarketState классифицирует общие структурные фазы рынка, такие как накопление или расширение. VolatilityRegime классифицирует текущие условия волатильности с использованием показателей на основе ATR, а PullbackRegime — качество коррекции: от агрессивного продолжения движения до полного разрушения структуры.
//+------------------------------------------------------------------+ //| Structures | //+------------------------------------------------------------------+ struct SwingPoint { datetime time; double price; bool isHigh; bool isValid; bool isUsed; int barIndex; double candleSize; void Reset() { time = 0; price = 0.0; isHigh = true; isValid = false; isUsed = false; barIndex = -1; candleSize = 0.0; } }; struct LiquidityZone { double price; datetime time; bool taken; bool isHigh; string type; void Reset() { price = 0.0; time = 0; taken = false; isHigh = true; type = ""; } }; struct MarketStructure { double lastHigh; double lastLow; datetime lastHighTime; datetime lastLowTime; bool bullish; MarketState state; void Reset() { lastHigh = 0.0; lastLow = 0.0; lastHighTime = 0; lastLowTime = 0; bullish = true; state = ACCUMULATION; } }; //--- Pullback Model Structure struct PullbackModel { //--- Impulse leg definition double impulseStart; // Price at impulse origin double impulseEnd; // Price at impulse peak/trough double impulseSizePoints; // Raw impulse size in points //--- Retracement measurement double retracementPrice; // Current pullback extreme double retracementDepth; // Normalized depth [0.0-1.0+] //--- Volatility context double atrAtImpulse; // ATR value when impulse occurred double atrDuringPullback; // ATR during the pullback phase double normalizedDepth; // retracementDistance / ATR //--- Regime classification bool shallowPullback; // depth < 0.25 bool healthyPullback; // depth 0.20-0.50 bool deepPullback; // depth 0.50-0.75 bool invalidPullback; // depth > 1.0 PullbackRegime regime; //--- Efficiency metrics double avgCounterCandleSize; // Average size of retracement candles double overlapPercent; // % of candles overlapping prior candle bool momentumDecaying; // Counter-trend momentum is fading //--- Compression signal bool compressionDetected; // ATR contracted during pullback //--- Quality score double pullbackScore; // 0-10 quality metric //--- Direction bool isBullish; // Bullish impulse = true //--- Timing datetime startTime; datetime endTime; void Reset() { impulseStart = 0; impulseEnd = 0; impulseSizePoints = 0; retracementPrice = 0; retracementDepth = 0; atrAtImpulse = 0; atrDuringPullback = 0; normalizedDepth = 0; shallowPullback = false; healthyPullback = false; deepPullback = false; invalidPullback = false; regime = PULLBACK_INVALID; avgCounterCandleSize = 0; overlapPercent = 0; momentumDecaying = false; compressionDetected = false; pullbackScore = 0; isBullish = true; startTime = 0; endTime = 0; } }; //+------------------------------------------------------------------+ //| Global Variables | //+------------------------------------------------------------------+ SwingPoint swingCandidates[]; SwingPoint validSwings[]; LiquidityZone liquidityZones[]; MarketStructure marketStruct; PullbackModel pullbacks[]; // Pullback models array int atrHandle; int atrAvgHandle; // Longer-period ATR for average datetime lastBarTime = 0; double avgCandleSize = 0; double currentATR = 0; // live ATR double avgATR = 0; // rolling ATR average VolatilityRegime currentVolRegime = NORMAL_VOL; // current vol regime //--- Trade tracking datetime lastTradeTime = 0; double lastTradePrice = 0; int lastTradeType = 0; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { ArrayResize(swingCandidates, 0); ArrayResize(validSwings, 0); ArrayResize(liquidityZones, 0); ArrayResize(pullbacks, 0); marketStruct.Reset(); atrHandle = iATR(_Symbol, PERIOD_CURRENT, ATR_Period); if(atrHandle == INVALID_HANDLE) { Print("Failed to create ATR handle"); return INIT_FAILED; } //--- Second ATR handle for longer rolling average atrAvgHandle = iATR(_Symbol, PERIOD_CURRENT, ATR_AvgPeriod); if(atrAvgHandle == INVALID_HANDLE) { Print("Failed to create ATR average handle"); return INIT_FAILED; } DetectSwingCandidates(); ValidateSwings(); AnalyzePullbackDepth(); // NEW LAYER 2.5 UpdateLiquidityZones(); UpdateMarketState(); return INIT_SUCCEEDED; }
В этом разделе кода первая группа структур составляет основу для хранения данных торговой системы. SwingPoint хранит информацию об обнаруженных максимумах и минимумах, включая их цену, время, размер свечи и статус проверки. Это позволяет нам отделить непроверенных кандидатов в свинги от свингов, прошедших структурную проверку. LiquidityZone отслеживает важные области, в которых может присутствовать ликвидность, такие как равные максимумы, равные минимумы или нетронутые точки свинга, а также фиксирует, была ли эта ликвидность уже снята. MarketStructure обеспечивает актуальную картину рынка, сохраняя данные о последних значимых максимумах и минимумах, преобладающей направленности движения и текущем состоянии рынка. Каждая структура содержит функцию Reset(), которая обеспечивает возвращение всех полей в известное состояние по умолчанию перед их повторным использованием.
Модель PullbackModel расширяет возможности системы за пределы простого анализа свингов, предлагая комплексную систему оценки качества коррекций. Мы сохраняем данные об импульсном участке, глубине отката, значениях ATR, показателях, нормализованных с учетом волатильности, а также классификации откатов — от здоровых коррекций до разрушения структуры. Дополнительные показатели, такие как перекрытие свечей, контртрендовый импульс и сжатие ATR, помогают нам оценивать качество и характер отката, а не полагаться исключительно на величину изменения цены.
Ниже этих структур мы объявляем глобальные массивы и переменные, в которых хранятся данные о свингах, зоны ликвидности, информация о структуре рынка, модели откатов, результаты расчетов ATR и данные для отслеживания сделок. Наконец, в функции OnInit() мы инициализируем эти контейнеры, создаем индикаторы ATR как для текущей, так и для средней волатильности, а также запускаем первый цикл анализа. Это позволяет определить исходную структуру рынка и контекст отката до того, как советник начнет обрабатывать рыночные данные в реальном времени.
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { if(atrHandle != INVALID_HANDLE) IndicatorRelease(atrHandle); if(atrAvgHandle != INVALID_HANDLE) IndicatorRelease(atrAvgHandle); if(Visualize) { ObjectsDeleteAll(0, "SF_"); ChartRedraw(); } } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { datetime currentBarTime = iTime(_Symbol, PERIOD_CURRENT, 0); if(currentBarTime != lastBarTime) { lastBarTime = currentBarTime; UpdateAvgCandleSize(); UpdateATRValues(); // refresh ATR + regime DetectSwingCandidates(); ValidateSwings(); AnalyzePullbackDepth(); // NEW LAYER 2.5 UpdateLiquidityZones(); UpdateMarketState(); CheckTradeConditions(); if(Visualize) UpdateVisualization(); } } //+------------------------------------------------------------------+ //| Update ATR Values and Volatility Regime | //+------------------------------------------------------------------+ void UpdateATRValues() { double atrBuf[]; double atrAvgBuf[]; ArraySetAsSeries(atrBuf, true); ArraySetAsSeries(atrAvgBuf, true); if(CopyBuffer(atrHandle, 0, 0, 1, atrBuf) > 0) currentATR = atrBuf[0]; if(CopyBuffer(atrAvgHandle, 0, 0, 1, atrAvgBuf) > 0) avgATR = atrAvgBuf[0]; if(avgATR == 0) return; double ratio = currentATR / avgATR; if(ratio < 0.7) currentVolRegime = LOW_VOL; else if(ratio < 1.2) currentVolRegime = NORMAL_VOL; else if(ratio < 1.8) currentVolRegime = HIGH_VOL; else currentVolRegime = EXTREME_VOL; }
В этом разделе осуществляется управление жизненным циклом советника и обеспечивается синхронизация рыночного анализа с только что закрывшимися свечами. В функции OnDeinit() мы освобождаем оба дескриптора индикаторов ATR для высвобождения ресурсов платформы, удаляем все объекты графика, созданные системой визуализации, а затем принудительно обновляем график. В функции OnTick() мы выполняем логику только при появлении нового бара, чтобы избежать лишних вычислений на каждом входящем тике.
В ходе выполнения алгоритм обновляет статистику свечей, обновляет значения ATR, определяет текущий режим волатильности, выявляет и подтверждает свинги, анализирует глубину отката, обновляет зоны ликвидности, оценивает структуру рынка и, наконец, проверяет наличие торговых возможностей. Функция UpdateATRValues() играет ключевую роль в адаптации к волатильности, сравнивая текущее значение ATR со средним значением ATR за более длительный период. Это сравнение позволяет отнести текущие рыночные условия к режиму низкой, нормальной, высокой или экстремальной волатильности, обеспечивая необходимый контекст для динамического анализа откатов и принятия торговых решений с учетом волатильности.
//+------------------------------------------------------------------+ //| NEW LAYER 2.5: Dynamic Pullback Depth Analysis | //+------------------------------------------------------------------+ void AnalyzePullbackDepth() { ArrayResize(pullbacks, 0); int pbCount = 0; int swingCount = ArraySize(validSwings); if(swingCount < 3) return; //--- Pair consecutive swings into impulse legs //--- Bullish impulse: Low -> High //--- Bearish impulse: High -> Low for(int i = 0; i < swingCount - 2; i++) { SwingPoint a = validSwings[i]; SwingPoint b = validSwings[i + 1]; SwingPoint c = validSwings[i + 2]; // The pullback extreme bool bullishImpulse = (!a.isHigh && b.isHigh); bool bearishImpulse = (a.isHigh && !b.isHigh); if(!bullishImpulse && !bearishImpulse) continue; PullbackModel pb; pb.Reset(); pb.isBullish = bullishImpulse; pb.startTime = a.time; pb.endTime = c.time; if(bullishImpulse) { pb.impulseStart = a.price; // swing low pb.impulseEnd = b.price; // swing high pb.retracementPrice = c.price; // next swing low (pullback) } else { pb.impulseStart = a.price; // swing high pb.impulseEnd = b.price; // swing low pb.retracementPrice = c.price; // next swing high (pullback) } pb.impulseSizePoints = MathAbs(pb.impulseEnd - pb.impulseStart) / _Point; //--- Skip negligible impulses if(pb.impulseSizePoints < 10) continue; //--- Calculate normalized retracement depth [0.0 - 1.0+] double impulseRange = MathAbs(pb.impulseEnd - pb.impulseStart); if(bullishImpulse) pb.retracementDepth = (pb.impulseEnd - pb.retracementPrice) / impulseRange; else pb.retracementDepth = (pb.retracementPrice - pb.impulseEnd) / impulseRange; //--- Clamp floor if(pb.retracementDepth < 0) pb.retracementDepth = 0; //--- ATR context at impulse pb.atrAtImpulse = currentATR; //--- Volatility-normalized depth: how many ATRs is the pullback? double retracementDistance = MathAbs(pb.impulseEnd - pb.retracementPrice); pb.normalizedDepth = (currentATR > 0) ? retracementDistance / currentATR : 0; //--- Adaptive depth thresholds based on vol regime double adaptiveMax = PullbackMaxDepth; double adaptiveMin = PullbackMinDepth; switch(currentVolRegime) { case LOW_VOL: //--- Low vol = expect tighter, shallower pullbacks adaptiveMax = PullbackMaxDepth - 0.10; adaptiveMin = PullbackMinDepth; break; case HIGH_VOL: //--- High vol = accept deeper pullbacks before invalidation adaptiveMax = PullbackMaxDepth + 0.10; adaptiveMin = PullbackMinDepth - 0.05; break; case EXTREME_VOL: adaptiveMax = PullbackMaxDepth + 0.15; adaptiveMin = PullbackMinDepth - 0.10; break; default: break; } //--- Classify pullback regime if(pb.retracementDepth < 0.25) { pb.shallowPullback = true; pb.regime = PULLBACK_AGGRESSIVE; } else if(pb.retracementDepth < 0.50) { pb.healthyPullback = true; pb.regime = PULLBACK_HEALTHY; } else if(pb.retracementDepth < 0.75) { pb.deepPullback = true; pb.regime = PULLBACK_DEEP; } else if(pb.retracementDepth < 1.0) { pb.regime = PULLBACK_WEAK; } else { pb.invalidPullback = true; pb.regime = PULLBACK_INVALID; } //--- Pullback efficiency analysis MeasurePullbackEfficiency(pb, i + 1); //--- Compression detection pb.compressionDetected = DetectVolatilityCompression(pb); //--- Score the pullback quality (0–10) pb.pullbackScore = ScorePullback(pb, adaptiveMin, adaptiveMax); ArrayResize(pullbacks, pbCount + 1); pullbacks[pbCount] = pb; pbCount++; } } //+------------------------------------------------------------------+ //| Measure pullback efficiency (candle overlap + momentum decay) | //+------------------------------------------------------------------+ void MeasurePullbackEfficiency(PullbackModel &pb, int impulseEndSwingIdx) { MqlRates rates[]; ArraySetAsSeries(rates, true); int copied = CopyRates(_Symbol, PERIOD_CURRENT, 0, 80, rates); if(copied < 5) return; //--- Find bar index of impulse end int startBar = FindBarIndexByTime(validSwings[impulseEndSwingIdx].time); if(startBar < 0) return; double totalCounterSize = 0; int overlapCount = 0; int candleCount = 0; double lastCounterSize = 0; //--- Scan up to 15 bars of pullback int scanBars = MathMin(startBar, 15); for(int i = 1; i <= scanBars; i++) { if(startBar - i < 0 || startBar - i >= copied) break; double candleBody = MathAbs(rates[startBar - i].close - rates[startBar - i].open); totalCounterSize += candleBody; //--- Check overlap with previous candle (sign of weak counter-trend) if(i > 1) { double prevHigh = rates[startBar - i + 1].high; double prevLow = rates[startBar - i + 1].low; double curHigh = rates[startBar - i].high; double curLow = rates[startBar - i].low; if(curHigh < prevHigh && curLow > prevLow) overlapCount++; } //--- Momentum decay: compare early vs late pullback candle size if(i == 1) lastCounterSize = candleBody; candleCount++; } pb.avgCounterCandleSize = (candleCount > 0) ? totalCounterSize / candleCount : 0; pb.overlapPercent = (candleCount > 1) ? (double)overlapCount / (candleCount - 1) * 100.0 : 0; //--- Momentum is decaying if average counter candle is shrinking pb.momentumDecaying = (pb.avgCounterCandleSize > 0 && lastCounterSize < pb.avgCounterCandleSize * 0.7); }
Функция AnalyzePullbackDepth() реализует механизм динамического анализа откатов, преобразуя подтвержденные свинги в модели импульса и коррекции. Мы выделяем бычьи импульсы — от свинг-минимума до свинг-максимума, а медвежьи импульсы — от свинг-максимума до свинг-минимума. Следующий свинг затем рассматривается как экстремум отката. Используя эти точки, мы рассчитываем величину импульса, глубину коррекции и расстояние отката, нормированное по ATR. Кроме того, эта функция адаптирует допустимые пороги отката к текущему режиму волатильности, допуская более глубокие откаты при высокой волатильности и менее глубокие — на спокойном рынке. Наконец, каждый откат относится к одному из режимов — от агрессивного продолжения тренда до полного разрушения структуры.
Функция MeasurePullbackEfficiency() оценивает качество коррекции, а не только её глубину. Мы анализируем свечи отката, чтобы рассчитать средний размер контртрендовых свечей, степень перекрытия свечей и затухание импульса. Значительное перекрытие свечей часто свидетельствует о слабой коррекции, тогда как уменьшение размеров свечей указывает на ослабление контртрендового импульса. Затем эти показатели объединяются с результатами анализа сжатия волатильности и оценкой отката. В совокупности они помогают нам определить, соответствует ли откат нормальному продолжению тренда или указывает на слабость структуры.
//+------------------------------------------------------------------+ //| Detect volatility compression during pullback | //+------------------------------------------------------------------+ bool DetectVolatilityCompression(PullbackModel &pb) { if(avgATR == 0) return false; //--- ATR has contracted below 80% of average during pullback return (currentATR < avgATR * 0.8); } //+------------------------------------------------------------------+ //| Score pullback quality (0–10) | //+------------------------------------------------------------------+ double ScorePullback(PullbackModel &pb, double adaptiveMin, double adaptiveMax) { double score = 0; //--- Depth scoring if(pb.retracementDepth >= adaptiveMin && pb.retracementDepth <= adaptiveMax) score += 4.0; // ideal zone else if(pb.retracementDepth < adaptiveMin) score += 2.0; // too shallow, possible continuation but lower conviction else if(pb.retracementDepth > adaptiveMax && pb.retracementDepth < 1.0) score += 1.0; // deep but not invalid //--- > 1.0 = 0 points //--- ATR-normalized depth bonus //--- 1-2 ATRs of pullback is ideal if(pb.normalizedDepth >= 0.8 && pb.normalizedDepth <= 2.0) score += 2.0; else if(pb.normalizedDepth > 2.0) score -= 1.0; // over-extended //--- Efficiency bonus if(pb.overlapPercent > 50.0) score += 1.5; // choppy/overlapping pullback = weak counter-trend if(pb.momentumDecaying) score += 1.0; // fading counter-trend momentum //--- Compression bonus if(pb.compressionDetected) score += 1.5; // volatility coil before expansion //--- Penalty for structural weakness if(pb.regime == PULLBACK_WEAK || pb.regime == PULLBACK_INVALID) score -= 3.0; return MathMax(0.0, MathMin(10.0, score)); } //+------------------------------------------------------------------+ //| Get best (most recent, highest scoring) pullback for direction | //+------------------------------------------------------------------+ bool GetBestPullback(bool bullish, PullbackModel &outPb) { int bestIdx = -1; double bestScore = -1; for(int i = 0; i < ArraySize(pullbacks); i++) { if(pullbacks[i].isBullish != bullish) continue; if(pullbacks[i].invalidPullback) continue; if(pullbacks[i].pullbackScore > bestScore) { bestScore = pullbacks[i].pullbackScore; bestIdx = i; } } if(bestIdx < 0) return false; outPb = pullbacks[bestIdx]; return true; }
В этом разделе мы оцениваем и ранжируем откаты, чтобы сосредоточиться на наиболее качественных торговых возможностях. Функция DetectVolatilityCompression() проверяет, сократился ли текущий показатель ATR до уровня ниже 80 % от его долгосрочного среднего значения, что может свидетельствовать о периоде снижения волатильности перед возможным расширением. Затем функция ScorePullback() присваивает оценку в диапазоне от 0 до 10, учитывая глубину коррекции, расстояние, нормированное по ATR, эффективность отката, затухание импульса, сжатие волатильности и устойчивость структуры.
Откаты, происходящие в пределах идеальной зоны коррекции, получают наивысшие оценки, тогда как за слабые или недопустимые структуры оценка снижается. Наконец, функция GetBestPullback() просматривает все проанализированные откаты и возвращает допустимый сетап с наивысшей оценкой для заданного направления. Это дает советнику простой способ отдавать приоритет наиболее благоприятному бычьему или медвежьему откату, доступному в каждый конкретный момент.
//+------------------------------------------------------------------+ //| LAYER 1: Raw Swing Detection | //+------------------------------------------------------------------+ void DetectSwingCandidates() { MqlRates rates[]; ArraySetAsSeries(rates, true); int copied = CopyRates(_Symbol, PERIOD_CURRENT, 0, 100, rates); if(copied < SwingLookback * 2 + 1) return; SwingPoint tempCandidates[]; ArrayResize(tempCandidates, 0); int newCount = 0; for(int i = SwingLookback; i < copied - SwingLookback; i++) { bool isSwingHigh = true; bool isSwingLow = true; for(int j = 1; j <= SwingLookback; j++) { if(rates[i].high <= rates[i-j].high || rates[i].high <= rates[i+j].high) isSwingHigh = false; if(rates[i].low >= rates[i-j].low || rates[i].low >= rates[i+j].low) isSwingLow = false; } if(isSwingHigh) { SwingPoint sp; sp.Reset(); sp.time = rates[i].time; sp.price = rates[i].high; sp.isHigh = true; sp.barIndex = i; sp.candleSize = (rates[i].high - rates[i].low) / _Point; ArrayResize(tempCandidates, newCount + 1); tempCandidates[newCount++] = sp; } if(isSwingLow) { SwingPoint sp; sp.Reset(); sp.time = rates[i].time; sp.price = rates[i].low; sp.isHigh = false; sp.barIndex = i; sp.candleSize = (rates[i].high - rates[i].low) / _Point; ArrayResize(tempCandidates, newCount + 1); tempCandidates[newCount++] = sp; } } ArrayResize(swingCandidates, newCount); for(int i = 0; i < newCount; i++) swingCandidates[i] = tempCandidates[i]; } //+------------------------------------------------------------------+ //| LAYER 2: Structural Validation Engine | //+------------------------------------------------------------------+ void ValidateSwings() { MqlRates rates[]; ArraySetAsSeries(rates, true); CopyRates(_Symbol, PERIOD_CURRENT, 0, 50, rates); if(ArraySize(rates) < StructureHoldBars + 1) return; for(int i = 0; i < ArraySize(swingCandidates); i++) swingCandidates[i].isValid = false; ArrayResize(validSwings, 0); int validCount = 0; for(int i = 0; i < ArraySize(swingCandidates); i++) { bool isValid = false; if(CheckBreakOfStructure(swingCandidates[i], rates)) isValid = true; if(CheckDisplacement(swingCandidates[i], rates)) isValid = true; if(CheckLiquiditySweep(swingCandidates[i], rates)) isValid = true; if(CheckTimeRespect(swingCandidates[i], rates)) isValid = true; if(isValid) { swingCandidates[i].isValid = true; ArrayResize(validSwings, validCount + 1); validSwings[validCount++] = swingCandidates[i]; } } } //+------------------------------------------------------------------+ //| Validation A: Break of Structure | //+------------------------------------------------------------------+ bool CheckBreakOfStructure(SwingPoint &sp, MqlRates &rates[]) { double extreme = 0; if(sp.isHigh) { for(int i = 0; i < ArraySize(validSwings); i++) if(validSwings[i].isHigh && validSwings[i].time < sp.time) if(extreme == 0 || validSwings[i].price > extreme) extreme = validSwings[i].price; return (extreme > 0 && sp.price > extreme); } else { for(int i = 0; i < ArraySize(validSwings); i++) if(!validSwings[i].isHigh && validSwings[i].time < sp.time) if(extreme == 0 || validSwings[i].price < extreme) extreme = validSwings[i].price; return (extreme > 0 && sp.price < extreme); } } //+------------------------------------------------------------------+ //| Validation B: Displacement | //+------------------------------------------------------------------+ bool CheckDisplacement(SwingPoint &sp, MqlRates &rates[]) { if(avgCandleSize == 0) return false; return (sp.candleSize > avgCandleSize * DisplacementFactor); } //+------------------------------------------------------------------+ //| Validation C: Liquidity Sweep | //+------------------------------------------------------------------+ bool CheckLiquiditySweep(SwingPoint &sp, MqlRates &rates[]) { int barIndex = FindBarIndexByTime(sp.time); if(barIndex < 0 || barIndex >= ArraySize(rates)) return false; if(sp.isHigh) { for(int i = 0; i < ArraySize(validSwings); i++) if(validSwings[i].isHigh && validSwings[i].time < sp.time) if(rates[barIndex].high > validSwings[i].price && rates[barIndex].close < validSwings[i].price) return true; } else { for(int i = 0; i < ArraySize(validSwings); i++) if(!validSwings[i].isHigh && validSwings[i].time < sp.time) if(rates[barIndex].low < validSwings[i].price && rates[barIndex].close > validSwings[i].price) return true; } return false; } //+------------------------------------------------------------------+ //| Validation D: Time-Based Respect | //+------------------------------------------------------------------+ bool CheckTimeRespect(SwingPoint &sp, MqlRates &rates[]) { int barIndex = FindBarIndexByTime(sp.time); //--- If not enough bars ahead, we cannot confirm, so accept the swing if(barIndex < 0 || barIndex + StructureHoldBars >= ArraySize(rates)) return true; if(sp.isHigh) { for(int i = 1; i <= StructureHoldBars; i++) if(rates[barIndex + i].high > sp.price) return false; return true; } else { for(int i = 1; i <= StructureHoldBars; i++) if(rates[barIndex + i].low < sp.price) return false; return true; } }
Первый этап работы системы направлен на выявление предварительных кандидатов в свинги на основе исторических ценовых данных. Мы анализируем каждую свечу и сравниваем её максимум и минимум с максимумами и минимумами соседних свечей в пределах заданного периода анализа. Свеча отмечается как свинг-максимум, если её максимум выше максимумов соседних свечей, а для свинг-минимума её минимум должен быть ниже минимумов соседних свечей. Для каждого обнаруженного свинга мы сохраняем важные данные, такие как цена, время, размер свечи и положение на графике. Эти кандидаты представляют собой потенциальные точки структурного разворота, но пока не подтверждены как значимые элементы рыночной структуры.
На втором этапе эти кандидаты в свинги проверяются с помощью нескольких структурных проверок. Мы оцениваем, приводит ли свинг к пробою структуры, формирует ли сильное смещение, совершает ли свип ликвидности или удерживается ли его уровень в течение заданного количества баров. Проверка пробоя структуры подтверждает, что свинг превышает предыдущие структурные экстремумы, а проверка смещения гарантирует, что амплитуда движения превышает уровень обычной рыночной активности. Проверка свипа ликвидности выявляет ложные пробои, после которых цена быстро возвращается внутрь структуры, а проверка удержания во времени подтверждает, что уровень свинга продолжает удерживаться после формирования. Любой свинг, прошедший хотя бы одну из этих проверок, добавляется в массив validSwings и становится элементом рыночной структуры более высокого уровня, используемым при анализе откатов.
//+------------------------------------------------------------------+ //| LAYER 3: Liquidity Interaction Layer | //+------------------------------------------------------------------+ void UpdateLiquidityZones() { MqlRates rates[]; ArraySetAsSeries(rates, true); CopyRates(_Symbol, PERIOD_CURRENT, 0, 100, rates); if(ArraySize(rates) < 50) return; ArrayResize(liquidityZones, 0); int zoneCount = 0; for(int i = 0; i < ArraySize(validSwings) - 1; i++) for(int j = i + 1; j < ArraySize(validSwings); j++) { double priceDiff = MathAbs(validSwings[i].price - validSwings[j].price); if(priceDiff < _Point * 10 && validSwings[i].isHigh == validSwings[j].isHigh) { LiquidityZone zone; zone.Reset(); zone.price = validSwings[i].price; zone.time = validSwings[i].time; zone.isHigh = validSwings[i].isHigh; zone.type = "equal"; zone.taken = CheckIfLiquidityTaken(zone, rates); ArrayResize(liquidityZones, zoneCount + 1); liquidityZones[zoneCount++] = zone; break; } } for(int i = 0; i < ArraySize(validSwings); i++) if(!validSwings[i].isUsed) { LiquidityZone zone; zone.Reset(); zone.price = validSwings[i].price; zone.time = validSwings[i].time; zone.isHigh = validSwings[i].isHigh; zone.type = "untouched"; zone.taken = CheckIfLiquidityTaken(zone, rates); ArrayResize(liquidityZones, zoneCount + 1); liquidityZones[zoneCount++] = zone; } } //+------------------------------------------------------------------+ //| Check if liquidity zone has been taken | //+------------------------------------------------------------------+ bool CheckIfLiquidityTaken(LiquidityZone &zone, MqlRates &rates[]) { for(int i = 0; i < ArraySize(rates); i++) { if(zone.isHigh && rates[i].high > zone.price) return true; if(!zone.isHigh && rates[i].low < zone.price) return true; } return false; } //+------------------------------------------------------------------+ //| LAYER 4: Structural State Machine | //+------------------------------------------------------------------+ void UpdateMarketState() { if(ArraySize(validSwings) < 2) return; SwingPoint lastSwing = validSwings[ArraySize(validSwings) - 1]; if(lastSwing.isHigh && lastSwing.price > marketStruct.lastHigh) { marketStruct.lastHigh = lastSwing.price; marketStruct.lastHighTime = lastSwing.time; } if(!lastSwing.isHigh && (marketStruct.lastLow == 0 || lastSwing.price < marketStruct.lastLow)) { marketStruct.lastLow = lastSwing.price; marketStruct.lastLowTime = lastSwing.time; } bool higherHigh = false; bool higherLow = false; for(int i = 0; i < ArraySize(validSwings); i++) { if(validSwings[i].isHigh && validSwings[i].price > marketStruct.lastHigh) higherHigh = true; if(!validSwings[i].isHigh && validSwings[i].price > marketStruct.lastLow) higherLow = true; } marketStruct.bullish = (higherHigh && higherLow); bool liquiditySweepFailed = CheckLiquiditySweepFailure(); switch(marketStruct.state) { case ACCUMULATION: if(marketStruct.bullish) marketStruct.state = EXPANSION; else if(ArraySize(validSwings) > 5) marketStruct.state = DISTRIBUTION; break; case EXPANSION: if(!marketStruct.bullish) marketStruct.state = DISTRIBUTION; else if(liquiditySweepFailed) marketStruct.state = REVERSAL; break; case DISTRIBUTION: if(marketStruct.bullish) marketStruct.state = ACCUMULATION; else if(liquiditySweepFailed) marketStruct.state = REVERSAL; break; case REVERSAL: if(marketStruct.bullish) marketStruct.state = ACCUMULATION; else if(!marketStruct.bullish) marketStruct.state = ACCUMULATION; break; } } //+------------------------------------------------------------------+ //| Check liquidity sweep failure | //+------------------------------------------------------------------+ bool CheckLiquiditySweepFailure() { MqlRates rates[]; ArraySetAsSeries(rates, true); CopyRates(_Symbol, PERIOD_CURRENT, 0, 10, rates); if(ArraySize(rates) < 5) return false; for(int i = 0; i < ArraySize(liquidityZones); i++) { if(liquidityZones[i].isHigh && !liquidityZones[i].taken) if(rates[0].high > liquidityZones[i].price && rates[0].close < liquidityZones[i].price) return true; if(!liquidityZones[i].isHigh && !liquidityZones[i].taken) if(rates[0].low < liquidityZones[i].price && rates[0].close > liquidityZones[i].price) return true; } return false; }
Третий уровень посвящён анализу ликвидности, в рамках которого подтверждённые точки свинга преобразуются в зоны ликвидности, на основе которых можно принимать торговые решения. Сначала мы ищем равные максимумы и равные минимумы, поскольку эти области часто привлекают стоп-ордера и ликвидность в неисполненных заявках. Когда два свинга формируются практически на одном и том же ценовом уровне, создается и отслеживается зона ликвидности. Кроме того, мы добавляем неиспользованные свинг-максимумы и свинг-минимумы в качестве нетронутых зон ликвидности, что позволяет системе отслеживать структурные уровни, к которым цена пока не возвращалась. Каждая зона проверяется по последним рыночным данным, чтобы определить, была ли ликвидность уже снята или по-прежнему доступна в качестве потенциальной будущей цели.
Четвертый уровень действует как структурный автомат состояний, который интерпретирует общее состояние рынка. Мы постоянно обновляем данные о наиболее значимых свинг-максимумах и свинг-минимумах, а затем оцениваем, формирует ли цена более высокие максимумы и более высокие минимумы, чтобы определить силу направленного движения. На основе этой информации рынок переходит между состояниями накопления, расширения, распределения и разворота. Система также отслеживает неудачные свипы ликвидности, при которых цена ненадолго выходит за пределы зоны ликвидности, но свеча закрывается внутри диапазона. Такое поведение часто указывает на истощение движения или неприятие рынком цен за пределами зоны, что позволяет автомату состояний выявлять потенциальные развороты и корректировать своё представление о структуре рынка до принятия торговых решений.
//+------------------------------------------------------------------+ //| LAYER 5: Execution Engine (Pullback-Upgraded) | //+------------------------------------------------------------------+ void CheckTradeConditions() { if(PositionSelect(_Symbol)) { ManageOpenPosition(); return; } if(TimeCurrent() - lastTradeTime < PeriodSeconds(PERIOD_CURRENT) * 3) return; MqlRates current[]; ArraySetAsSeries(current, true); CopyRates(_Symbol, PERIOD_CURRENT, 0, 3, current); if(ArraySize(current) < 2) return; if(ArraySize(validSwings) == 0) return; SwingPoint lastValidSwing = validSwings[ArraySize(validSwings) - 1]; if(CheckBuyConditions(lastValidSwing, current)) { ExecuteTrade(ORDER_TYPE_BUY); lastTradeTime = TimeCurrent(); lastTradeType = 1; } else if(CheckSellConditions(lastValidSwing, current)) { ExecuteTrade(ORDER_TYPE_SELL); lastTradeTime = TimeCurrent(); lastTradeType = -1; } } //+------------------------------------------------------------------+ //| NEW: Check Buy Conditions (Pullback-Aware) | //+------------------------------------------------------------------+ bool CheckBuyConditions(SwingPoint &lastSwing, MqlRates &rates[]) { if(lastSwing.isHigh || !lastSwing.isValid) return false; //--- Market state filter bool goodState = (marketStruct.state == EXPANSION || marketStruct.state == ACCUMULATION); if(!goodState) return false; //--- Layer 2.5 gate: require a high-quality bullish pullback PullbackModel bestPb; bool hasPullback = GetBestPullback(true, bestPb); if(!hasPullback) return false; if(bestPb.invalidPullback) return false; if(bestPb.pullbackScore < 4.0) return false; // Minimum quality threshold //--- Require pullback within healthy or deep zone (not way too shallow or broken) if(bestPb.retracementDepth < PullbackMinDepth) return false; //--- Adaptive max depth double adaptiveMax = PullbackMaxDepth; if(currentVolRegime == HIGH_VOL) adaptiveMax += 0.10; if(currentVolRegime == EXTREME_VOL) adaptiveMax += 0.15; if(bestPb.retracementDepth > adaptiveMax) return false; //--- Compression gate (optional) if(RequireCompression && !bestPb.compressionDetected) return false; //--- Liquidity sweep below (confirming) bool liquiditySweep = false; for(int i = 0; i < ArraySize(liquidityZones); i++) if(!liquidityZones[i].isHigh && !liquidityZones[i].taken) if(rates[0].low < liquidityZones[i].price && rates[0].close > liquidityZones[i].price) { liquiditySweep = true; break; } //--- Bullish displacement return candle bool bullishDisplacement = false; if(ArraySize(rates) >= 2) { double candleSize = (rates[0].close - rates[0].open) / _Point; if(candleSize > avgCandleSize * DisplacementFactor) bullishDisplacement = true; } return (liquiditySweep || bullishDisplacement); } //+------------------------------------------------------------------+ //| NEW: Check Sell Conditions (Pullback-Aware) | //+------------------------------------------------------------------+ bool CheckSellConditions(SwingPoint &lastSwing, MqlRates &rates[]) { if(!lastSwing.isHigh || !lastSwing.isValid) return false; bool goodState = (marketStruct.state == DISTRIBUTION || marketStruct.state == REVERSAL); if(!goodState) return false; PullbackModel bestPb; bool hasPullback = GetBestPullback(false, bestPb); if(!hasPullback) return false; if(bestPb.invalidPullback) return false; if(bestPb.pullbackScore < 4.0) return false; if(bestPb.retracementDepth < PullbackMinDepth) return false; double adaptiveMax = PullbackMaxDepth; if(currentVolRegime == HIGH_VOL) adaptiveMax += 0.10; if(currentVolRegime == EXTREME_VOL) adaptiveMax += 0.15; if(bestPb.retracementDepth > adaptiveMax) return false; if(RequireCompression && !bestPb.compressionDetected) return false; bool liquiditySweep = false; for(int i = 0; i < ArraySize(liquidityZones); i++) if(liquidityZones[i].isHigh && !liquidityZones[i].taken) if(rates[0].high > liquidityZones[i].price && rates[0].close < liquidityZones[i].price) { liquiditySweep = true; break; } bool bearishDisplacement = false; if(ArraySize(rates) >= 2) { double candleSize = (rates[0].open - rates[0].close) / _Point; if(candleSize > avgCandleSize * DisplacementFactor) bearishDisplacement = true; } return (liquiditySweep || bearishDisplacement); } //+------------------------------------------------------------------+ //| Execute Trade | //+------------------------------------------------------------------+ void ExecuteTrade(ENUM_ORDER_TYPE tradeType) { double price = (tradeType == ORDER_TYPE_BUY) ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID); double sl = CalculateStopLoss(tradeType); if(sl == 0) return; double riskPoints = MathAbs(price - sl) / _Point; double tpPoints = riskPoints * RiskRewardRatio; double tp = (tradeType == ORDER_TYPE_BUY) ? price + tpPoints * _Point : price - tpPoints * _Point; price = NormalizeDouble(price, _Digits); sl = NormalizeDouble(sl, _Digits); tp = NormalizeDouble(tp, _Digits); double volume = CalculateLotSize(price, sl); string comment = StringFormat("SF_%s_PB", (tradeType == ORDER_TYPE_BUY) ? "BUY" : "SELL"); bool success = trade.PositionOpen(_Symbol, tradeType, volume, price, sl, tp, comment); if(success) { lastTradePrice = price; string volLabel = ""; switch(currentVolRegime) { case LOW_VOL: volLabel = "LOW"; break; case NORMAL_VOL: volLabel = "NORMAL"; break; case HIGH_VOL: volLabel = "HIGH"; break; case EXTREME_VOL: volLabel = "EXTREME"; break; } Print(StringFormat("Trade: %s | Price: %.5f | SL: %.5f | TP: %.5f | Lots: %.2f | VolRegime: %s", (tradeType == ORDER_TYPE_BUY) ? "BUY" : "SELL", price, sl, tp, volume, volLabel)); for(int i = 0; i < ArraySize(validSwings); i++) if(MathAbs(validSwings[i].price - price) < _Point * 10) validSwings[i].isUsed = true; } else Print("Trade failed: ", trade.ResultRetcodeDescription()); } //+------------------------------------------------------------------+ //| NEW: ATR-Dynamic Stop Loss | //+------------------------------------------------------------------+ double CalculateStopLoss(ENUM_ORDER_TYPE tradeType) { double atr = (currentATR > 0) ? currentATR : GetCurrentATR(); if(tradeType == ORDER_TYPE_BUY) { double currentPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID); double nearestLow = 0; for(int i = 0; i < ArraySize(validSwings); i++) if(!validSwings[i].isHigh && validSwings[i].price < currentPrice) if(nearestLow == 0 || validSwings[i].price > nearestLow) nearestLow = validSwings[i].price; if(nearestLow > 0) { //--- Swing-based SL with ATR buffer double buffer = atr * ATR_SL_Multiplier * 0.3; return NormalizeDouble(nearestLow - buffer, _Digits); } } else { double currentPrice = SymbolInfoDouble(_Symbol, SYMBOL_ASK); double nearestHigh = 0; for(int i = 0; i < ArraySize(validSwings); i++) if(validSwings[i].isHigh && validSwings[i].price > currentPrice) if(nearestHigh == 0 || validSwings[i].price < nearestHigh) nearestHigh = validSwings[i].price; if(nearestHigh > 0) { double buffer = atr * ATR_SL_Multiplier * 0.3; return NormalizeDouble(nearestHigh + buffer, _Digits); } } //--- Fallback: pure ATR-based SL double currentPrice = (tradeType == ORDER_TYPE_BUY) ? SymbolInfoDouble(_Symbol, SYMBOL_BID) : SymbolInfoDouble(_Symbol, SYMBOL_ASK); if(atr > 0) return (tradeType == ORDER_TYPE_BUY) ? currentPrice - atr * ATR_SL_Multiplier : currentPrice + atr * ATR_SL_Multiplier; double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT); return (tradeType == ORDER_TYPE_BUY) ? currentPrice - StopLossPoints * point : currentPrice + StopLossPoints * point; }
Пятый уровень — это механизм исполнения, в котором мы преобразуем результаты анализа структуры и откатов в конкретные торговые решения. Сначала мы проверяем, нет ли уже открытой позиции, и ограничиваем частоту входов, чтобы избежать избыточного числа сделок. Затем мы получаем последние рыночные данные и, прежде чем продолжить, проверяем наличие корректной структуры свингов. Для открытия позиций нам необходимы как подтвержденная направленность рынка, так и высококачественная модель отката, отвечающая строгим условиям, таким как минимальная оценка качества, допустимая глубина коррекции и ограничения с поправкой на волатильность. В качестве триггеров подтверждения мы также используем свипы ликвидности и свечи смещения. Когда все эти условия выполняются одновременно, мы открываем сделку на покупку или продажу с полноценным управлением рисками.
Исполнение сделок тесно связано с волатильностью и структурой рынка, благодаря чему система является адаптивной, а не статичной. Стоп-лосс рассчитывается динамически с учетом как структуры свингов, так и ATR, поэтому расстояние до него увеличивается или уменьшается в зависимости от рыночных условий. Чтобы избежать преждевременного срабатывания стоп-лосса, добавляется буфер, а резервный метод на основе ATR обеспечивает надежность, когда структура рынка неясна. Уровень тейк-профита определяется на основе фиксированного соотношения риска к прибыли, что обеспечивает единообразие работы системы в различных режимах волатильности. При каждой сделке система также записывает в журнал текущий режим волатильности и помечает использованные точки свинга, чтобы исключить их повторное использование и избежать дублирования решений в последующих циклах.
//+------------------------------------------------------------------+ //| Manage Open Position (ATR-Dynamic Trailing) | //+------------------------------------------------------------------+ void ManageOpenPosition() { if(!PositionSelect(_Symbol)) return; double currentPrice = PositionGetDouble(POSITION_PRICE_CURRENT); double openPrice = PositionGetDouble(POSITION_PRICE_OPEN); int type = (int)PositionGetInteger(POSITION_TYPE); double atr = (currentATR > 0) ? currentATR : GetCurrentATR(); if(type == POSITION_TYPE_BUY) { for(int i = 0; i < ArraySize(validSwings); i++) if(!validSwings[i].isHigh && validSwings[i].price > openPrice && validSwings[i].price < currentPrice) { double newSL = validSwings[i].price - atr * ATR_SL_Multiplier * 0.3; if(newSL > PositionGetDouble(POSITION_SL)) { trade.PositionModify(_Symbol, NormalizeDouble(newSL, _Digits), PositionGetDouble(POSITION_TP)); Print(StringFormat("Trail SL -> %.5f (ATR buf: %.5f)", newSL, atr)); } } } else if(type == POSITION_TYPE_SELL) { for(int i = 0; i < ArraySize(validSwings); i++) if(validSwings[i].isHigh && validSwings[i].price < openPrice && validSwings[i].price > currentPrice) { double newSL = validSwings[i].price + atr * ATR_SL_Multiplier * 0.3; if(newSL < PositionGetDouble(POSITION_SL) || PositionGetDouble(POSITION_SL) == 0) { trade.PositionModify(_Symbol, NormalizeDouble(newSL, _Digits), PositionGetDouble(POSITION_TP)); Print(StringFormat("Trail SL -> %.5f (ATR buf: %.5f)", newSL, atr)); } } } } //+------------------------------------------------------------------+ //| Calculate Lot Size | //+------------------------------------------------------------------+ double CalculateLotSize(double entry, double sl) { double riskAmount = AccountInfoDouble(ACCOUNT_BALANCE) * (RiskPercent / 100.0); double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); double pointValue = SymbolInfoDouble(_Symbol, SYMBOL_POINT); if(tickValue == 0 || pointValue == 0) return 0.01; double riskPoints = MathAbs(entry - sl) / pointValue; double lots = riskAmount / (riskPoints * tickValue * pointValue / tickSize); double lotStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); lots = MathFloor(lots / lotStep) * lotStep; return MathMax(SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN), MathMin(lots, SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX))); } //+------------------------------------------------------------------+ //| Helper: Update Average Candle Size | //+------------------------------------------------------------------+ void UpdateAvgCandleSize() { MqlRates rates[]; ArraySetAsSeries(rates, true); int copied = CopyRates(_Symbol, PERIOD_CURRENT, 0, 50, rates); if(copied < 20) return; double totalSize = 0; for(int i = 0; i < copied; i++) totalSize += (rates[i].high - rates[i].low) / _Point; avgCandleSize = totalSize / copied; } //+------------------------------------------------------------------+ //| Helper: Find Bar Index by Time | //+------------------------------------------------------------------+ int FindBarIndexByTime(datetime targetTime) { MqlRates rates[]; ArraySetAsSeries(rates, true); int copied = CopyRates(_Symbol, PERIOD_CURRENT, 0, 100, rates); for(int i = 0; i < copied; i++) if(rates[i].time == targetTime) return i; return -1; } //+------------------------------------------------------------------+ //| Helper: Get Current ATR | //+------------------------------------------------------------------+ double GetCurrentATR() { double atr[]; ArraySetAsSeries(atr, true); if(CopyBuffer(atrHandle, 0, 0, 1, atr) <= 0) return 0; return atr[0]; } //+------------------------------------------------------------------+ //| Helper: Pullback regime to readable string | //+------------------------------------------------------------------+ string PullbackRegimeToString(PullbackRegime r) { switch(r) { case PULLBACK_AGGRESSIVE: return "AGGRESSIVE"; case PULLBACK_HEALTHY: return "HEALTHY"; case PULLBACK_DEEP: return "DEEP"; case PULLBACK_WEAK: return "WEAK"; case PULLBACK_INVALID: return "INVALID"; } return "UNKNOWN"; }
Уровень управления позициями отвечает за динамическую защиту и корректировку открытых позиций с учетом структуры рынка и текущей волатильности. Сначала мы проверяем наличие позиции, а затем извлекаем ключевую информацию о сделке, такую как цена входа, текущая цена и направление сделки. Для позиций на покупку мы ищем корректные свинг-минимумы между ценой входа и текущей ценой и подтягиваем стоп-лосс чуть ниже последнего значимого свинг-минимума, добавляя буфер на основе ATR, чтобы избежать срабатывания слишком близко установленного стоп-лосса. Для позиций на продажу мы зеркально применяем эту логику, используя свинг-максимумы выше цены входа. Это обеспечивает согласованность перемещений стоп-лосса со структурой рынка и одновременно позволяет адаптировать их к текущей волатильности за счет масштабирования по ATR.
Остальные вспомогательные функции обеспечивают работу системы за счет поддержания точности рыночных показателей и корректной обработки данных. Мы рассчитываем размер позиции с учетом допустимого риска по счету, стоимости тика и расстояния до стоп-лосса, чтобы риск в каждой сделке составлял фиксированный процент от средств на счете. Мы также рассчитываем средний размер свечи, чтобы оценить обычный масштаб движения рынка, и используем этот показатель в логике смещения в других частях системы. Дополнительные утилиты позволяют нам определять положение свечей по времени, получать значения ATR в режиме реального времени, а также преобразовывать режимы откатов в понятные метки для отладки и визуализации. В совокупности эти компоненты обеспечивают адаптивность и согласованность работы системы, а также учет рыночной структуры на всех этапах выполнения.
//+------------------------------------------------------------------+ //| Helper: Vol regime to string | //+------------------------------------------------------------------+ string VolRegimeToString(VolatilityRegime r) { switch(r) { case LOW_VOL: return "LOW"; case NORMAL_VOL: return "NORMAL"; case HIGH_VOL: return "HIGH"; case EXTREME_VOL: return "EXTREME"; } return "UNKNOWN"; } //+------------------------------------------------------------------+ //| Visualization | //+------------------------------------------------------------------+ void UpdateVisualization() { ObjectsDeleteAll(0, "SF_"); //--- Valid swings for(int i = 0; i < ArraySize(validSwings); i++) { color clr = validSwings[i].isHigh ? clrGreen : clrRed; string prefix = validSwings[i].isHigh ? "ValidHigh" : "ValidLow"; string name = StringFormat("SF_%s_%d", prefix, validSwings[i].time); ObjectCreate(0, name, OBJ_ARROW, 0, validSwings[i].time, validSwings[i].price); ObjectSetInteger(0, name, OBJPROP_ARROWCODE, validSwings[i].isHigh ? 218 : 217); ObjectSetInteger(0, name, OBJPROP_COLOR, clr); ObjectSetInteger(0, name, OBJPROP_FONTSIZE, 10); } //--- Invalid swing candidates (gray) for(int i = 0; i < ArraySize(swingCandidates); i++) if(!swingCandidates[i].isValid) { string name = StringFormat("SF_Invalid_%d", swingCandidates[i].time); ObjectCreate(0, name, OBJ_ARROW, 0, swingCandidates[i].time, swingCandidates[i].price); ObjectSetInteger(0, name, OBJPROP_ARROWCODE, swingCandidates[i].isHigh ? 218 : 217); ObjectSetInteger(0, name, OBJPROP_COLOR, clrGray); ObjectSetInteger(0, name, OBJPROP_FONTSIZE, 10); } //--- NEW: Draw pullback retracement zones (30%, 50%, 70% levels) for(int i = 0; i < ArraySize(pullbacks); i++) { PullbackModel pb = pullbacks[i]; if(pb.impulseSizePoints < 10) continue; double impulseRange = MathAbs(pb.impulseEnd - pb.impulseStart); double level30, level50, level70; if(pb.isBullish) { level30 = pb.impulseEnd - impulseRange * 0.30; level50 = pb.impulseEnd - impulseRange * 0.50; level70 = pb.impulseEnd - impulseRange * 0.70; } else { level30 = pb.impulseEnd + impulseRange * 0.30; level50 = pb.impulseEnd + impulseRange * 0.50; level70 = pb.impulseEnd + impulseRange * 0.70; } datetime tStart = pb.startTime; datetime tEnd = pb.endTime; if(tEnd <= tStart) tEnd = tStart + PeriodSeconds(PERIOD_CURRENT) * 20; //--- 30%-50% zone: green (healthy) string nameGreen = StringFormat("SF_PB_Green_%d", i); ObjectCreate(0, nameGreen, OBJ_RECTANGLE, 0, tStart, (pb.isBullish ? level50 : level30), tEnd, (pb.isBullish ? level30 : level50)); ObjectSetInteger(0, nameGreen, OBJPROP_COLOR, clrDarkGreen); ObjectSetInteger(0, nameGreen, OBJPROP_FILL, true); ObjectSetInteger(0, nameGreen, OBJPROP_BACK, true); //--- 50%-70% zone: yellow (deep) string nameYellow = StringFormat("SF_PB_Yellow_%d", i); ObjectCreate(0, nameYellow, OBJ_RECTANGLE, 0, tStart, (pb.isBullish ? level70 : level50), tEnd, (pb.isBullish ? level50 : level70)); ObjectSetInteger(0, nameYellow, OBJPROP_COLOR, clrDarkKhaki); ObjectSetInteger(0, nameYellow, OBJPROP_FILL, true); ObjectSetInteger(0, nameYellow, OBJPROP_BACK, true); //--- Score label string scoreLabel = StringFormat("SF_Score_%d", i); ObjectCreate(0, scoreLabel, OBJ_TEXT, 0, tStart, pb.impulseEnd); ObjectSetString(0, scoreLabel, OBJPROP_TEXT, StringFormat("PB:%.2f | %s | Sc:%.1f%s", pb.retracementDepth, PullbackRegimeToString(pb.regime), pb.pullbackScore, pb.compressionDetected ? " [COMP]" : "")); ObjectSetInteger(0, scoreLabel, OBJPROP_COLOR, clrCyan); ObjectSetInteger(0, scoreLabel, OBJPROP_FONTSIZE, 8); } //--- Liquidity zones for(int i = 0; i < ArraySize(liquidityZones); i++) if(!liquidityZones[i].taken) { string name = StringFormat("SF_Liq_%d", i); datetime tNow = TimeCurrent(); datetime tStart = tNow - PeriodSeconds(PERIOD_CURRENT) * 10; color zoneClr = liquidityZones[i].isHigh ? clrOrange : clrLightBlue; ObjectCreate(0, name, OBJ_RECTANGLE, 0, tStart, liquidityZones[i].price + _Point * 5, tNow, liquidityZones[i].price - _Point * 5); ObjectSetInteger(0, name, OBJPROP_COLOR, zoneClr); ObjectSetInteger(0, name, OBJPROP_FILL, true); ObjectSetInteger(0, name, OBJPROP_WIDTH, 1); } //--- HUD labels string stateText = "State: "; switch(marketStruct.state) { case ACCUMULATION: stateText += "ACCUMULATION"; break; case EXPANSION: stateText += "EXPANSION"; break; case DISTRIBUTION: stateText += "DISTRIBUTION"; break; case REVERSAL: stateText += "REVERSAL"; break; } CreateLabel("State", stateText, 10, 20, clrWhite); CreateLabel("Trend", StringFormat("Trend: %s", marketStruct.bullish ? "BULLISH" : "BEARISH"), 10, 40, marketStruct.bullish ? clrLime : clrRed); CreateLabel("VolRegime", StringFormat("Vol Regime: %s (ATR: %.5f | Avg: %.5f)", VolRegimeToString(currentVolRegime), currentATR, avgATR), 10, 60, clrYellow); CreateLabel("ValidSwings",StringFormat("Valid Swings: %d | Pullbacks: %d", ArraySize(validSwings), ArraySize(pullbacks)), 10, 80, clrCyan); //--- Best pullback info PullbackModel bestBull, bestBear; bool hasBull = GetBestPullback(true, bestBull); bool hasBear = GetBestPullback(false, bestBear); if(hasBull) CreateLabel("BestBull", StringFormat("Bull PB: %.2f | %s | Score: %.1f%s%s", bestBull.retracementDepth, PullbackRegimeToString(bestBull.regime), bestBull.pullbackScore, bestBull.compressionDetected ? " COMP" : "", bestBull.momentumDecaying ? " DECAY" : ""), 10, 100, clrLimeGreen); if(hasBear) CreateLabel("BestBear", StringFormat("Bear PB: %.2f | %s | Score: %.1f%s%s", bestBear.retracementDepth, PullbackRegimeToString(bestBear.regime), bestBear.pullbackScore, bestBear.compressionDetected ? " COMP" : "", bestBear.momentumDecaying ? " DECAY" : ""), 10, 120, clrOrangeRed); ChartRedraw(); } //+------------------------------------------------------------------+ //| Create Label helper | //+------------------------------------------------------------------+ void CreateLabel(string name, string text, int x, int y, color clr) { string objName = "SF_" + name; ObjectCreate(0, objName, OBJ_LABEL, 0, 0, 0); ObjectSetString(0, objName, OBJPROP_TEXT, text); ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, x); ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, y); ObjectSetInteger(0, objName, OBJPROP_COLOR, clr); ObjectSetInteger(0, objName, OBJPROP_FONTSIZE, 10); } //+------------------------------------------------------------------+
Здесь мы сосредоточимся на преобразовании исходных рыночных данных и данных моделей в наглядное представление на графике. Сначала мы отмечаем цветом валидные точки свинга, визуально различая максимумы и минимумы для удобства чтения структуры, а невалидные кандидаты в свинги отображаем серым цветом, чтобы выделить отклоненные сетапы. Затем мы отображаем на графике результаты анализа откатов, выделяя зоны коррекции на уровнях 30 %, 50 % и 70 % каждого импульсного движения. Эти зоны помогают нам наглядно понять, насколько глубоко происходит коррекция цены в рамках тренда. Кроме того, мы добавляем метки, в которых указываются глубина отката, классификация режима, оценка и сигналы сжатия, чтобы быстро оценить качество отката без изучения исходных данных.
В заключительной части визуализация дополняется отображением ликвидности, рыночного контекста и информации для поддержки принятия решений. Мы отображаем зоны ликвидности в виде прямоугольников с заливкой вокруг ключевых ценовых уровней, ликвидность на которых еще не снята, что позволяет нам четко видеть, где могут располагаться стоп-ордера и целевые уровни. Мы добавляем информационную панель, на которой отображаются состояние рынка, направление тренда, режим волатильности, а также общая статистика системы, например количество валидных свингов и откатов. Кроме того, мы выделяем лучшие бычьи и медвежьи откаты по их оценкам, показывая, являются ли они сильными, ослабевающими или сжатыми. Наконец, вспомогательные функции обеспечивают единообразное создание меток, а обновление графика позволяет синхронизировать все изменения и сохранять аккуратный вид.
Бэктест
Бэктест проводился в течение примерно двухмесячного периода с 1 апреля 2026 года по 30 мая 2026 года со следующими настройками:

Ниже приведены кривая капитала и результаты бэктеста:


Заключение
В ходе этой реализации советник был преобразован из средства проверки свингов по принципу «валиден/невалиден» в многоуровневую систему анализа откатов. Новый уровень 2.5 объединяет импульсные участки в пары, измеряет глубину коррекции в виде нормализованного отношения и оценивает каждый откат по шкале от 0 до 10. Была внедрена конфигурация с двумя дескрипторами ATR, позволяющая отделить быстрый сигнал режима волатильности от скользящего среднего и выделить четыре различных состояния волатильности, в зависимости от которых динамически расширяются или сужаются допустимые пороги глубины. Некорректно работавший автомат состояний был заменен корректно работающим механизмом определения направленности, который напрямую считывает подтвержденные максимумы и минимумы. Стоп-лоссы больше не задавались фиксированным количеством пунктов, а рассчитывались как величины, кратные ATR. Теперь сделка открывается только после того, как модель отката пройдёт все необходимые проверки на уровне исполнения.
Трейдер, который внимательно изучит эту статью, получит то, чего большинство систем никогда не дают, — обоснованный ответ на вопрос, почему на откате следует торговать или почему его следует пропустить. Догадки относительно глубины коррекции заменяются моделью с системой баллов, учитывающей рыночные режимы и адаптирующейся к реальным движениям рынка. Одного возвращения цены в зону больше недостаточно для открытия позиции — коррекция должна статистически соответствовать текущему уровню волатильности. Стоп-лоссы «дышат» вместе с рынком, а не противостоят ему. В результате получилась система, которая не просто выявляет структуру, но и понимает контекст. Этот переход — от распознавания паттернов к принятию решений, откалиброванных по волатильности, — отличает советник, лишь реагирующий на рынок, от советника, способного выживать в разных рыночных условиях.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/22807
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Очень хорошая статья!!! Поздравляю!!!
Версия, опубликованная на сайте Mql5, не открывает сделок и имеет другие параметры, чем в статье.