English
preview
Свинговые экстремумы и откаты в MQL5 (Часть 3): Определение структурной валидности за рамками простых максимумов и минимумов

Свинговые экстремумы и откаты в MQL5 (Часть 3): Определение структурной валидности за рамками простых максимумов и минимумов

MetaTrader 5 — Примеры |
24 1
Hlomohang John Borotho
Hlomohang John Borotho

Содержание

  1. Введение
  2. Обзор системы и принципы работы
  3. Начало работы
  4. Бэктест
  5. Заключение


Введение

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

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


Обзор системы и понимание принципов её работы

Советник построен на пятислойной архитектуре, предназначенной для фильтрации рыночного шума и выявления только структурно значимых ценовых уровней. Процесс начинается с этапа первичного обнаружения свингов, на котором потенциальные свинговые максимумы и минимумы выявляются с помощью ретроспективного окна в N баров. Однако это лишь кандидаты в свинги — они ещё не считаются валидными структурными элементами. Модуль структурной валидации применяет четыре критерия: слом структуры (BoS) на основе импульса, импульсное смещение (сила импульсной свечи), снятие ликвидности (выход тенью за предыдущий уровень с последующим разворотом) и удержание уровня во времени. Свинг считается валидным только в том случае, если он удовлетворяет хотя бы одному из этих условий, что позволяет системе игнорировать незначительные колебания и сосредоточиться только на уровнях, где проявился реальный рыночный интерес.


Только свинги, демонстрирующие слом структуры (BoS), импульсное смещение, снятие ликвидности или удержание уровня во времени, становятся валидными — это позволяет отделить шум от подлинной рыночной структуры.

После определения подтверждённых свингов советник использует слой взаимодействия с ликвидностью, чтобы выявлять места, где могут находиться институциональные ордера. Этот слой помечает такие зоны, как равные максимумы/минимумы и нетронутые точки свинга, как пулы ликвидности, а также отслеживает, была ли ликвидность в этих зонах снята. Одновременно структурная машина состояний непрерывно анализирует взаимосвязь между последовательными валидными свингами, чтобы определить текущую фазу рынка — накопление, расширение, распределение или разворот. Этот конечный автомат не просто присваивает метку «бычий» или «медвежий»; он отслеживает, как цена взаимодействует с подтверждёнными уровнями и зонами ликвидности, что позволяет системе прогнозировать, находится ли рынок в тренде, консолидируется или готовится к развороту. Сочетая подтверждённую структуру с отслеживанием ликвидности в режиме реального времени, советник формирует высоковероятностный контекст для принятия торговых решений.

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

Затем модуль исполнения сделок объединяет эти слои для определения точки входа. Сигнал на покупку формируется только в том случае, если система подтверждает валидный более высокий минимум, снятие ликвидности ниже текущей цены или бычье импульсное смещение — и, что особенно важно, когда состояние рынка поддерживает открытие длинной позиции (расширение или накопление). Аналогичным образом, для сигнала на продажу требуется валидный более низкий максимум, снятие ликвидности выше цены или медвежье импульсное смещение, согласованные с состоянием распределения или разворота. Стоп-лоссы размещаются на ближайшем подтверждённом уровне структуры, а не на точках исходных свингов. Это привязывает защитный уровень к точкам разворота. Тейк-профиты рассчитываются на основе заранее заданного соотношения риска к прибыли, с возможностью установить цель на следующую зону ликвидности или валидный структурный узел. Такая многоуровневая валидация гарантирует, что каждая сделка исполняется на основе структурной целостности, а не импульсивных ценовых движений, согласуя точки входа с базовым рыночным контекстом.

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


Начало работы

//+------------------------------------------------------------------+
//|                                        Structural_Validation.mq5 |
//|                        GIT under Copyright 2025, MetaQuotes Ltd. |
//|                     https://www.mql5.com/en/users/johnhlomohang/ |
//+------------------------------------------------------------------+
#property copyright "GIT under 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 double          RiskPercent         = 1.0;            // Risk per trade (%)
input int             StopLossPoints      = 2000;           // Stop Loss in points
input double          RiskRewardRatio     = 2.0;            // Risk:Reward ratio
input bool            Visualize           = true;           // Show visualization

//+------------------------------------------------------------------+
//| Enumerations                                                     |
//+------------------------------------------------------------------+
enum MarketState
  {
   ACCUMULATION,
   EXPANSION,
   DISTRIBUTION,
   REVERSAL
  };

Для начала подключите и создайте экземпляр класса CTrade для обработки исполнения ордеров и управления позициями. Затем мы определяем набор входных параметров, которые позволяют нам динамически управлять поведением системы без изменения кода. Эти параметры определяют, как мы выявляем точки свинга (SwingLookback), оцениваем силу рынка по импульсному смещению (DisplacementFactor), обеспечиваем удержание структуры во времени (StructureHoldBars) и измеряем волатильность с помощью ATR (ATR_Period). Кроме того, мы задаём ключевые параметры управления рисками, такие как процент капитала, которым рискуем в каждой сделке, расстояние стоп-лосса и желаемое соотношение риска к прибыли, а также включаем опциональный режим визуализации для отображения структурных элементов непосредственно на графике.

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

//+------------------------------------------------------------------+
//| 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;      // true = high liquidity, false = low liquidity
   string            type;        // "equal", "session", "untouched"

   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;
     }
  };

Теперь мы формализуем понимание цены в рамках советника, определяя структурированные модели данных для свингов, ликвидности и общего поведения рынка. Структура SwingPoint представляет каждую обнаруженную точку свинга на рынке, фиксируя не только её цену и время, но и то, является ли она максимумом или минимумом, была ли она подтверждена как значимый структурный элемент и использовалась ли она уже при принятии решений. Добавляя такие атрибуты, как barIndex и candleSize, мы получаем возможность связать каждый свинг с исходной свечой и оценить его силу. Функция Reset() обеспечивает возможность чистой переинициализации каждого экземпляра свинга, что особенно полезно при динамическом пересчёте структуры по мере поступления новых рыночных данных.

Параллельно с этим мы вводим LiquidityZone и MarketStructure, чтобы вывести нашу систему за рамки отдельных ценовых точек и перейти к контекстному пониманию. Структура LiquidityZone позволяет отслеживать области, в которых, вероятно, сконцентрированы ордера, различать такие типы, как равные максимумы/минимумы или нетронутые уровни, а также контролировать, была ли эта ликвидность уже снята. Структура MarketStructure затем объединяет все эти элементы, сохраняя последние значимые максимумы и минимумы, отслеживая, является ли рынок в данный момент бычьим или медвежьим, и присваивая более широкое состояние, например накопление или расширение.

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
SwingPoint         swingCandidates[];
SwingPoint         validSwings[];
LiquidityZone      liquidityZones[];
MarketStructure    marketStruct;
int                atrHandle;
datetime           lastBarTime = 0;
datetime           lastSwingDetection = 0;
double             avgCandleSize = 0;

//--- Trade tracking
datetime           lastTradeTime = 0;
double             lastTradePrice = 0;
int                lastTradeType = 0;  // 1 = buy, -1 = sell
double             BID = SymbolInfoDouble(_Symbol,SYMBOL_BID);
double             ASK = SymbolInfoDouble(_Symbol, SYMBOL_ASK);

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Initialize arrays
   ArrayResize(swingCandidates, 0);
   ArrayResize(validSwings, 0);
   ArrayResize(liquidityZones, 0);

   //--- Initialize market structure
   marketStruct.Reset();

   //--- Initialize ATR handle
   atrHandle = iATR(_Symbol, PERIOD_CURRENT, ATR_Period);
   if(atrHandle == INVALID_HANDLE)
     {
      Print("Failed to create ATR handle");
      return INIT_FAILED;
     }

   //--- Initial detection
   DetectSwingCandidates();
   ValidateSwings();
   UpdateLiquidityZones();
   UpdateMarketState();

   return INIT_SUCCEEDED;
  }

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   if(atrHandle != INVALID_HANDLE)
      IndicatorRelease(atrHandle);

   if(Visualize)
     {
      ObjectsDeleteAll(0, "SF_");
      ChartRedraw();
     }
  }

На данном этапе мы определяем глобальные переменные, используемые для отслеживания структуры, ликвидности и торговой активности. Массивы, такие как swingCandidates, validSwings и liquidityZones, выступают в роли динамических контейнеров, которые меняются по мере поступления ценовых данных, что позволяет нам отделить первичное обнаружение свингов от структурно подтверждённых точек и зон, представляющих интерес на рынке. Переменная marketStruct хранит текущий структурный контекст, а дополнительные переменные, такие как atrHandle, avgCandleSize и lastBarTime, помогают нам измерять волатильность, поддерживать согласованность по времени и гарантировать, что вычисления выполняются только на новых барах. Мы также используем переменные для отслеживания сделок, такие как lastTradeTime, lastTradePrice и lastTradeType, которые позволяют нам контролировать частоту исполнения и дают системе возможность принимать более обоснованные решения на основе недавней активности, наряду с актуальными ценами BID и ASK в реальном времени.

Функция OnInit() выполняет роль этапа инициализации, на котором мы подготавливаем все компоненты до начала работы советника. Здесь мы сбрасываем содержимое массивов и изменяем их размеры, чтобы обеспечить чистое начальное состояние, инициализируем структуру рынка и создаём дескриптор индикатора ATR, который впоследствии будет использоваться для вычислений на основе волатильности. Кроме того, мы выполняем первичный проход по обнаружению свингов, валидации, разметке ликвидности и классификации состояния рынка, чтобы советник начинал работу с полным представлением о графике, а не ждал появления первого тика. В свою очередь, функция OnDeinit() обеспечивает надлежащую очистку при удалении советника, освобождая ресурс ATR и удаляя все объекты графика, если включена визуализация.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   datetime currentBarTime = iTime(_Symbol, PERIOD_CURRENT, 0);

   //--- Process on new bar
   if(currentBarTime != lastBarTime)
     {
      lastBarTime = currentBarTime;

      //--- Update average candle size
      UpdateAvgCandleSize();

      //--- Layer 1: Detect new swing candidates
      DetectSwingCandidates();

      //--- Layer 2: Validate swings
      ValidateSwings();

      //--- Layer 3: Update liquidity zones
      UpdateLiquidityZones();

      //--- Layer 4: Update market state
      UpdateMarketState();

      //--- Layer 5: Check and execute trades
      CheckTradeConditions();

      //--- Visualization
      if(Visualize)
         UpdateVisualization();
     }
  }

//+------------------------------------------------------------------+
//| 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;

   //--- Clear old candidates (keep only last 50 bars)
   int newCount = 0;
   SwingPoint tempCandidates[];
   ArrayResize(tempCandidates, 0);

   for(int i = SwingLookback; i < copied - SwingLookback; i++)
     {
      bool isSwingHigh = true;
      bool isSwingLow = true;

      //--- Check swing high
      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;
        }

      //--- Add swing high candidate
      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;
         newCount++;
        }

      //--- Add swing low candidate
      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;
         newCount++;
        }
     }

   //--- Update global candidates array
   ArrayResize(swingCandidates, newCount);
   for(int i = 0; i < newCount; i++)
      swingCandidates[i] = tempCandidates[i];
  }

В функции OnTick() советник выполняет обработку только при появлении нового бара, а не при каждом тике. Мы достигаем этого путём сравнения текущего времени бара с ранее сохранённым значением lastBarTime, обеспечивая выполнение всех вычислений один раз на бар для согласованности и стабильности. Как только обнаруживается новый бар, мы запускаем структурированный конвейер: сначала обновляем средний размер свечи с учётом текущей волатильности, затем проходим по каждому логическому слою системы — выявляем кандидатов в свинги, подтверждаем их, определяем зоны ликвидности, обновляем общее состояние рынка и, наконец, оцениваем торговые возможности. Если визуализация включена, в завершение мы обновляем объекты графика, что позволяет нам визуально отслеживать, как система интерпретирует структуру рынка в режиме реального времени.

Функция DetectSwingCandidates() представляет собой первый слой этого конвейера, на котором мы выявляем потенциальные точки разворота цены, пока не придавая им структурного значения. Мы получаем последние рыночные данные с помощью CopyRates и просматриваем их, применяя окно обратного анализа, чтобы определить, является ли свеча свинговым максимумом или свинговым минимумом. Свинговый максимум считается подтверждённым, если его максимум превышает максимумы соседних свечей, а свинговый минимум определяется, когда его минимум ниже минимумов окружающих свечей. Для каждого обнаруженного кандидата в свинги мы создаём объект SwingPoint, заполняем его соответствующей информацией, такой как время, цена, позиция и размер свечи, и временно сохраняем его до обновления глобального массива. На данном этапе эти точки являются лишь кандидатами — они отражают возможную структуру, которая впоследствии будет отфильтрована и валидирована на последующих слоях системы.

//+------------------------------------------------------------------+
//| 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;

   //--- Reset validation for all candidates
   for(int i = 0; i < ArraySize(swingCandidates); i++)
      swingCandidates[i].isValid = false;

   //--- Clear valid swings array
   ArrayResize(validSwings, 0);
   int validCount = 0;

   for(int i = 0; i < ArraySize(swingCandidates); i++)
     {
      bool isValid = false;

      //--- Validation A: Break of Structure (BoS)
      if(CheckBreakOfStructure(swingCandidates[i], rates))
         isValid = true;

      //--- Validation B: Displacement (Impulse Strength)
      if(CheckDisplacement(swingCandidates[i], rates))
         isValid = true;

      //--- Validation C: Liquidity Sweep
      if(CheckLiquiditySweep(swingCandidates[i], rates))
         isValid = true;

      //--- Validation D: Time-Based Respect
      if(CheckTimeRespect(swingCandidates[i], rates))
         isValid = true;

      if(isValid)
        {
         swingCandidates[i].isValid = true;

         //--- Add to valid swings
         ArrayResize(validSwings, validCount + 1);
         validSwings[validCount] = swingCandidates[i];
         validCount++;
        }
     }
  }

//+------------------------------------------------------------------+
//| Validation A: Break of Structure                                 |
//+------------------------------------------------------------------+
bool CheckBreakOfStructure(SwingPoint &sp, MqlRates &rates[])
  {
   if(sp.isHigh)
     {
      //--- Find previous valid high
      double prevHigh = 0;
      for(int i = 0; i < ArraySize(validSwings); i++)
        {
         if(validSwings[i].isHigh && validSwings[i].time < sp.time)
           {
            if(prevHigh == 0 || validSwings[i].price > prevHigh)
               prevHigh = validSwings[i].price;
           }
        }
     }
   else
     {
      //--- Find previous valid low
      double prevLow = 0;
      for(int i = 0; i < ArraySize(validSwings); i++)
        {
         if(!validSwings[i].isHigh && validSwings[i].time < sp.time)
           {
            if(prevLow == 0 || validSwings[i].price < prevLow)
               prevLow = validSwings[i].price;
           }
        }
     }

   return false;
  }

//+------------------------------------------------------------------+
//| Validation B: Displacement (Impulse Strength)                    |
//+------------------------------------------------------------------+
bool CheckDisplacement(SwingPoint &sp, MqlRates &rates[])
  {
   if(avgCandleSize == 0)
      return false;

   double impulseSize = sp.candleSize;
   return (impulseSize > avgCandleSize * DisplacementFactor);
  }

Функция ValidateSwings() представляет собой ядро нашей структурной логики, в рамках которой мы преобразуем первичных кандидатов в свинги в значимую рыночную структуру. Вместо того чтобы рассматривать каждый обнаруженный максимум или минимум как значимый, мы систематически оцениваем каждого кандидата с учётом набора условий валидации, отражающих реальное поведение рынка. Для начала мы сбрасываем всех кандидатов в неподтверждённое состояние и очищаем массив validSwings, чтобы обеспечить новую оценку при каждом проходе. Затем для каждого кандидата в свинги мы проверяем, удовлетворяет ли он хотя бы одному из нескольких критериев: слом структуры (BoS), сильное импульсное смещение, снятие ликвидности или удержание уровня во времени. Если выполняется хотя бы одно из этих условий, свинг помечается как валидный и добавляется в массив validSwings, что позволяет эффективно отфильтровать шум и сохранить только те точки, которые отражают реальные намерения рынка.

Сами функции валидации определяют, как на практике выглядит «значимая» структура. В функции CheckBreakOfStructure() мы пытаемся определить, участвует ли текущий свинг в сломе предыдущего структурного уровня, сначала находя наиболее релевантный предыдущий максимум или минимум; хотя эта логика здесь реализована лишь частично, она закладывает основу для подтверждения направленных сдвигов цены. В отличие от этого, функция CheckDisplacement() фокусируется на импульсе, сравнивая размер свечи, от которой сформировался свинг, со средним размером свечи, чтобы определить, было ли движение импульсным, а не случайным.

//+------------------------------------------------------------------+
//| 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)
     {
      //--- Check if price wicked above previous high and closed below
      for(int i = 0; i < ArraySize(validSwings); i++)
        {
         if(validSwings[i].isHigh && validSwings[i].time < sp.time)
           {
            double prevHigh = validSwings[i].price;
            if(rates[barIndex].high > prevHigh && rates[barIndex].close < prevHigh)
               return true;
           }
        }
     }
   else
     {
      //--- Check if price wicked below previous low and closed above
      for(int i = 0; i < ArraySize(validSwings); i++)
        {
         if(!validSwings[i].isHigh && validSwings[i].time < sp.time)
           {
            double prevLow = validSwings[i].price;
            if(rates[barIndex].low < prevLow && rates[barIndex].close > prevLow)
               return true;
           }
        }
     }

   return false;
  }

//+------------------------------------------------------------------+
//| Validation D: Time-Based Respect                                 |
//+------------------------------------------------------------------+
bool CheckTimeRespect(SwingPoint &sp, MqlRates &rates[])
  {
   int barIndex = FindBarIndexByTime(sp.time);
   if(barIndex < 0 || barIndex + StructureHoldBars >= ArraySize(rates))
      return false;

   if(sp.isHigh)
     {
      //--- Structure must hold for X bars (price stays below swing high)
      for(int i = 1; i <= StructureHoldBars; i++)
        {
         if(rates[barIndex + i].high > sp.price)
            return false;
        }
      return true;
     }
   else
     {
      //--- Structure must hold for X bars (price stays above swing low)
      for(int i = 1; i <= StructureHoldBars; i++)
        {
         if(rates[barIndex + i].low < sp.price)
            return false;
        }
      return true;
     }
  }

Функция CheckLiquiditySweep() позволяет валидировать структуру на основе того, как цена взаимодействует с ликвидностью, а не только по тому, где она разворачивается. Для начала мы определяем точный бар, связанный с точкой свинга, чтобы убедиться, что анализируем нужную свечу в контексте. Затем мы проверяем, выходит ли цена временно за пределы предыдущего структурного уровня — например, предыдущего максимума или минимума, — но не удерживается ли за ним. Для свингового максимума это означает, что цена выходит тенью выше предыдущего максимума, а затем закрывается ниже него, сигнализируя об отторжении после снятия ликвидности. Та же логика применяется в обратном направлении для свинговых минимумов. Такое поведение имеет решающее значение, поскольку оно указывает на охоту за стопами или захват ликвидности, что часто предшествует сильным направленным движениям и делает такие свинги гораздо более значимыми, чем простые точки разворота.

Функция CheckTimeRespect() дополняет это, вводя требование стабильности, которое гарантирует удержание уровня свинга рынком в течение заданного числа баров. После определения индекса бара свинга мы проверяем, не нарушает ли цена уровень свинга в течение заданного периода (StructureHoldBars). Для свингового максимума цена должна оставаться ниже этого уровня, а для свингового минимума — выше него. Если это условие выполняется, свинг считается структурно удержанным рынком, что указывает на реальную значимость уровня для направления ценового движения.

//+------------------------------------------------------------------+
//| LAYER 3: Liquidity Interaction Layer                             |
//+------------------------------------------------------------------+
void UpdateLiquidityZones()
  {
   MqlRates rates[];
   ArraySetAsSeries(rates, true);
   CopyRates(_Symbol, PERIOD_CURRENT, 0, 100, rates);

   if(ArraySize(rates) < 50)
      return;

   //--- Clear old liquidity zones
   ArrayResize(liquidityZones, 0);
   int zoneCount = 0;

   //--- Detect equal highs/lows (double tops/bottoms)
   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;
            zoneCount++;
            break;
           }
        }
     }

   //--- Add untouched swing points as liquidity
   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;
         zoneCount++;
        }
     }
  }

//+------------------------------------------------------------------+
//| 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;

   //--- Get last two valid swings
   SwingPoint lastSwing = validSwings[ArraySize(validSwings) - 1];
   SwingPoint prevSwing = validSwings[ArraySize(validSwings) - 2];

   //--- Update last high/low
   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;
     }

   //--- Determine trend based on higher highs and higher lows
   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);

   //--- State transition logic
   MarketState prevState = marketStruct.state;

   //--- Check for liquidity sweep failure (potential reversal)
   bool liquiditySweepFailed = CheckLiquiditySweepFailure();

   switch(marketStruct.state)
     {
      case ACCUMULATION:
         if(marketStruct.bullish)
            marketStruct.state = EXPANSION;
         else
            if(!marketStruct.bullish && 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 && prevState == EXPANSION)
            marketStruct.state = ACCUMULATION;
         else
            if(!marketStruct.bullish && prevState == DISTRIBUTION)
               marketStruct.state = ACCUMULATION;
         break;
     }
  }

В этом слое мы переходим от отдельных структурных точек к определению того, где на рынке, вероятно, сосредоточена ликвидность. Функция UpdateLiquidityZones() создает динамическую карту этих областей, просматривая подтверждённые свинги и группируя их в значимые зоны. Сначала мы выявляем равные максимумы и минимумы, обычно связанные с двойными вершинами или двойными днами, проверяя, находятся ли две точки свинга почти на одном ценовом уровне. Они помечаются как зоны «равной» ликвидности, поскольку, как правило, привлекают стоп-ордера. Затем мы расширяем эту логику, рассматривая все неиспользованные валидные свинги как «нетронутую» ликвидность, то есть уровни, которые рынок ещё не тестировал повторно. Каждая зона оценивается с помощью функции CheckIfLiquidityTaken(), которая определяет, прошла ли цена уже через данный уровень, что позволяет различать активную и уже снятую ликвидность. Это даёт системе перспективу на будущее: куда цену может притянуть дальше.

На основе этого функция UpdateMarketState() вводит структурную машину состояний, которая интерпретирует, как развивается рынок. Сначала мы обновляем данные о последних значимых максимумах и минимумах, а затем оцениваем, формирует ли рынок более высокие максимумы и более высокие минимумы, чтобы определить рыночный уклон. Эта информация поступает в систему переходов состояний, которая относит рынок к таким фазам, как накопление, расширение, распределение или разворот. Учитывая такие условия, как неудачные снятия ликвидности, мы можем выявлять потенциальные точки разворота, где меняется намерение рынка. Такой многоуровневый подход позволяет нам выйти за рамки статического анализа и вместо этого моделировать рынок как последовательность меняющихся состояний, обеспечивая соответствие наших торговых решений как структуре рынка, так и лежащему в основе поведению ликвидности.

//+------------------------------------------------------------------+
//| Check liquidity sweep failure                                    |
//+------------------------------------------------------------------+
bool CheckLiquiditySweepFailure()
  {
   MqlRates rates[];
   ArraySetAsSeries(rates, true);
   CopyRates(_Symbol, PERIOD_CURRENT, 0, 10, rates);

   if(ArraySize(rates) < 5)
      return false;

   //--- Check if price swept a high and reversed
   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                                        |
//+------------------------------------------------------------------+
void CheckTradeConditions()
  {
   //--- Check if already in a position
   if(PositionSelect(_Symbol))
     {
      //--- Check if we should trail stop or manage position
      ManageOpenPosition();
      return;
     }

   //--- Avoid trading too frequently
   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;

   //--- Get last validated swing
   if(ArraySize(validSwings) == 0)
      return;

   SwingPoint lastValidSwing = validSwings[ArraySize(validSwings) - 1];

   //--- Buy conditions
   if(CheckBuyConditions(lastValidSwing, current))
     {
      ExecuteTrade(ORDER_TYPE_BUY);
      lastTradeTime = TimeCurrent();
      lastTradeType = 1;
     }
   //--- Sell conditions
   else
      if(CheckSellConditions(lastValidSwing, current))
        {
         ExecuteTrade(ORDER_TYPE_SELL);
         lastTradeTime = TimeCurrent();
         lastTradeType = -1;
        }
  }

Функция CheckLiquiditySweepFailure() предназначена для выявления очень специфичного и сильного рыночного поведения: когда цена пытается снять ликвидность, но не может продолжить движение в этом направлении, что часто сигнализирует о возможном развороте. Для начала мы собираем небольшое окно последних ценовых данных и убеждаемся, что информации достаточно для анализа. Затем для каждой активной (нетронутой) зоны ликвидности мы проверяем, вышла ли последняя свеча за пределы этого уровня — что указывает на снятие ликвидности, — но закрылась обратно внутри зоны, демонстрируя отвержение. Для максимумов это означает, что цена выходит выше зоны и закрывается ниже неё; для минимумов применяется обратное правило. Неспособность удержать пробой указывает на то, что движение было не истинным, а скорее захватом ликвидности, что может быть сильным предвестником смены направления.

CheckTradeConditions() — это шлюз исполнения, который объединяет структурный слой и слой ликвидности. Сначала мы проверяем, что сейчас нет открытой позиции; если она есть, передаём управление отдельной функции, а также применяем временной фильтр, чтобы избежать чрезмерной торговли. После получения последних ценовых данных мы ориентируемся на самый последний подтверждённый свинг как на нашу структурную опорную точку. На этом этапе мы оцениваем условия как для покупки, так и для продажи с помощью специальных функций, учитывающих структурную валидность, взаимодействие с ликвидностью и состояние рынка. Если выполняются критерии для любого из направлений, мы исполняем соответствующую сделку и обновляем переменные отслеживания, чтобы контролировать частоту и направление сделок. Это гарантирует, что точки входа не носят случайный характер, а соответствуют более широкому структурному контексту, определяемому системой.

//+------------------------------------------------------------------+
//| Check Buy Conditions                                             |
//+------------------------------------------------------------------+
bool CheckBuyConditions(SwingPoint &lastSwing, MqlRates &rates[])
  {
   //--- Must be a valid low
   if(lastSwing.isHigh || !lastSwing.isValid)
      return false;

   //--- Condition 1: Valid higher low
   double previousLow = 0;
   for(int i = 0; i < ArraySize(validSwings) - 1; i++)
     {
      if(!validSwings[i].isHigh && validSwings[i].price < lastSwing.price)
        {
         if(previousLow == 0 || validSwings[i].price > previousLow)
            previousLow = validSwings[i].price;
        }
     }

   bool higherLow = (previousLow > 0 && lastSwing.price > previousLow);

   //--- Condition 2: Liquidity sweep below
   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;
           }
        }
     }

   //--- Condition 3: Bullish displacement
   bool bullishDisplacement = false;
   if(ArraySize(rates) >= 2)
     {
      double candleSize = (rates[0].close - rates[0].open) / _Point;
      if(candleSize > avgCandleSize * DisplacementFactor)
         bullishDisplacement = true;
     }

   //--- Condition 4: Market state should be bullish or accumulation
   bool goodState = (marketStruct.state == EXPANSION || marketStruct.state == ACCUMULATION);

   return (higherLow || liquiditySweep || bullishDisplacement) && goodState;
  }

//+------------------------------------------------------------------+
//| Check Sell Conditions                                            |
//+------------------------------------------------------------------+
bool CheckSellConditions(SwingPoint &lastSwing, MqlRates &rates[])
  {
   //--- Must be a valid high
   if(!lastSwing.isHigh || !lastSwing.isValid)
      return false;

   //--- Condition 1: Valid lower high
   double previousHigh = 0;
   for(int i = 0; i < ArraySize(validSwings) - 1; i++)
     {
      if(validSwings[i].isHigh && validSwings[i].price > lastSwing.price)
        {
         if(previousHigh == 0 || validSwings[i].price < previousHigh)
            previousHigh = validSwings[i].price;
        }
     }

   bool lowerHigh = (previousHigh > 0 && lastSwing.price < previousHigh);

   //--- Condition 2: Liquidity sweep above
   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;
           }
        }
     }

   //--- Condition 3: Bearish displacement
   bool bearishDisplacement = false;
   if(ArraySize(rates) >= 2)
     {
      double candleSize = (rates[0].open - rates[0].close) / _Point;
      if(candleSize > avgCandleSize * DisplacementFactor)
         bearishDisplacement = true;
     }

   //--- Condition 4: Market state should be bearish or distribution
   bool goodState = (marketStruct.state == DISTRIBUTION || marketStruct.state == REVERSAL);

   return (lowerHigh || liquiditySweep || bearishDisplacement) && goodState;
  }

//+------------------------------------------------------------------+
//| Execute Trade                                                    |
//+------------------------------------------------------------------+
void ExecuteTrade(ENUM_ORDER_TYPE tradeType)
  {
   double price = (tradeType == ORDER_TYPE_BUY) ?
                  SymbolInfoDouble(_Symbol, SYMBOL_ASK) :
                  SymbolInfoDouble(_Symbol, SYMBOL_BID);

   //--- Calculate stop loss based on validated structure
   double sl = CalculateStopLoss(tradeType);
   if(sl == 0)
      return;

   //--- Calculate take profit based on risk:reward
   double riskPoints = MathAbs(price - sl) / _Point;
   double tpPoints = riskPoints * RiskRewardRatio;
   double tp = (tradeType == ORDER_TYPE_BUY) ?
               price + tpPoints * _Point :
               price - tpPoints * _Point;

   //--- Normalize prices
   price = NormalizeDouble(price, _Digits);
   sl = NormalizeDouble(sl, _Digits);
   tp = NormalizeDouble(tp, _Digits);

   //--- Calculate lot size
   double volume = CalculateLotSize(price, sl);

   //--- Execute trade
   string comment = StringFormat("SF_%s", (tradeType == ORDER_TYPE_BUY) ? "BUY" : "SELL");
   bool success = trade.PositionOpen(_Symbol, tradeType, volume, price, sl, tp, comment);

   if(success)
     {
      lastTradePrice = price;
      Print(StringFormat("Trade Opened: %s | Price: %.5f | SL: %.5f | TP: %.5f | Lots: %.2f",
                         (tradeType == ORDER_TYPE_BUY) ? "BUY" : "SELL", price, sl, tp, volume));

      //--- Mark used swings
      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());
     }
  }

Функции CheckBuyConditions() и CheckSellConditions() определяют, как мы преобразуем понимание структуры рынка в практические торговые сигналы. Для покупок мы требуем, чтобы последний свинг был валидным свинговым минимумом, что гарантирует работу от структурно значимой базы. Затем мы анализируем три основных подтверждения: формирует ли рынок более высокий минимум (что указывает на бычью структуру), была ли ликвидность ниже снята и отвергнута (что предполагает накопление и намерение двигаться выше) и наблюдается ли на последней свече сильное бычье импульсное смещение. Эти условия не обязательно должны выполняться одновременно; вместо этого мы допускаем гибкость, принимая любое из них при условии, что общее состояние рынка поддерживает бычье поведение, например в фазах накопления или расширения. Логика продаж зеркально повторяет этот подход, уделяя особое внимание подтверждённым свинговым максимумам, более низким максимумам, снятию ликвидности выше уровня и медвежьему импульсному смещению, при этом согласуясь с фазами распределения или разворота.

Затем функция ExecuteTrade() применяет эти сигналы на практике, размещая ордера с соблюдением надлежащего управления рисками. Для начала мы определяем корректную цену входа с учётом направления сделки и рассчитываем стоп-лосс, привязанный к подтверждённым структурным уровням, а не к произвольным расстояниям. Исходя из этого, мы рассчитываем уровень тейк-профита с использованием заранее заданного соотношения риска к прибыли, что обеспечивает стабильность математического ожидания сделок. Цены нормализуются в соответствии с точностью инструмента, а размер позиции рассчитывается динамически на основе заданного процента риска и расстояния до стопа. После совершения сделки мы фиксируем её детали в целях прозрачности и помечаем ближайшие подтверждённые свинги как «использованные», чтобы избежать повторной опоры на одну и ту же структуру. Это гарантирует, что каждая сделка учитывает контекст и управляется по системным правилам, что укрепляет целостность общей стратегии.

//+------------------------------------------------------------------+
//| Calculate Stop Loss Based on Validated Structure                 |
//+------------------------------------------------------------------+
double CalculateStopLoss(ENUM_ORDER_TYPE tradeType)
  {
   if(ArraySize(validSwings) < 1)
      return 0;

   if(tradeType == ORDER_TYPE_BUY)
     {
      //--- Find nearest valid low below current price
      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)
        {
         double buffer = _Point * 10;
         return nearestLow - buffer;
        }
     }
   else
     {
      //--- Find nearest valid high above current price
      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 = _Point * 10;
         return nearestHigh + buffer;
        }
     }

   //--- Fallback to fixed stop loss
   double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
   double currentPrice = (tradeType == ORDER_TYPE_BUY) ?
                         SymbolInfoDouble(_Symbol, SYMBOL_BID) :
                         SymbolInfoDouble(_Symbol, SYMBOL_ASK);

   if(tradeType == ORDER_TYPE_BUY)
      return currentPrice - (StopLossPoints * point);
   else
      return currentPrice + (StopLossPoints * point);
  }

Функция CalculateStopLoss() определяет структурно обоснованный стоп-лосс, привязывая его к ближайшему подтверждённому свингу, а не используя произвольные расстояния. При открытии позиций на покупку мы просматриваем подтверждённые свинги, чтобы найти ближайший свинговый минимум ниже текущей цены, а при открытии позиций на продажу — ближайший свинговый максимум выше текущей цены. После определения этого уровня за ним добавляется небольшой буфер, чтобы учесть незначительные ценовые колебания и избежать преждевременного срабатывания стопа. Если подходящий структурный уровень не найден, функция переключается на фиксированный стоп-лосс, основанный на заранее заданных пунктах, что гарантирует сохранение ограничения риска по каждой сделке даже при отсутствии чёткой структуры.

//+------------------------------------------------------------------+
//| Helper Functions                                                 |
//+------------------------------------------------------------------+
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;
  }

//+------------------------------------------------------------------+
//|  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;
  }

//+------------------------------------------------------------------+
//| Visualization Functions                                          |
//+------------------------------------------------------------------+
void UpdateVisualization()
  {
   ObjectsDeleteAll(0, "SF_");

   //--- Draw valid swings (green for highs, red for lows)
   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);
     }

   //--- Draw invalid swings (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);
        }
     }

   //--- Draw liquidity zones as rectangles
   for(int i = 0; i < ArraySize(liquidityZones); i++)
     {
      if(!liquidityZones[i].taken)
        {
         string name = StringFormat("SF_Liq_%d", i);
         datetime timeNow = TimeCurrent();
         datetime timeStart = timeNow - PeriodSeconds(PERIOD_CURRENT) * 10;
         datetime timeEnd = timeNow;

         color zoneClr = liquidityZones[i].isHigh ? clrOrange : clrLightBlue;

         ObjectCreate(0, name, OBJ_RECTANGLE, 0, timeStart,
                      liquidityZones[i].price + _Point * 5, timeEnd,
                      liquidityZones[i].price - _Point * 5);
         ObjectSetInteger(0, name, OBJPROP_COLOR, zoneClr);
         ObjectSetInteger(0, name, OBJPROP_FILL, true);
         ObjectSetInteger(0, name, OBJPROP_WIDTH, 1);
        }
     }

   //--- Draw market state label
   string stateText = "Market State: ";
   switch(marketStruct.state)
     {
      case ACCUMULATION:
         stateText += "ACCUMULATION";
         break;
      case EXPANSION:
         stateText += "EXPANSION (BULLISH)";
         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("ValidSwings", StringFormat("Valid Swings: %d", ArraySize(validSwings)),
               10, 60, clrYellow);
  }

Вспомогательные функции обеспечивают необходимую поддержку анализа и синхронизации внутри советника (EA). В функции UpdateAvgCandleSize() мы вычисляем средний диапазон последних свечей, суммируя разницу между максимумами и минимумами на выборке баров и нормализуя результат в пунктах. Это значение становится динамическим ориентиром для выявления импульсного смещения, что позволяет отличать обычное движение цены от сильного импульсного поведения. Функция FindBarIndexByTime() дополняет этот процесс, сопоставляя конкретную временную метку с соответствующим ей индексом в ценовом ряду, что гарантирует точное соотнесение всех структурных оценок — таких как валидация и проверки ликвидности — с нужной свечой в исторических данных.

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


Результаты бэктеста

Бэктест проводился на паре XAUUSD на таймфрейме H1 в течение 2-месячного периода тестирования (с 1 декабря 2025 года по 30 января 2026 года) с использованием настроек по умолчанию:



Заключение

В заключение мы преобразовали упрощённое представление о том, что «свинг — это просто максимум или минимум», в структурированную систему, основанную на правилах, которая определяет, что действительно имеет значение в ценовом движении. Вводя слои валидации, такие как слом структуры (BoS), импульсное смещение, снятие ликвидности и удержание уровня во времени, мы переходим от первичного обнаружения к содержательной интерпретации. Теперь каждый свинг должен подтверждать свою значимость через взаимодействие, намерение и устойчивость, эффективно отфильтровывая шум и выделяя только те точки, которые отражают подлинное поведение рынка. В результате получается система, которая больше не реагирует на цену вслепую, а понимает, почему цена движется, и основывает каждое решение на наблюдаемой структурной логике.

На практике после реализации этого фреймворка вы сможете:

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

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

Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (1)
Jacryptoking
Jacryptoking | 5 апр. 2026 в 04:45

внутренний сдвиг на таймфрейме H4

сдвиг на таймфрейме M15

таймфрейм M15 = POI


Особенности написания Пользовательских Индикаторов Особенности написания Пользовательских Индикаторов
Написание пользовательских индикаторов в торговой системе MetaTrader 4
Алгоритм оптимизации скопы — Osprey Optimization Algorithm(OOA) Алгоритм оптимизации скопы — Osprey Optimization Algorithm(OOA)
Алгоритм оптимизации скопы (OOA) — ещё один представитель метаэвристик с красивой метафорой и первыми местами на бенчмарках. Проверка на стенде показывает другое. В статье разбираем, какие две формулы ломают канон, исправляем их по одной с измерением вклада каждой правки и доводим алгоритм с 30% до 60%. На выходе — готовая реализация в C_AO с тремя параметрами для оптимизации торговых систем и понятный способ проверить любой новый алгоритм, прежде чем ему доверять.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Автоматическое принятие и отклонение изменений ML-советника в MIDAS/MQL5: защита от переобучения через проверку устойчивости Автоматическое принятие и отклонение изменений ML-советника в MIDAS/MQL5: защита от переобучения через проверку устойчивости
В статье рассматривается Robustness Guard для отбора изменений торговой ML-модели в MIDAS. Кандидаты сравниваются с зафиксированной базовой версией на нескольких временных блоках и принимаются только при улучшении целевой оценки без нарушения ограничений по Profit Factor, просадке, числу сделок, стабильности месяцев и концентрации по инструментам. Механизм перенесён в MQL5 и дополнен self-test и shadow-режимом.