preview
Formulating Dynamic Multi-Pair EA (Part 9): Market Microstructure Execution Noise Filtering

Formulating Dynamic Multi-Pair EA (Part 9): Market Microstructure Execution Noise Filtering

MetaTrader 5Examples |
1 431 0
Hlomohang John Borotho
Hlomohang John Borotho

Table of contents

  1. Introduction
  2. System Overview
  3. Getting Started
  4. Backtest
  5. Conclusion


Introduction

Most traders build strategies around price action, indicators, or statistical edges. They then wonder why a system that backtests beautifully bleeds money in live trading. The culprit is rarely the signal. It is the moment of execution. Spreads can widen suddenly during news releases, rollovers, and low-liquidity sessions. Tick flow becomes erratic when institutional algorithms reposition. Price gaps past levels that were supposed to act as support. Slippage on a clean market order eats half the expected reward. The trader watches their edge dissolve—not because the direction was wrong, but because the market's microstructure was in controlled chaos at the exact moment the system pulled the trigger. None of this is visible on a standard candlestick chart. Most retail-grade EAs have no mechanism to detect or respond to it.

The solution is to treat execution quality as a first-class filter. It sits as a layer between your strategy logic and the market. It only opens the gate when conditions are mechanically sound. Instead of trading every valid signal, the EA first checks execution conditions. It verifies spread, tick velocity, recent quote gaps, and micro-volatility stability. Only when all questions return clean answers does the system evaluate the trading signal itself. This approach pairs with a liquidity sweep continuation strategy that targets the aftermath of stop hunts. The result is an EA that is not just smarter about when to trade—it is structurally incapable of trading in the conditions where most retail losses actually occur.


System Overview

This Expert Advisor operates as a multi-layered execution filter rather than a conventional signal generator. It monitors multiple symbols independently, maintaining per-symbol tick buffers, spread histories, and volatility profiles. Before any trade can fire, five noise filters must clear: spread expansion, tick velocity, quote gaps, micro-volatility, and execution stability. Only then does the strategy layer activate. The trading logic itself uses a liquidity sweep continuation model. It waits for stop hunts to resolve, confirms a structural shift, and enters in the direction of the new flow. Risk management caps total exposure, limits correlated positions, and scales lot sizes to account for equity.

The EA does not predict direction. It predicts whether the market can currently fill an order without destroying the edge.

Conventional EAs treat every valid signal as a trigger. This EA treats the signal as conditional—dependent entirely on whether the market is currently capable of filling an order cleanly. It does not predict direction more accurately. It avoids the moments when direction is not essential because execution is too degraded to capture it. The strategy targets liquidity sweeps because these events concentrate retail stop losses and create sharp, short-lived distortions. Waiting for noise to settle after a sweep means the EA enters when the fake move is over and genuine order flow resumes.

The EA does not chase the sweep. It waits for the chaos to clear, confirms the market has chosen a direction, and only then commits capital.


Getting Started

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

To get started, we include the trading and position management libraries that give the Expert Advisor access to order execution, symbol data, and active trade information. We then organize the system into clearly separated input groups that control symbol monitoring, execution noise filtering, liquidity sweep detection, risk management, and diagnostic logging. The configuration allows us to monitor multiple instruments XAUUSD, GBPUSD, USDJPY, and USDZAR while adapting the behavior of the EA through adjustable thresholds. These settings define how the system reacts to spread spikes, abnormal tick velocity, quote gaps, and unstable volatility before any trade is allowed to execute. By exposing these controls as inputs, we create a flexible framework that can be optimized for different symbols, sessions, and execution environments.

The rest of the code builds the internal market state engine used by the EA to classify trading conditions and manage symbol-specific data in real time. We define several enumerations that describe the current market environment, trading signals, and liquidity sweep direction, allowing the system to make structured decisions rather than relying on isolated indicator values. The TickData and SymbolState structures store per-symbol data: spread statistics, tick activity, volatility, slippage estimates, sweep state, and session performance. Finally, the global variables initialize the trading objects, symbol arrays, performance counters, and tester detection logic that coordinate the entire multi-pair execution framework.

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

The initialization stage begins by preparing the environment that the Expert Advisor will operate in. We first create helper utilities that detect whether the EA is running inside the strategy tester, manage controlled logging output, and determine the correct order filling mode supported by the broker. These functions help the system adapt its behavior automatically between live trading and testing conditions. Inside OnInit(), we display startup information, parse the symbol list, trim whitespace, and validate the instrument count. Once the symbols are confirmed, we configure the trading engine with the selected magic number, slippage settings, and execution filling mode before allocating memory for all symbol states. This creates a clean and structured startup process that ensures the EA is fully prepared before market processing begins.

The InitSymbol() function then builds the internal data model for each monitored symbol independently. We initialize all execution statistics, spread measurements, liquidity sweep states, volatility metrics, tick buffers, and session tracking variables so every instrument starts with a stable baseline configuration. The function also validates broker availability using SymbolSelect(), retrieves important trading properties such as point size, lot limits, tick value, and contract size, and applies fallback defaults if any values are missing. To improve execution awareness from the start, we preload historical tick data using CopyTicks() and use it to seed the spread history and tick buffers that drive the noise filtering engine. Finally, we calculate the initial spread conditions, create the ATR indicator handle for volatility analysis, update the swing levels used by the liquidity sweep strategy, and prepare the symbol for real-time monitoring and execution.

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

The shutdown and tick handling stages are designed to keep the Expert Advisor stable while continuously collecting live market data from multiple symbols. Inside OnDeinit(), we stop the timer system, release all ATR indicator handles, and print the final trading statistics before the EA is removed from the chart. This ensures that memory and indicator resources are cleaned up correctly. The OnTick() function then acts as the real-time data collector for the entire framework. Every incoming chart tick triggers a loop through all monitored symbols, allowing the EA to maintain synchronized market information across instruments rather than depending only on the active chart symbol. This creates a centralized multi-pair monitoring process that is essential for execution quality analysis and noise filtering.

The CollectTick() function updates the internal tick buffers and spread history that power the market microstructure engine. We refresh bid and ask prices, reject duplicate ticks, and store new market data inside circular buffers for efficient high-frequency tracking. At the same time, the system updates spread measurements, tick counters, and timestamp information used for volatility and execution analysis.

The OnTimer() function then becomes the main processing engine of the EA. Here we calculate spread statistics, tick velocity, quote gaps, micro volatility, and slippage estimates before classifying the current market state. If the environment is considered stable, we proceed with the liquidity sweep strategy by updating swing levels, detecting sweeps, confirming structure shifts, and validating risk conditions before execution. When the market becomes noisy, the system blocks trading activity and records diagnostic information, ensuring that the strategy only operates during cleaner execution conditions.

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

The IsNoisyTesterAware() function acts as the core execution quality filter of the Expert Advisor. Its purpose is to decide whether current market conditions are too unstable for safe trading. We begin by checking if the noise filter is enabled, then dynamically adjust the filtering thresholds depending on whether the EA is running in live trading or inside the strategy tester. This is important because tester environments often produce artificial tick behavior and unrealistic execution conditions. The function then evaluates several microstructure signals, including spread expansion, abnormal tick velocity, large quote gaps, low or excessive micro volatility, and unstable slippage estimates. If any of these conditions exceed the allowed limits, the system flags the market as noisy and blocks trading activity. We also prevent execution during classified high-risk states such as unstable news conditions or severe execution instability.

The UpdateSpreadStats() function continuously updates the statistical spread model used by the noise filtering engine. We first ensure that enough historical spread samples are available before calculating the average spread and its standard deviation across the stored history buffer. These calculations allow the EA to understand what normal spread behavior looks like for each monitored symbol instead of relying on fixed assumptions. Once the spread baseline is established, the system compares the current spread against the expected average using the configured spread multiplier. If the spread exceeds the acceptable range, a spread spike event is recorded. This process allows the Expert Advisor to dynamically detect deteriorating execution conditions and adapt its trading decisions according to real-time market quality.

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

The first group of functions focuses on measuring real-time market activity and identifying unstable execution behavior. In UpdateTickVelocity(), we calculate how quickly ticks are arriving by dividing the number of collected ticks by the elapsed time inside the monitoring window. This gives us a live estimate of market activity intensity for each symbol. If the tick velocity exceeds the configured threshold, the system records a tick burst event, which may indicate aggressive order flow, news-driven volatility, or unstable liquidity conditions.

The UpdateQuoteGap() function then measures sudden price jumps between consecutive bid updates. By converting these changes into point distance, we can track abnormal quote gaps that often appear during fast market movements or poor liquidity conditions. When a gap exceeds the allowed limit, the EA logs the event and increases the gap event counter for that symbol.

The second group of functions evaluates short-term volatility behavior and estimates the overall execution quality of the market. Inside UpdateMicroVolatility(), we collect recent midpoint prices from the tick buffer and calculate tick-to-tick returns using the symbol’s point value. We then compute the mean and variance of these returns to derive a micro volatility measurement that reflects short-term price instability. This allows the EA to distinguish between stable directional movement and random execution noise. Finally, UpdateSlippageEstimate() combines spread behavior, tick velocity, and micro volatility into a single execution stability score. By averaging these normalized factors, the system creates a simplified slippage risk estimate that helps determine whether current market conditions are suitable for reliable trade execution.

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

The ClassifyMarketState() function acts as the decision engine that interprets current market conditions using the execution statistics collected earlier in the system. We evaluate several real-time microstructure signals including spread expansion, tick bursts, quote gaps, low liquidity activity, slippage instability, and abnormal volatility behavior. Based on the combination of these conditions, the EA classifies the market into predefined states such as HIGH_RISK_NEWS, EXECUTION_UNSTABLE, THIN_LIQUIDITY, NOISY_CHOP, CLEAN_BREAKOUT, or CLEAN_TREND. This classification process allows the trading system to understand whether the environment is safe for execution or too unstable for reliable trading. Instead of treating all market conditions equally, the EA dynamically adapts its behavior according to the current quality of execution and liquidity conditions.

The remaining functions refine the execution model further by assigning quality scores and tracking market structure levels used by the liquidity sweep strategy. In UpdateQualityScore(), we build a dynamic scoring system that rewards stable spreads, controlled volatility, and cleaner market states while penalizing unstable execution conditions such as excessive slippage, noisy volatility, and high-risk news behavior. This produces a numerical quality rating between 0 and 100 that summarizes the current trading environment for each symbol. The UpdateSwingLevels() function then scans recent H1 candle data to detect swing highs and swing lows that represent important liquidity zones in the market. By maintaining both current and previous swing levels, the EA can later identify liquidity sweeps, structure shifts, and continuation opportunities that form the core of the trading strategy.

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

The liquidity detection stage is responsible for identifying potential stop-hunt and liquidity grab scenarios around important swing levels in the market. Inside DetectLiquiditySweep(), we first verify that valid swing highs and swing lows are available before monitoring whether price temporarily pushes beyond these levels. If the ask price moves above a swing high or the bid price drops below a swing low within the allowed tolerance range, the EA registers a liquidity sweep event and records its direction, price level, and detection time. At the same time, the system resets the noise settlement and structure confirmation flags because a sweep alone is not enough to justify a trade. This process allows the strategy to recognize areas where market participants may have triggered resting liquidity before a potential directional continuation develops.

The following functions then validate whether the market environment becomes stable enough to trade after the liquidity event occurs. In CheckNoiseSettlement(), we wait for at least one completed bar after the sweep and evaluate whether spread conditions, tick velocity, and micro-volatility have normalized. Only when these execution conditions become stable does the EA mark the market as settled.

The CheckStructureShift() function then confirms directional intent by analyzing recent candle structure. After a low sweep, the system looks for price to break above recent highs to confirm bullish continuation, while a high sweep requires a close below recent lows to confirm bearish continuation. Once both noise settlement and structural confirmation are complete, the EA has a much stronger probability that the liquidity sweep has transitioned into a cleaner continuation move rather than random execution noise.

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

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

The signal generation stage ensures that trades are only triggered after all liquidity sweep and execution quality conditions have been fully confirmed. Inside GetSignal(), we first verify that a valid sweep exists, that market noise has settled, and that structural confirmation has already occurred. We also discard expired sweep setups and prevent duplicate entries if a position is already open for the symbol. Once these conditions are satisfied, the function converts the detected liquidity sweep into either a buy or sell signal depending on the sweep direction and the enabled trade settings.

Before allowing execution, the EA checks whether the symbol’s quality score meets the required threshold, ensuring that only higher-quality market environments can trigger trades. If the setup passes all filters, the system logs the final signal along with its execution quality score. The ResetSweepState() function is then used to clear all sweep-related information once a setup becomes invalid or a trade has been executed.

The risk management and execution layer focuses on protecting the account while ensuring stable trade placement. In ValidateRisk(), we confirm that the account has enough free margin and that the total number of open positions does not exceed the configured limits. The CalcLotSize() function then calculates position size dynamically using account balance, ATR-based stop distance, tick value, and percentage risk allocation. This allows the EA to adapt lot size automatically according to symbol volatility and account conditions.

The CheckGlobalDrawdown() function acts as a portfolio safety mechanism by halting trading activity whenever the account drawdown exceeds the maximum allowed percentage. Finally, ExecuteTrade() calculates SL/TP from ATR, refreshes prices, checks broker stop constraints, and submits the order with retries. Once a trade executes successfully, the system records the event, updates session statistics, and resets the active liquidity sweep state for the symbol.


Backtest

The backtest was conducted across roughly a 2-month testing window from 02 February 2026 to 01 April 2026, with the default settings:


Conclusion

Throughout this article, we built a professional-grade MQL5 Expert Advisor from the ground up. We started with a multi-symbol architecture that treats each instrument independently, maintaining its own tick buffer, spread history, and market state. We layered in six real-time noise filters—spread expansion, tick velocity, quote gaps, micro-volatility, slippage estimation, and execution state classification—each one targeting a specific way the market can become mechanically unsafe.

Moreover, we built a liquidity sweep continuation strategy that waits for stop hunts to complete, confirms noise has settled, and only enters when structure shifts in the intended direction. We then wrapped everything in a multi-layer risk engine that guards against correlated exposure, drawdown breaches, and margin stress across all pairs simultaneously.

What the trader leaves with is a complete shift in how they think about trade execution. The EA itself is a working tool—drop it on a chart, and it immediately starts scoring market quality, and filtering out the noise that quietly destroys most automated strategies. But more importantly, the trader now has a mental model that separates signal quality from execution quality, and understands that these are two different problems requiring two different solutions. Every concept in this article is modular—the noise filters can be dropped into any existing EA, and the sweep detection logic can be adapted to any liquidity-based strategy. The trader walks away not just with code, but with a framework they can apply, modify, and build on for every system they develop going forward.


Attached files |
Market_Micro.mq5 (48.28 KB)
Building an EquiVolume Indicator in MQL5 Building an EquiVolume Indicator in MQL5
We implement an EquiVolume indicator in MQL5 that converts standard candlesticks into volume-weighted boxes. The workflow includes selecting volume type, detecting the maximum volume within a lookback range, normalizing all values against it, and mapping them into proportional box widths. The result is a chart-based structure that visualizes trading activity intensity alongside price movement in MetaTrader 5.
Custom Debugging and Profiling Tools for MQL5 Development (Part II): Profiling EAs and Testing Trading Logic Custom Debugging and Profiling Tools for MQL5 Development (Part II): Profiling EAs and Testing Trading Logic
We build a compact profiler that records calls, min/max/average times, and slow-call counts to CSV, and a simple test runner that writes deterministic pass/fail reports. The article explains where to place measurements in an EA, how to sample ticks, and how to keep pure calculations testable. Running the script first and the profiling EA second provides repeatable evidence for regression analysis.
From Basic to Intermediate: Indicator (V) From Basic to Intermediate: Indicator (V)
In this article, we will look at how to handle user requests to change the chart plotting mode. This is necessary so that an indicator designed for the current chart plotting mode does not look strange or differ from what a MetaTrader 5 user expects.
Position Management: Scaling Into Winners With A Falling-Risk Pyramid Position Management: Scaling Into Winners With A Falling-Risk Pyramid
We introduce CPyramidBridge, a thin MQL5 layer that maps bet-sizing results to CPyramidEngine. The bridge applies probability to initial lot sizing, enforces a capacity-aware entry gate, promotes add-ons from dynamic divergence, adapts the trailing stop to reserve estimates, and syncs signals on close, allowing an Expert Advisor to convert model confidence and concurrency into a structured, decreasing-risk pyramid.