Formulating Dynamic Multi-Pair EA (Part 9): Market Microstructure Execution Noise Filtering
Table of contents
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.
Warning: All rights to these materials are reserved by MetaQuotes Ltd. Copying or reprinting of these materials in whole or in part is prohibited.
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.
Building an EquiVolume Indicator in MQL5
Custom Debugging and Profiling Tools for MQL5 Development (Part II): Profiling EAs and Testing Trading Logic
From Basic to Intermediate: Indicator (V)
Position Management: Scaling Into Winners With A Falling-Risk Pyramid
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use