Свинговые экстремумы и откаты в MQL5 (Часть 3): Определение структурной валидности за рамками простых максимумов и минимумов
Содержание
Введение
Для многих трейдеров понятие «структура рынка» зачастую сводится к упрощённому поиску более высоких максимумов и более низких минимумов. Хотя визуально этот подход интуитивно понятен, у него есть критический недостаток: он рассматривает каждый ценовой экстремум как одинаково значимый, что приводит к входам на основе «шума», а не реальных изменений в расстановке сил на рынке. Такое отсутствие фильтрации зачастую приводит к тому, что трейдеры открывают позиции в фазах консолидации или на фоне кратковременного снятия ликвидности, принимая незначительные откаты за разворот тренда. Следовательно, эта «структурная неоднозначность» увеличивает число ложных пробоев и ухудшает сетапы с точки зрения соотношения риска и прибыли. Кроме того, это вызывает разочарование, когда чётко обозначенную линию поддержки выносит тенью, после чего цена разворачивается.
Для решения этой проблемы мы предлагаем многоуровневый модуль валидации, который выходит за рамки простых максимумов и минимумов, определяя, что делает структурную точку валидной. Вместо того чтобы опираться на исходные точки свинга, система использует модуль структурной валидации. Свинг считается валидным только в том случае, если он демонстрирует импульсный слом структуры (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
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Особенности написания Пользовательских Индикаторов
Алгоритм оптимизации скопы — Osprey Optimization Algorithm(OOA)
Автоматическое принятие и отклонение изменений ML-советника в MIDAS/MQL5: защита от переобучения через проверку устойчивости
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Ознакомьтесь с новой статьей: «Свинг-экстремумы и откаты в MQL5 (Часть 3): определение структурной достоверности за пределами простых максимумов и минимумов».
Автор: Хломоханг Джон Борото
внутренний сдвиг на таймфрейме H4
сдвиг на таймфрейме M15
таймфрейм M15 = POI