English
preview
Разработка динамического мультивалютного советника (Часть 9): Фильтрация шума исполнения с учетом микроструктуры рынка

Разработка динамического мультивалютного советника (Часть 9): Фильтрация шума исполнения с учетом микроструктуры рынка

MetaTrader 5 — Примеры |
49 0
Hlomohang John Borotho
Hlomohang John Borotho

Содержание

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


Введение

Большинство трейдеров строят свои стратегии на основе ценового движения, индикаторов или статистических преимуществ. Затем они удивляются, почему система, которая показывает отличные результаты на бэктесте, теряет деньги в реальной торговле. Виновником редко бывает сигнал. Дело в моменте исполнения. Спреды могут внезапно расширяться во время публикации новостей, переноса позиций и торговых сессий с низкой ликвидностью. Поток тиков становится нестабильным, когда алгоритмы институциональных инвесторов меняют позиции. Цена скачком проходит уровни, которые должны были служить поддержкой. Проскальзывание при исполнении обычного рыночного ордера съедает половину ожидаемой прибыли. Трейдер наблюдает, как его преимущество тает на глазах — не потому, что направление было выбрано неверно, а потому, что микроструктура рынка находилась в состоянии контролируемого хаоса именно в тот момент, когда система попыталась совершить сделку. Ничего из этого не видно на стандартном свечном графике. Большинство экспертных советников (EA) для розничных трейдеров не имеют механизмов обнаружения таких явлений и реагирования на них.

Решение заключается в том, чтобы рассматривать качество исполнения как фильтр первостепенной важности. Он выступает в качестве промежуточного слоя между логикой вашей стратегии и рынком. Он разрешает торговлю только тогда, когда технические условия исполнения благоприятны. Вместо того чтобы открывать сделку по каждому достоверному сигналу, советник сначала проверяет условия исполнения. Он проверяет спред, скорость тиков, недавние разрывы в котировках и стабильность микроволатильности. Только после успешного прохождения всех проверок система приступает к оценке самого торгового сигнала. Этот подход сочетается со стратегией продолжения движения после свипа ликвидности, нацеленной на использование последствий охоты за стопами. В результате получается советник, который не просто разумнее выбирает моменты для торговли — сама его архитектура исключает торговлю в условиях, на которые фактически приходится большая часть убытков розничных трейдеров.


Обзор системы

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

Советник не прогнозирует направление движения. Он прогнозирует, позволяет ли текущее состояние рынка исполнить ордер без утраты торгового преимущества.

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

Советник не гонится за свипом ликвидности. Он дожидается, пока хаос уляжется, убеждается, что рынок определил направление движения, и только после этого вкладывает капитал.


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

//+------------------------------------------------------------------+
//|                                                 Market Micro.mq5 |
//|                                  Copyright 2025, MetaQuotes Ltd. |
//|                     https://www.mql5.com/en/users/johnhlomohang/ |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com/en/users/johnhlomohang/"
#property version   "1.00"
#property description "Market Microstructure Execution Noise Filtering EA"

#include <Trade\Trade.mqh>
#include <Trade\PositionInfo.mqh>
#include <Trade\OrderInfo.mqh>
#include <Trade\SymbolInfo.mqh>

//+------------------------------------------------------------------+
//|                         INPUT PARAMETERS                         |
//+------------------------------------------------------------------+
input group "═══ Symbol Configuration ═══"
input string   Symbols                 = "XAUUSD,GBPUSD,USDJPY,USDZAR";

input group "═══ Noise Filter Engine ═══"
input bool     EnableNoiseFilter       = true;
input double   MaxSpreadMultiplier     = 2.0;
input double   MaxTickVelocity         = 40.0;
input double   MaxGapPoints            = 150.0;
input double   MinVolatilityPoints     = 3.0;
input double   MaxVolatilityPoints     = 120.0;
input int      SpreadSamplePeriod      = 200;
input int      VolatilitySampleTicks   = 20;
input double   SlippageStabilityFactor = 2.5;

input group "═══ Liquidity Sweep Strategy ═══"
input int      SwingLookback           = 20;
input int      StructureConfirmBars    = 3;
input double   SweepTolerancePoints    = 10.0;
input int      NoiseSettleSeconds      = 30;
input bool     EnableBuySignals        = true;
input bool     EnableSellSignals       = true;

input group "═══ Risk Management ═══"
input double   RiskPercent             = 1.0;
input double   MaxTotalExposurePercent = 5.0;
input int      MaxTotalPositions       = 4;
input double   MaxCorrelatedRisk       = 3.0;
input double   MaxDrawdownPercent      = 10.0;
input double   StopLossATRMultiplier   = 1.5;
input double   TakeProfitRR            = 2.0;
input int      ATRPeriod               = 14;

input group "═══ Execution Engine ═══"
input int      MaxSlippagePoints       = 30;
input int      MaxRetries              = 3;
input int      RetryDelayMS            = 200;
input ulong    MagicNumber             = 20250101;

input group "═══ Logging & Diagnostics ═══"
input bool     EnableDetailedLog       = true;
input bool     EnableTradeLog          = true;
input bool     EnableScoreDisplay      = true;
input int      LogLevel                = 2;

input group "═══ Tester Overrides ═══"
input bool     TesterMode              = true;       // Enable relaxed filters for testing
input double   TesterSpreadMult        = 3.0;        // Relaxed spread multiplier in tester
input double   TesterMaxGapPoints      = 300.0;      // Relaxed gap points in tester
input double   TesterMinVolatility     = 1.0;        // Relaxed min volatility in tester
input double   TesterMaxVolatility     = 200.0;      // Relaxed max volatility in tester


enum MarketState
  {
   CLEAN_TREND         = 0,
   CLEAN_BREAKOUT      = 1,
   NOISY_CHOP          = 2,
   HIGH_RISK_NEWS      = 3,
   THIN_LIQUIDITY      = 4,
   EXECUTION_UNSTABLE  = 5
  };

enum SignalType
  {
   SIGNAL_NONE  =  0,
   SIGNAL_BUY   =  1,
   SIGNAL_SELL  = -1
  };

enum SweepState
  {
   SWEEP_NONE =  0,
   SWEEP_HIGH =  1,
   SWEEP_LOW  = -1
  };

#define MAX_TICKS        300
#define MAX_SYMBOLS       20
#define MAX_SPREAD_HIST  300

struct TickData
  {
   double            bid;
   double            ask;
   long              volume;
   datetime          time;
   ulong             ms;
  };

struct SymbolState
  {
   string            symbol;
   int               digits;
   double            point;
   double            tickSize;
   double            tickValue;
   double            lotStep;
   double            minLot;
   double            maxLot;
   double            contractSize;
   double            bid;
   double            ask;
   double            spread;
   datetime          lastTickTime;
   ulong             lastTickMS;
   double            avgSpread;
   double            spreadStdDev;
   int               spreadSpikeCount;
   double            spreadHistory[MAX_SPREAD_HIST];
   int               spreadHistoryIdx;
   int               spreadHistoryCount;
   double            tickVelocity;
   int               tickCountWindow;
   datetime          tickWindowStart;
   int               tickBurstCount;
   double            lastBid;
   double            lastAsk;
   double            maxGapPoints;
   int               gapEventCount;
   double            microVolatility;
   double            slippageEstimate;
   double            avgSlippage;
   int               slippageSamples;
   MarketState       state;
   bool              noisy;
   bool              tradable;
   int               qualityScore;
   SweepState        sweepState;
   double            sweepLevel;
   datetime          sweepTime;
   bool              noiseSettled;
   bool              structureConfirmed;
   datetime          sweepDetectedAt;
   double            swingHigh;
   double            swingLow;
   double            prevSwingHigh;
   double            prevSwingLow;
   int               tradesThisSession;
   int               blockedByNoise;
   double            sessionPnL;
   TickData          tickBuffer[MAX_TICKS];
   int               tickIdx;
   int               tickCount;
   int               atrHandle;
   datetime          lastCollectTime;     // Track last tick collection time
   int               barsSinceLastTick;   // Track bars without ticks
  };

//--- Global variables
CTrade        Trade;
CPositionInfo PositionInfo;

string        SymbolArray[];
int           SymbolCount      = 0;
SymbolState   States[];

int           TotalSignals      = 0;
int           TotalFiltered     = 0;
int           TotalExecuted     = 0;
datetime      SessionStart;
double        SessionStartBalance;
bool          IsTesting = false;  // Auto-detect tester mode

Для начала мы подключаем библиотеки торговли и управления позициями, которые предоставляют экспертному советнику (EA) доступ к исполнению ордеров, данным по символам и информации об активных сделках. Затем мы организуем систему в виде четко разграниченных групп входных параметров, которые управляют мониторингом символов, фильтрацией шума исполнения, обнаружением свипов ликвидности, управлением рисками и ведением диагностического журнала. Данная конфигурация позволяет отслеживать несколько инструментов — XAUUSD, GBPUSD, USDJPY и USDZAR — и настраивать поведение советника с помощью регулируемых пороговых значений. Эти настройки определяют, как система реагирует на скачки спреда, аномальную скорость тиков, разрывы в котировках и нестабильную волатильность до того, как будет разрешено исполнение любой сделки. Вынося эти настройки во входные параметры, мы создаем гибкую основу, которую можно оптимизировать для различных символов, торговых сессий и условий исполнения.

Остальная часть кода реализует внутренний механизм отслеживания состояния рынка, который советник использует для классификации торговых условий и управления данными по каждому символу в режиме реального времени. Мы определяем несколько перечислений, которые описывают текущую рыночную обстановку, торговые сигналы и направление свипа ликвидности, что позволяет системе принимать структурированные решения, а не полагаться на отдельные значения индикаторов. Структуры TickData и SymbolState хранят данные по каждому символу: статистику спреда, тиковую активность, волатильность, оценки проскальзывания, состояние свипа ликвидности и показатели эффективности за сессию. Наконец, с помощью глобальных переменных инициализируются торговые объекты, массивы символов, счетчики эффективности и логика определения работы в тестере, обеспечивающие согласованную работу всей многопарной системы исполнения.

//+------------------------------------------------------------------+
//|  Helper function to detect if running in tester                  |
//+------------------------------------------------------------------+
bool IsTesterMode()
  {
   //--- Check if tester is active
   if(MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_OPTIMIZATION) || MQLInfoInteger(MQL_VISUAL_MODE))
      return true;
   return false;
  }

//+------------------------------------------------------------------+
//|  Log                                                             |
//+------------------------------------------------------------------+
void LOG(int level, string msg)
  {
   if(level <= LogLevel)
      Print(msg);
  }

//+------------------------------------------------------------------+
//|  Get Filling Mode                                                |
//+------------------------------------------------------------------+
ENUM_ORDER_TYPE_FILLING GetFillingMode(string sym)
  {
   if(sym == "" || SymbolInfoInteger(sym, SYMBOL_SELECT) == 0)
      return ORDER_FILLING_RETURN;

   uint filling = (uint)SymbolInfoInteger(sym, SYMBOL_FILLING_MODE);
   if((filling & SYMBOL_FILLING_FOK)    != 0)
      return ORDER_FILLING_FOK;
   if((filling & SYMBOL_FILLING_IOC)    != 0)
      return ORDER_FILLING_IOC;
   return ORDER_FILLING_RETURN;
  }

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Auto-detect tester mode
   IsTesting = IsTesterMode();

   LOG(2, "════════════════════════════════════════════");
   LOG(2, "  Market Microstructure Noise Filter EA     ");
   if(IsTesting)
      LOG(2, "  TESTER MODE - Relaxed Filters Active     ");
   LOG(2, "  Liquidity Sweep Continuation Strategy     ");
   LOG(2, "════════════════════════════════════════════");

   string rawTokens[];
   int rawCount = StringSplit(Symbols, ',', rawTokens);
   if(rawCount == 0 || rawCount > MAX_SYMBOLS)
     {
      LOG(1, "ERROR: Invalid symbol list");
      return INIT_FAILED;
     }

   ArrayResize(SymbolArray, rawCount);
   SymbolCount = 0;
   for(int i = 0; i < rawCount; i++)
     {
      string sym = rawTokens[i];
      StringTrimLeft(sym);
      StringTrimRight(sym);
      if(StringLen(sym) == 0)
         continue;
      SymbolArray[SymbolCount++] = sym;
     }

   if(SymbolCount == 0)
     {
      LOG(1, "ERROR: No valid symbols after trim.");
      return INIT_FAILED;
     }

   Trade.SetExpertMagicNumber(MagicNumber);
   Trade.SetDeviationInPoints(MaxSlippagePoints);

   if(SymbolCount > 0 && SymbolArray[0] != "")
     {
      ENUM_ORDER_TYPE_FILLING filling = GetFillingMode(SymbolArray[0]);
      Trade.SetTypeFilling(filling);
     }
   else
     {
      Trade.SetTypeFilling(ORDER_FILLING_RETURN);
     }

   Trade.SetAsyncMode(false);

   ArrayResize(States, SymbolCount);

   for(int i = 0; i < SymbolCount; i++)
     {
      if(!InitSymbol(i))
        {
         LOG(1, "ERROR: Failed to init symbol: " + SymbolArray[i]);
         return INIT_FAILED;
        }
      LOG(2, " ... " + States[i].symbol +
          "  Digits=" + IntegerToString(States[i].digits) +
          "  AvgSpread≈" + DoubleToString(States[i].avgSpread, 1));
     }

   SessionStart        = TimeCurrent();
   SessionStartBalance = AccountInfoDouble(ACCOUNT_BALANCE);

   //--- Use millisecond timer for faster updates
   if(IsTesting)
      EventSetMillisecondTimer(100);  // 100ms updates in tester
   else
      EventSetTimer(1);

   LOG(2, "═══ EA Online — " + IntegerToString(SymbolCount) + " symbols ═══");
   return INIT_SUCCEEDED;
  }

//+------------------------------------------------------------------+
//| Symbol Initializer                                               |
//+------------------------------------------------------------------+
bool InitSymbol(int idx)
  {
   string sym = SymbolArray[idx];

   States[idx].symbol             = sym;
   States[idx].digits             = 0;
   States[idx].point              = 0;
   States[idx].tickSize           = 0;
   States[idx].tickValue          = 0;
   States[idx].lotStep            = 0;
   States[idx].minLot             = 0;
   States[idx].maxLot             = 0;
   States[idx].contractSize       = 0;
   States[idx].bid                = 0;
   States[idx].ask                = 0;
   States[idx].spread             = 0;
   States[idx].lastTickTime       = 0;
   States[idx].lastTickMS         = 0;
   States[idx].avgSpread          = 0;
   States[idx].spreadStdDev       = 0;
   States[idx].spreadSpikeCount   = 0;
   States[idx].spreadHistoryIdx   = 0;
   States[idx].spreadHistoryCount = 0;
   States[idx].tickVelocity       = 0;
   States[idx].tickCountWindow    = 0;
   States[idx].tickWindowStart    = TimeCurrent();
   States[idx].tickBurstCount     = 0;
   States[idx].lastBid            = 0;
   States[idx].lastAsk            = 0;
   States[idx].maxGapPoints       = 0;
   States[idx].gapEventCount      = 0;
   States[idx].microVolatility    = 0;
   States[idx].slippageEstimate   = 0;
   States[idx].avgSlippage        = 0;
   States[idx].slippageSamples    = 0;
   States[idx].state              = THIN_LIQUIDITY;
   States[idx].noisy              = false;  // Start as not noisy to allow initial trading
   States[idx].tradable           = true;   // Start as tradable
   States[idx].qualityScore       = 70;     // Higher initial score
   States[idx].sweepState         = SWEEP_NONE;
   States[idx].sweepLevel         = 0;
   States[idx].sweepTime          = 0;
   States[idx].noiseSettled       = false;
   States[idx].structureConfirmed = false;
   States[idx].sweepDetectedAt    = 0;
   States[idx].swingHigh          = 0;
   States[idx].swingLow           = 0;
   States[idx].prevSwingHigh      = 0;
   States[idx].prevSwingLow       = 0;
   States[idx].tradesThisSession  = 0;
   States[idx].blockedByNoise     = 0;
   States[idx].sessionPnL         = 0;
   States[idx].tickIdx            = 0;
   States[idx].tickCount          = 0;
   States[idx].atrHandle          = INVALID_HANDLE;
   States[idx].lastCollectTime    = 0;
   States[idx].barsSinceLastTick  = 0;

   if(!SymbolSelect(sym, true))
     {
      LOG(1, "[" + sym + "] Cannot select — not found in broker symbols.");
      return false;
     }

   double testBid = SymbolInfoDouble(sym, SYMBOL_BID);
   if(testBid <= 0)
     {
      LOG(1, "[" + sym + "] Symbol selected but bid=0 — may not be available.");
     }

   States[idx].digits       = (int)SymbolInfoInteger(sym, SYMBOL_DIGITS);
   States[idx].point        = SymbolInfoDouble(sym, SYMBOL_POINT);
   States[idx].tickSize     = SymbolInfoDouble(sym, SYMBOL_TRADE_TICK_SIZE);
   States[idx].tickValue    = SymbolInfoDouble(sym, SYMBOL_TRADE_TICK_VALUE);
   States[idx].lotStep      = SymbolInfoDouble(sym, SYMBOL_VOLUME_STEP);
   States[idx].minLot       = SymbolInfoDouble(sym, SYMBOL_VOLUME_MIN);
   States[idx].maxLot       = SymbolInfoDouble(sym, SYMBOL_VOLUME_MAX);
   States[idx].contractSize = SymbolInfoDouble(sym, SYMBOL_TRADE_CONTRACT_SIZE);

   if(States[idx].point    <= 0)
      States[idx].point    = 0.00001;
   if(States[idx].tickSize <= 0)
      States[idx].tickSize = States[idx].point;
   if(States[idx].lotStep  <= 0)
      States[idx].lotStep  = 0.01;
   if(States[idx].minLot   <= 0)
      States[idx].minLot   = 0.01;
   if(States[idx].maxLot   <= 0)
      States[idx].maxLot   = 100.0;

   //--- Seed spread history with current data
   MqlTick ticks[];
   int loaded = CopyTicks(sym, ticks, COPY_TICKS_ALL, 0, SpreadSamplePeriod);

   if(loaded > 0)
     {
      double spreadSum = 0.0;
      for(int t = 0; t < loaded; t++)
        {
         double sp = 0.0;
         if(States[idx].point > 0)
            sp = (ticks[t].ask - ticks[t].bid) / States[idx].point;
         if(sp < 0)
            sp = 0;
         spreadSum += sp;

         if(States[idx].spreadHistoryCount < MAX_SPREAD_HIST)
           {
            States[idx].spreadHistory[States[idx].spreadHistoryIdx] = sp;
            States[idx].spreadHistoryIdx =
               (States[idx].spreadHistoryIdx + 1) % MAX_SPREAD_HIST;
            States[idx].spreadHistoryCount++;
           }
        }
      States[idx].avgSpread = (loaded > 0) ? spreadSum / loaded : 0;

      //--- Seed tick buffer
      int seedCount = MathMin(loaded, MAX_TICKS);
      for(int t = loaded - seedCount; t < loaded; t++)
        {
         int bi = States[idx].tickIdx;
         States[idx].tickBuffer[bi].bid  = ticks[t].bid;
         States[idx].tickBuffer[bi].ask  = ticks[t].ask;
         States[idx].tickBuffer[bi].time = (datetime)(ticks[t].time / 1000);
         States[idx].tickIdx = (bi + 1) % MAX_TICKS;
         if(States[idx].tickCount < MAX_TICKS)
            States[idx].tickCount++;
        }
     }

   if(States[idx].avgSpread <= 0)
     {
      double liveBid = SymbolInfoDouble(sym, SYMBOL_BID);
      double liveAsk = SymbolInfoDouble(sym, SYMBOL_ASK);
      if(liveBid > 0 && liveAsk > 0 && States[idx].point > 0)
         States[idx].avgSpread = (liveAsk - liveBid) / States[idx].point;
      else
         States[idx].avgSpread = 10.0;
     }

   States[idx].bid      = SymbolInfoDouble(sym, SYMBOL_BID);
   States[idx].ask      = SymbolInfoDouble(sym, SYMBOL_ASK);
   States[idx].lastBid  = States[idx].bid;
   States[idx].lastAsk  = States[idx].ask;
   if(States[idx].point > 0)
      States[idx].spread = (States[idx].ask - States[idx].bid) / States[idx].point;

   States[idx].atrHandle = iATR(sym, PERIOD_H1, ATRPeriod);
   if(States[idx].atrHandle == INVALID_HANDLE)
      LOG(2, "[" + sym + "] Warning: could not create ATR handle (may be OK in tester).");

   UpdateSwingLevels(idx);

   return true;
  }

Этап инициализации начинается с подготовки среды, в которой будет работать EA. Сначала мы создаем вспомогательные функции, которые определяют, запущен ли советник в тестере стратегий, управляют выводом сообщений в журнал и выбирают подходящий тип исполнения ордеров, поддерживаемый брокером. Эти функции помогают системе автоматически адаптировать свое поведение в зависимости от того, происходит ли реальная торговля или тестирование. В функции OnInit() мы выводим информацию о запуске, разбираем список символов, удаляем пробелы по краям и проверяем количество инструментов. После проверки символов мы настраиваем торговый механизм, задавая выбранный magic number, параметры проскальзывания и тип исполнения ордеров, а затем выделяем память для хранения состояний всех символов. Это обеспечивает четкий и упорядоченный процесс запуска, благодаря которому советник полностью готов к работе до начала обработки рыночных данных.

Затем функция InitSymbol() формирует внутреннюю модель данных для каждого отслеживаемого символа отдельно. Мы инициализируем всю статистику исполнения, показатели спреда, состояния свипа ликвидности, показатели волатильности, буферы тиков и переменные отслеживания торговых сессий, чтобы каждый инструмент начинал работу со стабильной базовой конфигурацией. Кроме того, эта функция проверяет доступность инструмента у брокера с помощью SymbolSelect(), получает важные торговые параметры, такие как размер пункта, ограничения объема в лотах, стоимость тика и размер контракта, и подставляет значения по умолчанию, если какие-либо данные отсутствуют. Чтобы с самого начала учитывать условия исполнения, мы предварительно загружаем исторические тиковые данные с помощью CopyTicks() и используем их для начального заполнения истории спреда и буферов тиков, на которых основан механизм фильтрации шума. В заключение мы рассчитываем начальные параметры спреда, создаем дескриптор индикатора ATR для анализа волатильности, обновляем уровни локальных экстремумов, используемые стратегией свипа ликвидности, и подготавливаем символ к мониторингу и исполнению в режиме реального времени.

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   EventKillTimer();

   for(int i = 0; i < SymbolCount; i++)
     {
      if(States[i].atrHandle != INVALID_HANDLE)
        {
         IndicatorRelease(States[i].atrHandle);
         States[i].atrHandle = INVALID_HANDLE;
        }
     }

   PrintSessionStats();
   LOG(2, "EA deinitialized. Reason: " + IntegerToString(reason));
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   //--- Collect ticks for ALL symbols on every chart tick
   for(int i = 0; i < SymbolCount; i++)
     {
      CollectTick(i);
     }
  }

//+------------------------------------------------------------------+
//|  Tick Collector                                                  |
//+------------------------------------------------------------------+
void CollectTick(int idx)
  {
   //--- Refresh symbol data (critical for tester mode)
   SymbolInfoDouble(States[idx].symbol, SYMBOL_BID);  // Force refresh

   double newBid = SymbolInfoDouble(States[idx].symbol, SYMBOL_BID);
   double newAsk = SymbolInfoDouble(States[idx].symbol, SYMBOL_ASK);
   if(newBid <= 0 || newAsk <= 0)
      return;

   //--- Skip if same as last tick (avoid duplicates)
   if(newBid == States[idx].bid && newAsk == States[idx].ask)
      return;

   States[idx].lastCollectTime = TimeCurrent();

   //--- Push into circular buffer
   int bi = States[idx].tickIdx;
   States[idx].tickBuffer[bi].bid  = newBid;
   States[idx].tickBuffer[bi].ask  = newAsk;
   States[idx].tickBuffer[bi].time = TimeCurrent();
   States[idx].tickBuffer[bi].ms   = GetTickCount64();
   States[idx].tickIdx = (bi + 1) % MAX_TICKS;
   if(States[idx].tickCount < MAX_TICKS)
      States[idx].tickCount++;

   //--- Live spread history
   double sp = 0;
   if(States[idx].point > 0)
      sp = (newAsk - newBid) / States[idx].point;

   States[idx].spreadHistory[States[idx].spreadHistoryIdx] = sp;
   States[idx].spreadHistoryIdx =
      (States[idx].spreadHistoryIdx + 1) % MAX_SPREAD_HIST;
   if(States[idx].spreadHistoryCount < MAX_SPREAD_HIST)
      States[idx].spreadHistoryCount++;

   States[idx].lastBid       = States[idx].bid;
   States[idx].lastAsk       = States[idx].ask;
   States[idx].bid           = newBid;
   States[idx].ask           = newAsk;
   States[idx].spread        = sp;
   States[idx].lastTickTime  = TimeCurrent();

   States[idx].tickCountWindow++;
  }

//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer()
  {
   if(CheckGlobalDrawdown())
      return;

   //--- In tester mode, actively collect ticks for all symbols every timer event
   if(IsTesting)
     {
      for(int i = 0; i < SymbolCount; i++)
        {
         //--- Force collect regardless of chart
         CollectTick(i);
        }
     }

   for(int i = 0; i < SymbolCount; i++)
     {
      //--- Update statistics
      UpdateSpreadStats(i);
      UpdateTickVelocity(i);
      UpdateQuoteGap(i);
      UpdateMicroVolatility(i);
      UpdateSlippageEstimate(i);

      //--- Classify state
      ClassifyMarketState(i);

      //--- Noise decision with tester overrides
      States[i].noisy    = IsNoisyTesterAware(i);
      States[i].tradable = !States[i].noisy;

      //--- Quality score
      UpdateQualityScore(i);

      //--- Strategy
      if(States[i].tradable)
        {
         UpdateSwingLevels(i);
         DetectLiquiditySweep(i);
         CheckNoiseSettlement(i);
         CheckStructureShift(i);

         if(States[i].sweepState    != SWEEP_NONE &&
            States[i].noiseSettled               &&
            States[i].structureConfirmed)
           {
            TotalSignals++;
            SignalType sig = GetSignal(i);
            if(sig != SIGNAL_NONE)
              {
               if(ValidateRisk(i, sig))
                  ExecuteTrade(i, sig);
               else
                 {
                  TotalFiltered++;
                  LOG(2, "[" + States[i].symbol + "] Signal blocked by Risk Manager");
                 }
              }
           }
        }
      else
        {
         States[i].blockedByNoise++;
         if(EnableDetailedLog && States[i].blockedByNoise % 30 == 1)
            LogNoiseBlock(i);
        }
     }
  }

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

Функция CollectTick() обновляет внутренние буферы тиков и историю спреда, на которых основан механизм анализа микроструктуры рынка. Мы обновляем цены спроса и предложения, отбрасываем дубликаты тиков и сохраняем новые рыночные данные в кольцевых буферах для эффективного отслеживания рынка с высокой частотой. Одновременно система обновляет показатели спреда, счетчики тиков и временные метки, используемые для анализа волатильности и исполнения.

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

//+------------------------------------------------------------------+
//|  TESTER-AWARE NOISE FILTER                                       |
//+------------------------------------------------------------------+
bool IsNoisyTesterAware(int idx)
  {
   if(!EnableNoiseFilter)
      return false;

   //--- Use relaxed thresholds in tester mode
   double effectiveSpreadMult = IsTesting ? TesterSpreadMult : MaxSpreadMultiplier;
   double effectiveMaxGap     = IsTesting ? TesterMaxGapPoints : MaxGapPoints;
   double effectiveMinVol     = IsTesting ? TesterMinVolatility : MinVolatilityPoints;
   double effectiveMaxVol     = IsTesting ? TesterMaxVolatility : MaxVolatilityPoints;
   double effectiveMaxVel     = IsTesting ? MaxTickVelocity * 2 : MaxTickVelocity;  // Double velocity in tester

   //--- A. Spread expansion
   if(States[idx].spread > States[idx].avgSpread * effectiveSpreadMult)
     {
      LOG(3, "[" + States[idx].symbol + "] NOISE: Spread " +
          DoubleToString(States[idx].spread, 1) + " vs avg " +
          DoubleToString(States[idx].avgSpread, 1));
      return true;
     }

   //--- B. Tick velocity burst (skip)
   if(!IsTesting || States[idx].tickCountWindow > 5)  // Only check if we have data
     {
      if(States[idx].tickVelocity > effectiveMaxVel)
        {
         LOG(3, "[" + States[idx].symbol + "] NOISE: TickVel " +
             DoubleToString(States[idx].tickVelocity, 1) + "/s");
         return true;
        }
     }

   //--- C. Quote gap (relaxed)
   if(States[idx].point > 0 && States[idx].lastBid > 0)
     {
      double gap = MathAbs(States[idx].bid - States[idx].lastBid) /
                   States[idx].point;
      if(gap > effectiveMaxGap)
        {
         LOG(3, "[" + States[idx].symbol + "] NOISE: Gap " +
             DoubleToString(gap, 1) + " pts");
         return true;
        }
     }

   //--- D. Micro-vol too low (skip)
   if(!IsTesting || States[idx].tickCount >= VolatilitySampleTicks)
     {
      if(States[idx].microVolatility < effectiveMinVol)
        {
         LOG(3, "[" + States[idx].symbol + "] NOISE: MicroVol low " +
             DoubleToString(States[idx].microVolatility, 2));
         return true;
        }
     }

   //--- E. Micro-vol too high
   if(States[idx].microVolatility > effectiveMaxVol)
     {
      LOG(3, "[" + States[idx].symbol + "] NOISE: MicroVol high " +
          DoubleToString(States[idx].microVolatility, 2));
      return true;
     }

   //--- F. Execution instability
   if(!IsTesting && States[idx].slippageEstimate > SlippageStabilityFactor)
     {
      LOG(3, "[" + States[idx].symbol + "] NOISE: Slippage est=" +
          DoubleToString(States[idx].slippageEstimate, 3));
      return true;
     }

   //--- G. State-level block
   if(States[idx].state == HIGH_RISK_NEWS ||
      States[idx].state == EXECUTION_UNSTABLE)
      return true;

   return false;
  }

//+------------------------------------------------------------------+
//|  ANALYSIS FUNCTIONS                                              |
//+------------------------------------------------------------------+
void UpdateSpreadStats(int idx)
  {
   int n = States[idx].spreadHistoryCount;
   if(n < 5)
      return;

   double sum = 0.0;
   for(int i = 0; i < n; i++)
      sum += States[idx].spreadHistory[i];
   States[idx].avgSpread = sum / n;

   double varSum = 0.0;
   for(int i = 0; i < n; i++)
     {
      double d = States[idx].spreadHistory[i] - States[idx].avgSpread;
      varSum += d * d;
     }
   States[idx].spreadStdDev = MathSqrt(varSum / n);

   if(States[idx].spread > States[idx].avgSpread * MaxSpreadMultiplier)
      States[idx].spreadSpikeCount++;
  }

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

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

//+------------------------------------------------------------------+
//|  Update Tick Velocity                                            |
//+------------------------------------------------------------------+
void UpdateTickVelocity(int idx)
  {
   datetime now     = TimeCurrent();
   double   elapsed = (double)(now - States[idx].tickWindowStart);
   if(elapsed <= 0)
      elapsed = 1.0;

   States[idx].tickVelocity = States[idx].tickCountWindow / elapsed;

   if(States[idx].tickVelocity > MaxTickVelocity)
      States[idx].tickBurstCount++;

   States[idx].tickCountWindow = 0;
   States[idx].tickWindowStart = now;
  }

//+------------------------------------------------------------------+
//|  Update Quote Gap                                                |
//+------------------------------------------------------------------+
void UpdateQuoteGap(int idx)
  {
   if(States[idx].lastBid <= 0 || States[idx].point <= 0)
      return;

   double gapPts = MathAbs(States[idx].bid - States[idx].lastBid)
                   / States[idx].point;
   if(gapPts > States[idx].maxGapPoints)
      States[idx].maxGapPoints = gapPts;

   if(gapPts > MaxGapPoints)
     {
      States[idx].gapEventCount++;
      LOG(3, "[" + States[idx].symbol + "] Quote gap: " +
          DoubleToString(gapPts, 1) + " pts");
     }
  }

//+------------------------------------------------------------------+
//|  Update Micro Vol                                                |
//+------------------------------------------------------------------+
void UpdateMicroVolatility(int idx)
  {
   int n = MathMin(States[idx].tickCount, VolatilitySampleTicks);
   if(n < 5)
      return;

   double prices[];
   ArrayResize(prices, n);
   int start = (States[idx].tickIdx - n + MAX_TICKS) % MAX_TICKS;
   for(int i = 0; i < n; i++)
     {
      int bi = (start + i) % MAX_TICKS;
      prices[i] = (States[idx].tickBuffer[bi].bid +
                   States[idx].tickBuffer[bi].ask) / 2.0;
     }

   double returns[];
   ArrayResize(returns, n - 1);
   double pt = States[idx].point;
   if(pt <= 0)
      pt = 0.00001;
   for(int i = 0; i < n - 1; i++)
      returns[i] = (prices[i+1] - prices[i]) / pt;

   double mean = 0.0;
   for(int i = 0; i < n - 1; i++)
      mean += returns[i];
   mean /= (n - 1);

   double var = 0.0;
   for(int i = 0; i < n - 1; i++)
     {
      double d = returns[i] - mean;
      var += d * d;
     }
   States[idx].microVolatility = (n > 2) ? MathSqrt(var / (n - 1)) : 0;
  }

//+------------------------------------------------------------------+
//|  Update Slippage Estimate                                               |
//+------------------------------------------------------------------+
void UpdateSlippageEstimate(int idx)
  {
   if(States[idx].avgSpread < 0.5)
      return;
   double spreadFactor   = States[idx].spread /
                           MathMax(States[idx].avgSpread, 1.0);
   double velocityFactor = States[idx].tickVelocity /
                           MathMax(MaxTickVelocity, 1.0);
   double volFactor      = States[idx].microVolatility /
                           MathMax(MaxVolatilityPoints, 1.0);

   States[idx].slippageEstimate =
      (spreadFactor + velocityFactor + volFactor) / 3.0;
  }

Первая группа функций предназначена для измерения рыночной активности в режиме реального времени и выявления нестабильности исполнения. В функции UpdateTickVelocity() мы рассчитываем скорость тиков, деля количество собранных тиков на время, прошедшее в пределах окна мониторинга. Это позволяет нам получить оперативную оценку интенсивности рыночной активности по каждому инструменту. Если скорость тиков превышает заданный порог, система фиксирует событие "всплеска тиков", что может свидетельствовать об агрессивном потоке ордеров, волатильности, вызванной новостями, или нестабильной ликвидности.

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

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

//+------------------------------------------------------------------+
//|  Classify Market State                                           |
//+------------------------------------------------------------------+
void ClassifyMarketState(int idx)
  {
   bool spreadSpike  = States[idx].spread >
                       States[idx].avgSpread * MaxSpreadMultiplier;
   bool tickBurst    = States[idx].tickVelocity > MaxTickVelocity;
   bool quoteGap     = States[idx].point > 0 &&
                       MathAbs(States[idx].bid - States[idx].lastBid) /
                       States[idx].point > MaxGapPoints;
   bool lowTick      = States[idx].tickVelocity < 0.01;
   bool highSlippage = States[idx].slippageEstimate > SlippageStabilityFactor;
   bool choppyVol    = States[idx].microVolatility < MinVolatilityPoints ||
                       States[idx].microVolatility > MaxVolatilityPoints;

   if(spreadSpike && tickBurst)
      States[idx].state = HIGH_RISK_NEWS;
   else
      if(highSlippage || spreadSpike)
         States[idx].state = EXECUTION_UNSTABLE;
      else
         if(tickBurst && quoteGap)
            States[idx].state = HIGH_RISK_NEWS;
         else
            if(lowTick)
               States[idx].state = THIN_LIQUIDITY;
            else
               if(choppyVol)
                  States[idx].state = NOISY_CHOP;
               else
                  if(States[idx].microVolatility > MinVolatilityPoints &&
                     !spreadSpike && !tickBurst)
                    {
                     double atr = GetATR(idx);
                     States[idx].state = (atr > 0 &&
                                          States[idx].microVolatility > atr * 0.5)
                                         ? CLEAN_BREAKOUT : CLEAN_TREND;
                    }
                  else
                     States[idx].state = NOISY_CHOP;
  }

//+------------------------------------------------------------------+
//|  Update Score                                                    |
//+------------------------------------------------------------------+
void UpdateQualityScore(int idx)
  {
   int score = 70;  // Start with high score
   if(IsTesting)
      score = 75;

   double spreadRatio = (States[idx].avgSpread > 0)
                        ? States[idx].spread / States[idx].avgSpread
                        : 1.0;
   if(spreadRatio > 1.0)
      score -= (int)MathMin(30.0, (spreadRatio - 1.0) * 30.0);

   double velRatio = States[idx].tickVelocity / MathMax(MaxTickVelocity, 1.0);
   if(velRatio > 0.5)
      score -= (int)MathMin(20.0, velRatio * 20.0);

   double volMid   = (MinVolatilityPoints + MaxVolatilityPoints) / 2.0;
   double volRange = (MaxVolatilityPoints - MinVolatilityPoints) / 2.0;
   double volDist  = MathAbs(States[idx].microVolatility - volMid) /
                     MathMax(volRange, 1.0);
   score -= (int)MathMin(25.0, volDist * 25.0);

   score -= (int)MathMin(15.0, States[idx].slippageEstimate * 10.0);

   switch(States[idx].state)
     {
      case CLEAN_TREND:
         score += 10;
         break;
      case CLEAN_BREAKOUT:
         score += 5;
         break;
      case NOISY_CHOP:
         score -= 15;
         break;
      case HIGH_RISK_NEWS:
         score -= 40;
         break;
      case THIN_LIQUIDITY:
         score -= 20;
         break;
      case EXECUTION_UNSTABLE:
         score -= 35;
         break;
     }

   States[idx].qualityScore = (int)MathMax(0.0, MathMin(100.0, (double)score));
  }

//+------------------------------------------------------------------+
//|  Update Swings                                                   |
//+------------------------------------------------------------------+
void UpdateSwingLevels(int idx)
  {
   MqlRates rates[];
   ArraySetAsSeries(rates, true);
   int copied = CopyRates(States[idx].symbol, PERIOD_H1,
                          0, SwingLookback + 5, rates);
   if(copied < SwingLookback + 3)
      return;

   double highVal = 0.0, lowVal = DBL_MAX;
   double prevHighVal = 0.0, prevLowVal = DBL_MAX;
   bool   foundHigh = false, foundLow = false;

   for(int i = 1; i < SwingLookback && i < copied - 1; i++)
     {
      if(rates[i].high > rates[i-1].high && rates[i].high > rates[i+1].high)
        {
         if(!foundHigh)
           {
            highVal = rates[i].high;
            foundHigh = true;
           }
         else
            if(prevHighVal == 0.0)
               prevHighVal = rates[i].high;
        }
      if(rates[i].low < rates[i-1].low && rates[i].low < rates[i+1].low)
        {
         if(!foundLow)
           {
            lowVal = rates[i].low;
            foundLow = true;
           }
         else
            if(prevLowVal == DBL_MAX)
               prevLowVal = rates[i].low;
        }
      if(foundHigh && foundLow && prevHighVal > 0 && prevLowVal < DBL_MAX)
         break;
     }

   if(foundHigh)
     {
      States[idx].prevSwingHigh = (States[idx].swingHigh > 0)
                                  ? States[idx].swingHigh : highVal;
      States[idx].swingHigh     = highVal;
     }
   if(foundLow && lowVal < DBL_MAX)
     {
      States[idx].prevSwingLow = (States[idx].swingLow > 0)
                                 ? States[idx].swingLow : lowVal;
      States[idx].swingLow     = lowVal;
     }
   if(prevHighVal > 0)
      States[idx].prevSwingHigh = prevHighVal;
   if(prevLowVal < DBL_MAX)
      States[idx].prevSwingLow  = prevLowVal;
  }

Функция ClassifyMarketState() выступает в качестве механизма принятия решений, который анализирует текущую рыночную ситуацию на основе статистических данных об исполнении ордеров, ранее собранных системой. Мы анализируем ряд микроструктурных сигналов, поступающих в режиме реального времени, в том числе расширение спреда, всплески тиков, разрывы в котировках, активность в условиях низкой ликвидности, нестабильность проскальзывания и аномальное поведение волатильности. На основании совокупности этих условий советник классифицирует рынок по заранее заданным состояниям, таким как HIGH_RISK_NEWS, EXECUTION_UNSTABLE, THIN_LIQUIDITY, NOISY_CHOP, CLEAN_BREAKOUT или CLEAN_TREND. Этот процесс классификации позволяет торговой системе определить, является ли рыночная обстановка безопасной для исполнения сделок или слишком нестабильной для надёжной торговли. Вместо того чтобы одинаково реагировать на все рыночные условия, советник динамически адаптирует своё поведение в зависимости от текущего качества исполнения и условий ликвидности.

Остальные функции позволяют ещё больше уточнить модель исполнения за счёт присвоения оценок качества и отслеживания уровней рыночной структуры, используемых в стратегии свипа ликвидности. В функции UpdateQualityScore() мы создаём динамическую систему оценки, которая повышает балл за стабильные спреды, контролируемую волатильность и более "чистые" состояния рынка и снижает его за нестабильные условия исполнения, такие как чрезмерное проскальзывание, "шумная" волатильность и высокорисковое поведение рынка на фоне новостей. В результате получается числовой показатель качества в диапазоне от 0 до 100, который отражает текущую торговую конъюнктуру для каждого символа. Затем функция UpdateSwingLevels() анализирует данные последних свечей H1 для выявления локальных максимумов и минимумов, которые представляют собой важные зоны ликвидности на рынке. Благодаря сохранению как текущих, так и предыдущих уровней локальных экстремумов советник может впоследствии выявлять свипы ликвидности, изменения структуры рынка и возможности для продолжения движения, которые составляют основу торговой стратегии.

//+------------------------------------------------------------------+
//|  Detect Liquidity Sweep                                          |
//+------------------------------------------------------------------+
void DetectLiquiditySweep(int idx)
  {
   if(States[idx].swingHigh <= 0 || States[idx].swingLow <= 0)
      return;

   double   tolerance = SweepTolerancePoints * States[idx].point;
   datetime now       = TimeCurrent();

   if(States[idx].sweepState != SWEEP_NONE &&
      (now - States[idx].sweepDetectedAt) < (datetime)(NoiseSettleSeconds * 4))
      return;

   if(States[idx].ask > States[idx].swingHigh &&
      States[idx].bid < States[idx].swingHigh + tolerance * 20.0)
     {
      States[idx].sweepState        = SWEEP_HIGH;
      States[idx].sweepLevel        = States[idx].swingHigh;
      States[idx].sweepTime         = now;
      States[idx].sweepDetectedAt   = now;
      States[idx].noiseSettled      = false;
      States[idx].structureConfirmed = false;

      LOG(2, "[" + States[idx].symbol + "] Sweep HIGH @ " +
          DoubleToString(States[idx].sweepLevel, States[idx].digits));
      return;
     }

   if(States[idx].bid < States[idx].swingLow &&
      States[idx].ask > States[idx].swingLow - tolerance * 20.0)
     {
      States[idx].sweepState        = SWEEP_LOW;
      States[idx].sweepLevel        = States[idx].swingLow;
      States[idx].sweepTime         = now;
      States[idx].sweepDetectedAt   = now;
      States[idx].noiseSettled      = false;
      States[idx].structureConfirmed = false;

      LOG(2, "[" + States[idx].symbol + "] Sweep LOW @ " +
          DoubleToString(States[idx].sweepLevel, States[idx].digits));
     }
  }

//+------------------------------------------------------------------+
//|  Check Noise Filters                                             |
//+------------------------------------------------------------------+
void CheckNoiseSettlement(int idx)
  {
   if(States[idx].sweepState == SWEEP_NONE)
      return;
   if(States[idx].noiseSettled)
      return;

   //--- In tester mode, settle immediately after 1 bar
   int requiredBars = IsTesting ? 1 : 1;

   datetime barTime = iTime(States[idx].symbol, PERIOD_CURRENT, 0);
   int barsElapsed  = Bars(States[idx].symbol, PERIOD_CURRENT,
                           States[idx].sweepDetectedAt, barTime);
   if(barsElapsed < requiredBars)
      return;

   double spreadLimit = IsTesting ? TesterSpreadMult : 1.8;
   bool spreadOK   = (States[idx].avgSpread <= 0) ||
                     (States[idx].spread <= States[idx].avgSpread * spreadLimit);
   bool velocityOK = IsTesting ? true : States[idx].tickVelocity < MaxTickVelocity;
   bool volOK      = IsTesting ? true :
                     (States[idx].tickCount >= VolatilitySampleTicks &&
                      States[idx].microVolatility >= MinVolatilityPoints * 0.5 &&
                      States[idx].microVolatility <= MaxVolatilityPoints);

   if(spreadOK && velocityOK && volOK)
     {
      States[idx].noiseSettled = true;
      LOG(2, "[" + States[idx].symbol + "] Noise settled (" +
          IntegerToString(barsElapsed) + " bars after sweep)");
     }
  }

//+------------------------------------------------------------------+
//|  Structural Shift                                                 |
//+------------------------------------------------------------------+
void CheckStructureShift(int idx)
  {
   if(States[idx].sweepState    == SWEEP_NONE)
      return;
   if(!States[idx].noiseSettled)
      return;
   if(States[idx].structureConfirmed)
      return;

   ENUM_TIMEFRAMES tf = PERIOD_CURRENT;
   MqlRates rates[];
   ArraySetAsSeries(rates, true);
   int needed = StructureConfirmBars + 4;
   int copied = CopyRates(States[idx].symbol, tf, 0, needed, rates);
   if(copied < needed)
     {
      tf = PERIOD_H1;
      copied = CopyRates(States[idx].symbol, tf, 0, needed, rates);
      if(copied < needed)
         return;
     }

   if(States[idx].sweepState == SWEEP_LOW)
     {
      double refHigh = 0.0;
      for(int i = 1; i <= StructureConfirmBars; i++)
         refHigh = MathMax(refHigh, rates[i].high);

      if(refHigh > 0 && rates[0].close > refHigh)
        {
         States[idx].structureConfirmed = true;
         LOG(2, "[" + States[idx].symbol + "] BUY structure confirmed (LOW sweep)");
        }
     }
   else
      if(States[idx].sweepState == SWEEP_HIGH)
        {
         double refLow = DBL_MAX;
         for(int i = 1; i <= StructureConfirmBars; i++)
            refLow = MathMin(refLow, rates[i].low);

         if(refLow < DBL_MAX && rates[0].close < refLow)
           {
            States[idx].structureConfirmed = true;
            LOG(2, "[" + States[idx].symbol + "] SELL structure confirmed (HIGH sweep)");
           }
        }
  }

Этап выявления ликвидности предназначен для обнаружения потенциальных сценариев охоты за стопами и захвата ликвидности вблизи важных уровней локальных экстремумов на рынке. Внутри функции DetectLiquiditySweep() мы сначала проверяем наличие корректно определённых локальных максимумов и минимумов, а затем отслеживаем, не выходит ли цена временно за пределы этих уровней. Если цена Ask поднимается выше локального максимума или цена Bid опускается ниже локального минимума в пределах допустимого отклонения, советник фиксирует свип ликвидности и записывает его направление, ценовой уровень и время обнаружения. Одновременно система сбрасывает флаги затухания шума и подтверждения структуры, поскольку одного свипа ликвидности недостаточно для обоснования сделки. Этот процесс позволяет стратегии выявлять области, в которых участники рынка могли задействовать ликвидность размещённых заявок перед возможным продолжением направленного движения.

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

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

//+------------------------------------------------------------------+
//|  Signal Type                                                     |
//+------------------------------------------------------------------+
SignalType GetSignal(int idx)
  {
   if(States[idx].sweepState    == SWEEP_NONE)
      return SIGNAL_NONE;
   if(!States[idx].noiseSettled)
      return SIGNAL_NONE;
   if(!States[idx].structureConfirmed)
      return SIGNAL_NONE;

   if(TimeCurrent() - States[idx].sweepDetectedAt > 14400)
     {
      ResetSweepState(idx);
      return SIGNAL_NONE;
     }

   if(CountPositions(States[idx].symbol) > 0)
      return SIGNAL_NONE;

   SignalType sig = SIGNAL_NONE;
   if(States[idx].sweepState == SWEEP_LOW  && EnableBuySignals)
      sig = SIGNAL_BUY;
   if(States[idx].sweepState == SWEEP_HIGH && EnableSellSignals)
      sig = SIGNAL_SELL;

   //--- Lower score threshold in tester
   int scoreThreshold = IsTesting ? 50 : 55;
   if(sig != SIGNAL_NONE && States[idx].qualityScore < scoreThreshold)
     {
      LOG(2, "[" + States[idx].symbol + "] Score too low (" +
          IntegerToString(States[idx].qualityScore) + ") — skip");
      return SIGNAL_NONE;
     }

   if(sig != SIGNAL_NONE)
      LOG(2, "[" + States[idx].symbol + "] Signal: " +
          (sig == SIGNAL_BUY ? "BUY" : "SELL") +
          "  Score=" + IntegerToString(States[idx].qualityScore));

   return sig;
  }

void ResetSweepState(int idx)
  {
   States[idx].sweepState         = SWEEP_NONE;
   States[idx].sweepLevel         = 0;
   States[idx].noiseSettled       = false;
   States[idx].structureConfirmed = false;
  }

//+------------------------------------------------------------------+
//|  RISK & EXECUTION                                                |
//+------------------------------------------------------------------+
bool ValidateRisk(int idx, SignalType sig)
  {
   string sym = States[idx].symbol;

   if(CountAllPositions() >= MaxTotalPositions)
     {
      LOG(2, "[" + sym + "] Risk: max positions");
      return false;
     }

   if(AccountInfoDouble(ACCOUNT_MARGIN_FREE) < 100)
     {
      LOG(1, "[" + sym + "] Risk: low margin");
      return false;
     }

   return true;
  }

//+------------------------------------------------------------------+
//|  Calculate Lots                                                  |
//+------------------------------------------------------------------+
double CalcLotSize(int idx)
  {
   double balance  = AccountInfoDouble(ACCOUNT_BALANCE);
   double atr      = GetATR(idx);
   double pt       = States[idx].point;
   if(pt  <= 0)
      pt  = 0.00001;
   if(atr <= 0)
      atr = 50 * pt;

   double slDist    = atr * StopLossATRMultiplier;
   double riskAmt   = balance * (RiskPercent / 100.0);
   double tickVal   = States[idx].tickValue;
   double tickSz    = States[idx].tickSize;
   double pipValue  = (tickSz > 0 && tickVal > 0)
                      ? (tickVal / tickSz) * pt
                      : 10.0;
   if(pipValue <= 0)
      pipValue = 10.0;

   double slPips = slDist / pt;
   if(slPips <= 0)
      slPips = 1.0;
   double rawLot = riskAmt / (slPips * pipValue);

   rawLot = MathFloor(rawLot / States[idx].lotStep) * States[idx].lotStep;
   rawLot = MathMax(States[idx].minLot, MathMin(States[idx].maxLot, rawLot));
   return NormalizeDouble(rawLot, 2);
  }

//+------------------------------------------------------------------+
//|  Check Drawdown                                                  |
//+------------------------------------------------------------------+
bool CheckGlobalDrawdown()
  {
   double balance = AccountInfoDouble(ACCOUNT_BALANCE);
   double equity  = AccountInfoDouble(ACCOUNT_EQUITY);
   if(balance <= 0)
      return false;

   double ddPct = ((balance - equity) / balance) * 100.0;
   if(ddPct >= MaxDrawdownPercent)
     {
      LOG(1, "GLOBAL DD HALT: " + DoubleToString(ddPct, 2) + "%");
      return true;
     }
   return false;
  }

//+------------------------------------------------------------------+
//|  Execute Trade                                                   |
//+------------------------------------------------------------------+
void ExecuteTrade(int idx, SignalType sig)
  {
   string sym = States[idx].symbol;

   double atr = GetATR(idx);
   if(atr <= 0)
     {
      LOG(1, "[" + sym + "] No ATR — skip");
      return;
     }

   double slDist  = atr * StopLossATRMultiplier;
   double tpDist  = slDist * TakeProfitRR;
   double lots    = CalcLotSize(idx);
   int    digits  = States[idx].digits;
   double point   = States[idx].point;
   int    stopLvl = (int)SymbolInfoInteger(sym, SYMBOL_TRADE_STOPS_LEVEL);
   double minDist = stopLvl * point;

   slDist = MathMax(slDist, minDist + 2 * point);
   tpDist = MathMax(tpDist, minDist + 2 * point);

   for(int attempt = 0; attempt < MaxRetries; attempt++)
     {
      double freshBid = SymbolInfoDouble(sym, SYMBOL_BID);
      double freshAsk = SymbolInfoDouble(sym, SYMBOL_ASK);
      double price = 0, sl = 0, tp = 0;

      if(sig == SIGNAL_BUY)
        {
         price = freshAsk;
         sl    = NormalizeDouble(price - slDist, digits);
         tp    = NormalizeDouble(price + tpDist, digits);

         if(Trade.Buy(lots, sym, price, sl, tp,
                      "MMEF_BUY|Sc=" + IntegerToString(States[idx].qualityScore)))
           {
            TotalExecuted++;
            States[idx].tradesThisSession++;
            ResetSweepState(idx);
            LOG(2, "[" + sym + "] BUY executed! Lots=" + DoubleToString(lots,2));
            break;
           }
        }
      else
        {
         price = freshBid;
         sl    = NormalizeDouble(price + slDist, digits);
         tp    = NormalizeDouble(price - tpDist, digits);

         if(Trade.Sell(lots, sym, price, sl, tp,
                       "MMEF_SELL|Sc=" + IntegerToString(States[idx].qualityScore)))
           {
            TotalExecuted++;
            States[idx].tradesThisSession++;
            ResetSweepState(idx);
            LOG(2, "[" + sym + "] SELL executed! Lots=" + DoubleToString(lots,2));
            break;
           }
        }

      LOG(1, "[" + sym + "] Attempt " + IntegerToString(attempt+1) + " failed. Code=" +
          IntegerToString((int)Trade.ResultRetcode()));
      Sleep(RetryDelayMS);
     }
  }

//+------------------------------------------------------------------+

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

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

Уровень управления рисками и исполнения сделок направлен на защиту счета при одновременном обеспечении стабильного размещения торговых ордеров. В функции ValidateRisk() мы проверяем, что на счете имеется достаточный объем свободной маржи и что общее количество открытых позиций не превышает заданных лимитов. Затем функция CalcLotSize() динамически рассчитывает размер позиции с учетом баланса счета, расстояния до стопа, рассчитанного на основе ATR, стоимости тика и процентной доли риска. Это позволяет советнику автоматически адаптировать размер лота в зависимости от волатильности инструмента и состояния счета.

Функция CheckGlobalDrawdown() выступает в качестве механизма защиты портфеля, приостанавливая торговую деятельность в случае, если просадка счета превышает максимально допустимый процент. Наконец, функция ExecuteTrade() рассчитывает уровни SL/TP на основе ATR, обновляет цены, проверяет ограничения брокера на установку стоп-уровней и отправляет ордер, при необходимости повторяя попытки. После успешного исполнения сделки система фиксирует это событие, обновляет статистику сессии и сбрасывает состояние активного свипа ликвидности для данного инструмента.


Бэктест

Бэктест проводился в течение примерно двухмесячного периода с 2 февраля 2026 года по 1 апреля 2026 года с использованием настроек по умолчанию:


Заключение

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

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

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


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

Прикрепленные файлы |
Market_Micro.mq5 (48.28 KB)
Метаразметка классических стратегий (Часть 2): Фильтрация сделок по ADX и определение размера позиций Метаразметка классических стратегий (Часть 2): Фильтрация сделок по ADX и определение размера позиций
Пересечение линий DI часто дает сигналы на боковом рынке, где +DI и -DI колеблются без устойчивого преобладания одной из линий. Мы создаем двухслойную гибридную модель: алгоритм выбора гиперпараметров TPE в Optuna оптимизирует фильтр рыночного режима по порогу ADXR, периоду расчета DI и минимальному расстоянию между линиями DI, чтобы максимизировать точность (precision) сигналов на отложенном валидационном интервале; затем случайный лес использует одиннадцать признаков, рассчитанных на основе ADX, для допуска входов в рынок или определения размера позиций с помощью afml.bet_sizing. Полученная модель отфильтровывает всплески сигналов на боковом рынке и калибрует размер позиции при торговле EURUSD на H1.
Конструирование признаков для машинного обучения (Часть 6): Признаки микроструктуры рынка в MQL5 Конструирование признаков для машинного обучения (Часть 6): Признаки микроструктуры рынка в MQL5
В статье представлен CMicrostructureFeatures — класс на языке MQL5 для расчета признаков микроструктуры рынка на уровне баров: спреда Ролла и ценового воздействия по Роллу, спреда и сигмы Корвина — Шульца, лямбды Кайла, показателя неликвидности Амихуда (ILLIQ) и лямбды Хасбрука. Расчеты основываются исключительно на данных OHLCV с использованием скользящих окон. В статье разъясняется, как использование тикового объема MetaTrader 5 влияет на оценки лямбды; при этом оценки спреда остаются независимыми от объема. Скрипт проверяет размеры выходных данных и соблюдение базовых ограничений на их значения.
Индикатор скользящего коэффициента Шарпа с доверительными границами в MQL5 Индикатор скользящего коэффициента Шарпа с доверительными границами в MQL5
В данной статье представлен пользовательский индикатор для MetaTrader 5, который вычисляет скользящий коэффициент Шарпа в годовом выражении и отображает полосы статистической значимости с настраиваемой z-оценкой на основе асимптотической стандартной ошибки по формуле Ло. В индикаторе используется кольцевой буфер доходностей с инкрементальным расчетом дисперсии, что обеспечивает обновление за время O(1). Мы объясняем, как неопределенность масштабируется пропорционально n^(-1/2), почему интервалы расширяются при высоких значениях коэффициента Шарпа и как настроить приведение к годовому выражению для каждого инструмента, чтобы корректно использовать индикатор.
Адаптивное сглаживание цен в MQL5: фильтр Калмана с оценкой шума без внешних библиотек Адаптивное сглаживание цен в MQL5: фильтр Калмана с оценкой шума без внешних библиотек
Скользящие средние с фиксированным весовым коэффициентом вносят запаздывание, которое не учитывает смену рыночных режимов. В данной работе представлен индикатор на основе адаптивного скалярного фильтра Калмана, реализованный штатными средствами MQL5. Он оценивает дисперсию шума процесса Q по дисперсии приращений цены в скользящем окне, а дисперсию шума измерений R — по дисперсии цены в скользящем окне, применяет ограничение снизу для обеспечения численной устойчивости и пересчитывает коэффициент усиления Калмана на каждом баре. Линия индикатора, отображаемая поверх ценового графика, сравнивается с 20-периодной EMA по показателям MAE, RMSE, запаздывания и гладкости для количественной оценки качества отслеживания цены и подавления шума.