preview
Why Backtested Scalpers Diverge in Live Trading (Part 1): Building an Intelligent Friction Guard for Dynamic Spread and Micro-Volatility Filtering

Why Backtested Scalpers Diverge in Live Trading (Part 1): Building an Intelligent Friction Guard for Dynamic Spread and Micro-Volatility Filtering

MetaTrader 5 — Trading systems |
63 0
Wanateki Solutions LTD
Kelvin Muturi Muigua

Table of Contents

  1. Introduction: The Scalper's Reality Gap
  2. The Mathematics and Mechanics of Market Friction
  3. Software Architecture and Safe Memory Design
  4. Implementation Overview
  5. Master Facade: FrictionGuard.mqh
  6. Live Verification: MicrostructureRadarTest.mq5
  7. Conclusion


Introduction: The Scalper's Reality Gap

Every quantitative trader developing short-horizon or tick-driven scalping systems encounters a common practical problem. In the MetaTrader 5 Strategy Tester, an Expert Advisor appears highly profitable in historical simulation. The equity curve ascends smoothly, the win rate hovers above 80%, and drawdowns appear minimal.

Yet when deployed onto a live account backed by real capital, live performance may materially diverge. Trades that were consistently profitable in simulation can be repeatedly stopped out or closed at break-even, accumulating unexpected friction losses.

Why do +3.0 pip basket targets collapse into negative expectancy on live ECN accounts?

Fig. 1. The Scalper's Reality Gap: Idealized Backtest Execution vs. Live Microstructure Friction Drag

Fig. 1. The Scalper's Reality Gap: Idealized Backtest Execution vs. Live Microstructure Friction Drag

This divergence arises from differences between backtest assumptions and live execution. The Strategy Tester replays recorded broker quotes, whereas live fills depend on network latency, broker queues, order-book depth, and slippage. As illustrated in Figure 1, a hypothetical 3.0-pip price movement worth $300 per lot can be reduced by 1.2 pips of spread expansion, 0.8 pips of transmission latency, and 0.9 pips of order-book slippage. Together, these frictions consume 2.9 pips, or approximately 96.7% of the gross price movement, leaving just 0.1 pip ($10 per lot) before commissions. With a $14 commission, the resulting net P/L is −$4 per lot.

The −$4 per lot result assumes the three listed costs under normal execution conditions. The −$70 lower bound illustrates a stress case in which spread blowouts and adverse slippage add approximately $66 per lot of incremental cost, reducing the remaining $10 gross edge to a substantially larger net loss. In short-horizon trading, transaction friction is not an incidental cost; it can be a primary determinant of strategy survival.

In my live trading architecture, I address this challenge by engineering a modular pre-trade filtering layer: the Intelligent Friction Guard (CFrictionGuard). Rather than relying on naive spread checks, our engine continuously audits incoming tick velocity, statistical spread expansion, and transaction friction to screen out adverse execution conditions before an order is dispatched.

The Triad of Market Friction: Spreads, Latency, and Slippage

Market friction in electronic trading stems from three interconnected forces: dynamic spread expansion, execution latency, and limit order book slippage. These factors interact throughout the execution lifecycle, from the moment an order is dispatched by the client terminal to its eventual fill at the broker's matching engine. Figure 2 illustrates their individual effects and how execution delays and adverse price movements can erode a scalper's expected profit.

Fig. 2. The Anatomy of Execution Drag: Deconstructing Spreads, Transit Latencies, and Limit Order Book Slippage

Fig. 2. The Anatomy of Execution Drag: Deconstructing Spreads, Transit Latencies, and Limit Order Book Slippage

  1. Floating Spread Expansion: Liquidity providers widen Bid-Ask spreads during volatility spikes, rollover periods (such as 5:00 PM EST), and illiquid session transitions to manage inventory risk. Under normal conditions, the spread may be approximately 0.8 pips, but volatility-driven widening can push it toward 3.5–5.0 pips or higher. Inventory imbalances and adverse order flow can further amplify this effect, potentially consuming 30%–80% of a scalper's expected edge.
  2. Execution Latency: Network transit, broker gateway routing, and server matching introduce quote aging, causing orders to arrive after target market prices have moved. In the illustrated scenario, round-trip latency ranges from 15 to 45 ms, with VPS-to-broker transmission taking approximately 1–25 ms and gateway queuing another 2–10 ms. During periods of rapid market activity, quotes may update more than 100 times per second, and several price changes can occur before an order reaches the matching engine. The resulting in-flight price drift can increase execution costs.
  3. Limit Order Book Slippage: Dispatching aggressive market orders into shallow top-of-book liquidity can sweep across multiple price levels, increasing realized entry costs. In the illustrated example, top-of-book liquidity may be limited to 2–5 lots, while deeper levels can require sweeping two or three additional price levels. Adverse slippage may range from approximately 0.9 to 2.5 pips. An order dispatched at a quoted Ask of 1.08500 with a target of 4.0 pips ($400) receives a realized fill at 1.08522, resulting in 2.2 pips of adverse slippage. This consumes 55% of the intended 4.0-pip target move.

The total microstructure toll consists of dynamic spread costs, in-flight price drift, adverse order book slippage, and commission. The guard rejects a trade when the estimated Friction Ratio exceeds the configured maximum. The library uses a default threshold of 0.65 (65%), while the diagnostic script applies a stricter threshold of 0.40 (40%) to demonstrate more conservative trade filtering.

Our client-side architecture focuses directly on the primary observable force: gating trades against unfavorable spread expansions and stalled momentum before orders leave the terminal.

The Flaw of Static Thresholds: Why Naive Spread Filters Fail

Retail algorithmic developers frequently implement a basic spread check:

//--- Naive retail spread filter
if(SymbolInfoInteger(_Symbol, SYMBOL_SPREAD) > InpMaxSpread)
  {
   return;
  }

This static check treats spread as an isolated scalar. In a high-momentum breakout, an expanded spread (e.g., 14 points vs. normal 8) may be easily compensated by price accelerating at 25 points per second. Conversely, in a quiet session, a narrow spread (e.g., 9 points) can be deceptive if velocity stalls at 0.5 points per second, leaving price displacement insufficient to clear basic trading costs. Spread must never be evaluated in isolation; it must be assessed dynamically against recent price displacement.

Pre-Trade Engineering Scope and Verification Boundary

Important: [Core Operational Boundary] Passing the pre-trade filter does not guarantee trade profitability or positive forward expectancy. It verifies strictly that observable quote-side conditions (spread anomalies, tick frequency, and price velocity) currently satisfy configured execution safety criteria.

I have defined three strict operational boundaries for our pre-trade gatekeeper:

  • Pre-trade screening vs. execution simulation: The engine operates strictly as a client-side gatekeeper inspecting top-of-book quotes before order dispatch. It does not simulate network transit delays or order-book queue depth.
  • Parametric cost baselines: Slippage (DEFAULT_SLIPPAGE_ESTIMATE_PTS) and commission hurdles are initialized as empirical baseline inputs. Part 2 will extend the system, while Part 3 will add live post-trade updates through OnTradeTransaction().
  • Functional software verification: Verification in this article refers strictly to validating algorithmic logic, ring-buffer stability, single-pass variance accuracy, and regime state transitions on live chart ticks.


The Mathematics and Mechanics of Market Friction

To engineer an adaptive pre-trade gatekeeper, we formulate market friction through three quantitative measures: kinetic price displacement velocity, statistical spread profiling via single-pass online variance with exponential decay, and the dimensionless Friction Ratio.

Quantifying Price Velocity: Measuring Tick Displacement Over Rolling Windows

Price velocity captures the instantaneous rate of price displacement across an event-driven rolling window, avoiding the temporal aliasing of fixed calendar sampling:

Eq. 1. Instantaneous Price Velocity Formula

Where:

  • v_tick: Instantaneous price velocity in points per second.
  • P_last: Most recent mid-price sample: (Bid + Ask) / 2.0.
  • P_first: Oldest mid-price sample within the rolling window.
  • delta_t_actual: The measured elapsed time. For the velocity calculation, the effective interval is bounded below by Δt ≥ 0.10 s to prevent artificial spikes from sub-millisecond tick bursts.

Mid-price tracking dampens bid-ask bounce noise. In addition, our engine computes tick arrival frequency (tick count per second, or tick frequency):

Eq. 1b. Tick Arrival Frequency Formula

Here, 𝑘 represents the exact number of ticks in the window. Evaluating velocity alongside arrival frequency provides essential microstructure context: elevated velocity with high-frequency signals genuine directional momentum, whereas high frequency with minimal price displacement indicates absorption, indecision, or rapid quote revision.

Statistical Spread Profiling: Single-Pass Online Variance and Adaptive EWMA Variance

To build an adaptive spread baseline without unbounded memory use, the engine applies a two-phase statistical model:

1. Initial Cumulative Phase (Before 500 Observations): The engine accumulates running statistics using B.P. Welford's exact single-pass recurrence (1962):

Eq. 2. Welford's Online Variance Recurrence

Where x_k is the current spread observation in points, mu_k is the sample mean, and M2_k is the running sum of squared deviations.

2. Adaptive Steady-State Phase (Starting at Observation 500): Starting with observation 500, the estimator switches to exponentially weighted moving variance with a decay factor of 𝛼 = 0.002. This gives an effective memory horizon of approximately 1/𝛼 ≈ 500 ticks and allows the estimate to adapt to intraday volatility changes:

Eq. 2d. Exponentially Weighted Moving Variance

Following Finch (2009), the factor (1 − alpha) adjusts for the moving mean and helps avoid upward variance bias in nonstationary spreads.

When initializing the statistical profiler, balancing stability and responsiveness requires managing two distinct operational boundaries. The configuration parameter DEFAULT_WARMUP_TICKS (set to 15 ticks by default) enforces a strict calibration phase where incoming spread observations are recorded into the rolling distribution without triggering emergency anomaly blocks. This ensures that the Z-score calculation is never evaluated against an empty or statistically volatile baseline. Once this initial baseline is established, the engine transitions at DEFAULT_EWMA_TRANSITION_TICKS (500 samples) from cumulative Welford accumulation to exponentially weighted moving variance. This algorithmic cutover prevents long-term memory saturation, allowing the spread model to gracefully adapt to shifting intraday liquidity regimes without requiring manual resets.

With running mean and standard deviation continuously updated, the dynamic spread Z-score is computed as:

Eq. 3. Statistical Spread Z-Score Formula

To prevent division-by-zero on raw ECN accounts where spreads compress near zero, the denominator enforces an effective standard deviation floor of 1.0 point whenever the running average spread is less than or equal to 2.0 points.

Crucially, IsSpreadSpike() evaluates before updating the distribution with the incoming sample. This prevents a sudden liquidity gap from artificially inflating its own mean before the anomaly can be flagged. When the spread Z-score exceeds 2.5 standard deviations or crosses an absolute ceiling, the engine flags a spread spike and blocks execution.

The Friction Ratio Formula: Transaction Cost vs. Price Momentum

To determine whether recent price velocity justifies transaction friction, we introduce the dimensionless Friction Ratio (FR):

Eq. 4. Dynamic Friction Ratio Formula

Where:

  • Total Friction = Current Spread + Commission Points + Slippage Estimate
  • Projected Movement = Velocity * delta t_horizon

In this article (Part 1), top-of-book execution drag is parameterized as an empirical constant (DEFAULT_SLIPPAGE_ESTIMATE_PTS = 2.0 points). The series will later extend this baseline with dynamic post-trade updates through OnTradeTransaction().

The metric balances costs against momentum:

  • Viable Momentum: If spread is 10 points, slippage is 2 points, velocity is 20 points/sec, and the horizon is 2 seconds, projected displacement is 40 points. The resulting Friction Ratio is (10 + 2) / 40 = 0.30 (30%), indicating friction represents a manageable fraction of displacement.
  • Stagnant Drift: If spread is 12 points, slippage is 2 points, and velocity drops to 1 point/sec, projected displacement is only 2 points. The resulting Friction Ratio surges to (12 + 2) / 2 = 7.0 (700%), causing the guard to block trade initiation.

Defining the Four Operational Regimes

The engine categorizes market conditions into four operational regimes:

  1. OPTIMAL (Green): Normal spreads (Z-score <= 1.0) and low friction (Friction Ratio <= 0.35). Order dispatch approved.
  2. MODERATE (Yellow): The residual safe mode after the stricter OPTIMAL conditions are not met. It includes acceptable but less ideal conditions, such as low friction with an elevated spread Z-score, or modest friction generally within the 0.35–0.65 range. Trading remains possible only if all subsequent safety checks pass.
  3. FRICTION-LOCKED (Orange): Low velocity relative to spread (Friction Ratio exceeding the configured maximum, 0.65 by default) or price velocity below minimum kinetic threshold. Order execution blocked.
  4. SPREAD-SPIKE (Red): Statistical spread anomaly (Z-score > 2.5) or spread exceeding the absolute ceiling limit. Order execution blocked.

Beyond standard regime classification, live trading environments frequently introduce anomalous edge cases that pure spread and velocity metrics cannot resolve alone. To prevent catastrophic parameter misconfigurations, such as applying 5-digit Forex parameters to crypto or gold contracts, the engine integrates two primary safety overrides into the clearance path. First, the scale mismatch guard actively audits decimal precision at startup. Second, the directional momentum guard cross-checks signed velocity against the intended order direction, ensuring the system never authorizes a Buy order during a violent downward price thrust regardless of how tight the current spread appears.

Hierarchical Decision Priority: When multiple adverse conditions coincide on a single tick, mReason records the first blocker under this strict sequence:

  1. Scale Mismatch Lockout (REASON_SCALE_MISMATCH)
  2. Buffer Warm-up Incomplete (REASON_BUFFER_NOT_READY)
  3. Spread Spike or Hard Ceiling Breach (REASON_SPREAD_SPIKE / REASON_HARD_SPREAD_MAX)
  4. Insufficient Kinetic Velocity (REASON_LOW_VELOCITY)
  5. Prohibitive Friction Ratio or Commission Hurdle (REASON_HIGH_FRICTION / REASON_COMMISSION_HURDLE)
  6. Adverse Directional Momentum (REASON_ADVERSE_MOMENTUM)

Fig. 3. CFrictionClassifier 4-Regime Decision Hierarchy and Execution Gating

Fig. 3. CFrictionClassifier 4-Regime Decision Hierarchy and Execution Gating


Software Architecture and Safe Memory Design

Pre-trade screening requires deterministic, low-latency execution. In MQL5, allocating dynamic heap memory inside OnTick() can introduce memory fragmentation and latency jitter during high-frequency quote bursts. To ensure predictable performance, the engine relies on pre-allocated structures and a decoupled object-oriented architecture.

Fig. 4. Intelligent Friction Guard Modular Architecture and Execution Pipeline

Fig. 4. Intelligent Friction Guard Modular Architecture and Execution Pipeline

Architectural Blueprint: Modular Pipeline Overview

To maintain a deterministic execution footprint and eliminate memory fragmentation during high-frequency quote bursts, the system is organized into a modular pipeline where specialized components pass telemetry payloads by constant reference (const SFrictionMetrics &metrics), completely avoiding structure-copying overhead on incoming ticks. At the center of this pipeline lies CFrictionGuard.mqh, acting as a master facade that unifies CoreDefines.mqh for centralized constants, CCircularTickBuffer for pre-allocated ring buffering, CTickVelocityTracker for kinetic momentum, CSpreadAnalyzer for Welford-EWMA spread profiling, and CFrictionClassifier for the 4-regime decision state machine.

Data passes through the pipeline by constant reference (const SFrictionMetrics &metrics), preventing unnecessary structure copying on incoming ticks.

Pre-Allocated Circular Buffering: Zero-Reallocation Storage

To avoid repeated ArrayResize() calls inside the tick handler, CCircularTickBuffer allocates a fixed-capacity array once during initialization. It advances its write pointer through a rolling head index using modular arithmetic: next_index = (head + 1) % capacity. Once capacity is reached, it continuously overwrites the oldest tick in constant O(1) time.

Fig. 5. CCircularTickBuffer Ring Buffer Architecture and Time-Window Slicing

Fig. 5. CCircularTickBuffer Ring Buffer Architecture and Time-Window Slicing

When querying the oldest sample within a time window (GetOldestInWindow()), the method traverses backward from the head pointer across active samples. This search operates in O(k) time complexity, where k is the number of ticks spanning the requested window duration (where k <= active sample count). Because k is small (typically 10 to 50 ticks for a 5-second window), queries complete with zero heap allocations.

Safe Memory Management and Object Lifecycle

For long-running automated systems, strict resource management is essential:

  1. Dynamic objects instantiated with new are immediately validated using CheckPointer(). If an allocation fails, initialization aborts safely.
  2. Destructors systematically release allocated instances via delete and nullify pointers to prevent dangling references.
  3. The tick processing path uses pre-allocated memory exclusively, ensuring zero dynamic heap resizing during live trade evaluation.


Implementation Overview

The suite is implemented in modular headers under ...\ScalperGuard\ in the default MQL5 Include folder.

Project Directory Structure

Separating shared library components from executable scripts and EAs maintains a clean architecture.

In MetaEditor Navigator (Ctrl+D), organize our project under the following layout:

MQL5/
├── Scripts/
│   └── Why_Backtested_Scalpers_Diverge_in_Live_Trading/
│       └── Part_1/
│           └── MicrostructureRadarTest.mq5
│
└── Include/
    └── Why_Backtested_Scalpers_Diverge_in_Live_Trading/
        └── ScalperGuard/
            ├── CoreDefines.mqh
            ├── CircularTickBuffer.mqh
            ├── TickVelocityTracker.mqh
            ├── SpreadAnalyzer.mqh
            ├── FrictionClassifier.mqh
            ├── TelemetryDashboard.mqh
            └── FrictionGuard.mqh

Analytical components reside under MQL5\Include\Why_Backtested_Scalpers_Diverge_in_Live_Trading\ScalperGuard\. Diagnostic and verification scripts reside in MQL5\Scripts\Why_Backtested_Scalpers_Diverge_in_Live_Trading\Part_1\.

Core Constants and Data Structures: CoreDefines.mqh

CoreDefines.mqh establishes the shared enumerations, tick structures, symbol metadata, and default thresholds used across all components.

//+------------------------------------------------------------------+
//|                                                  CoreDefines.mqh |
//|                          Copyright 2026, Wanateki Solutions Ltd. |
//|                                         https://www.wanateki.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Wanateki Solutions Ltd."
#property link      "https://www.wanateki.com"

#ifndef SCALPER_GUARD_CORE_DEFINES_MQH
#define SCALPER_GUARD_CORE_DEFINES_MQH

The default preprocessor parameters define baseline thresholds for buffer sizing, kinetic velocity windows, statistical spread Z-score limits, and transaction friction ratios:

//+------------------------------------------------------------------+
//| Global Constants & Defaults                                      |
//+------------------------------------------------------------------+
#define DEFAULT_BUFFER_CAPACITY        500
#define DEFAULT_VELOCITY_WINDOW_SEC    5.0
#define DEFAULT_MIN_VELOCITY_PTS_SEC   0.5
#define DEFAULT_SPREAD_ZSCORE_LIMIT    2.5
#define DEFAULT_HARD_SPREAD_MAX_PTS    30.0
#define DEFAULT_MAX_FRICTION_RATIO     0.65
#define DEFAULT_EXPECTED_HOLDING_SEC   10.0
#define DEFAULT_MIN_EXPECTED_MOVE_PTS  10.0
#define DEFAULT_SLIPPAGE_ESTIMATE_PTS  2.0
#define DEFAULT_WARMUP_TICKS           15
#define DEFAULT_EWMA_TRANSITION_TICKS  500

The constants above are intentionally conservative, symbol-agnostic starting points rather than universally optimal values. They are designed to make the Friction Guard fail safe during initial deployment: uncertain or unfavorable execution conditions are more likely to block a trade than to allow it.

  • DEFAULT_MAX_FRICTION_RATIO (0.65) sets the upper limit at which estimated friction remains acceptable relative to the strategy’s expected gross edge. A value below 1.0 ensures that estimated friction does not consume the entire expected edge; 0.65 leaves an additional buffer for slippage, spread variability, and estimation error.
  • DEFAULT_HARD_SPREAD_MAX is intended as an absolute execution-quality ceiling. It should be set above the symbol’s normal spread range but below the level at which entering a short-horizon trade becomes economically impractical. In practice, it is calibrated from the upper tail of observed spread distributions during the intended trading sessions.
  • The slippage estimate is a conservative allowance for execution uncertainty. It should reflect the broker’s execution model, typical order size, instrument liquidity, and the difference between requested and filled prices observed during comparable sessions.
  • DEFAULT_MIN_VELOCITY establishes the minimum directional movement required for the guard to consider a setup meaningful. It should be calibrated relative to the strategy’s target holding period and typical noise level: too low permits low-conviction entries, while too high may filter out valid short-term opportunities.

A practical calibration workflow is:

  1. Collect tick, spread, friction, and execution data separately for each symbol, broker, account type, and trading session.
  2. Reserve an out-of-sample period for validation.
  3. Measure the distribution of spreads, friction ratios, slippage, and directional velocity.
  4. Select thresholds from the training data; for example, using percentile-based limits or a target blocker rate.
  5. Test the selected thresholds on the out-of-sample period, measuring blocked-trade percentage, missed opportunities, realized friction, and net expectancy.
  6. Avoid optimizing thresholds directly on the final test period.
The next article in this series (part 2), will build a complete scalping Expert Advisor around the Friction Guard. That article will demonstrate how to integrate these thresholds into a practical strategy and will cover calibration, sensitivity analysis, and parameter selection in greater depth.

Note: These thresholds are illustrative baselines. They should be calibrated out of sample for each symbol, broker, account type, session, and strategy horizon.

The core enumerations classify broker spread types, operational execution regimes, and explicit rejection reason codes:

//+------------------------------------------------------------------+
//| Enum: ENUM_SPREAD_TYPE                                           |
//| Distinguishes broker spread mechanism (informational metadata)   |
//+------------------------------------------------------------------+
enum ENUM_SPREAD_TYPE
  {
   SPREAD_TYPE_FLOATING = 0,   // Dynamic/variable market spread (ECN, standard Forex)
   SPREAD_TYPE_FIXED    = 1    // Constant/fixed algorithmically or contractually (Synthetics, Fixed FX)
  };

//+------------------------------------------------------------------+
//| Enum: ENUM_MARKET_REGIME                                         |
//| Classifies market friction state into discrete execution regimes |
//+------------------------------------------------------------------+
enum ENUM_MARKET_REGIME
  {
   REGIME_OPTIMAL = 0,         // Tight spread, sufficient velocity (Trade Allowed)
   REGIME_MODERATE = 1,        // Normal spread, acceptable cost (Trade Allowed)
   REGIME_FRICTION_LOCKED = 2, // Low velocity or excessive friction ratio (Trade Blocked)
   REGIME_SPREAD_SPIKE = 3     // Statistical anomaly or hard threshold breach (Trade Blocked)
  };

//+------------------------------------------------------------------+
//| Enum: ENUM_REJECTION_REASON                                      |
//| Explicit categorization of why a pre-trade check was rejected    |
//+------------------------------------------------------------------+
enum ENUM_REJECTION_REASON
  {
   REASON_NONE = 0,              // Trade permitted
   REASON_SPREAD_SPIKE = 1,      // Z-Score exceeded threshold
   REASON_HARD_SPREAD_MAX = 2,   // Spread exceeded absolute maximum limit
   REASON_LOW_VELOCITY = 3,      // Market price velocity is too low
   REASON_HIGH_FRICTION = 4,     // Friction ratio exceeded threshold
   REASON_BUFFER_NOT_READY = 5,  // Insufficient tick history in circular buffer
   REASON_SCALE_MISMATCH = 6,    // User inputs are out-of-scale for symbol digits/dynamics (Trading Paused)
   REASON_COMMISSION_HURDLE = 7, // Zero-spread commission hurdle exceeds expected price displacement
   REASON_ADVERSE_MOMENTUM = 8,  // Order direction opposes strong instantaneous market velocity
   REASON_INVALID_TICK = 9       // Non-positive Bid/Ask or crossed quotes (ask < bid)
  };

Symbol metadata, tick snapshots, and consolidated telemetry are encapsulated in memory-aligned structures:

//+------------------------------------------------------------------+
//| Struct: SSymbolProfile                                           |
//| Encapsulates symbol precision, tick sizing, and broker execution |
//+------------------------------------------------------------------+
struct SSymbolProfile
  {
   string            mSymbol;             // Symbol name
   int               mDigits;             // Decimal digits (5, 3, 2, 1, 0)
   double            mPoint;              // Point unit size
   double            mTickSize;           // Minimum price change step
   double            mTickValue;          // Monetary value of 1 tick per 1 lot
   ENUM_SPREAD_TYPE  mSpreadType;         // Broker declared spread type (Floating vs Fixed)
   int               mStopsLevel;         // Broker minimum stops level in points
   int               mFreezeLevel;        // Broker freeze level in points
   double            mTypicalSpreadPts;   // Current observed spread in points
   bool              mIsZeroSpreadAccount;// Zero-spread or raw ECN account flag
   double            mCommissionPerLot;   // Detected round-turn commission per 1.0 lot in deposit currency
   double            mCommissionPoints;   // Commission converted to equivalent symbol points
  };

//+------------------------------------------------------------------+
//| Struct: STickSample                                              |
//| Compact representation of a millisecond-stamped tick record      |
//+------------------------------------------------------------------+
struct STickSample
  {
   ulong             mTimestampMs;      // Server or local tick timestamp in milliseconds
   double            mBid;              // Bid price
   double            mAsk;              // Ask price
   double            mMidPrice;         // (Bid + Ask) / 2.0
   double            mSpreadPoints;     // (Ask - Bid) / Point
   double            mVolume;           // Tick volume or real traded volume (supports fractional crypto/CFD sizes)
  };

//+------------------------------------------------------------------+
//| Struct: SFrictionMetrics                                         |
//| Consolidated snapshot of real-time analytical metrics            |
//+------------------------------------------------------------------+
struct SFrictionMetrics
  {
   ENUM_MARKET_REGIME    mRegime;                   // Current operational regime
   ENUM_REJECTION_REASON mReason;                   // Active rejection reason code
   ENUM_SPREAD_TYPE      mSpreadType;               // Symbol spread model (Floating vs Fixed)
   int                   mDigits;                   // Symbol decimal digits
   double                mPoint;                    // Symbol point size
   double                mCurrentSpreadPoints;      // Current spread in points
   double                mMeanSpreadPoints;         // Rolling baseline mean spread
   double                mStdDevSpreadPoints;       // Rolling spread standard deviation
   double                mSpreadZScore;             // Instantaneous spread Z-score
   double                mTickVelocityPointsPerSec; // Absolute price displacement speed
   double                mSignedVelocity;           // Directional price displacement speed
   double                mTickFrequency;            // Real-time tick arrival frequency in ticks per second
   double                mFrictionRatio;            // Current friction ratio
   bool                  mIsZeroSpreadAccount;      // Zero-spread account flag
   double                mCommissionPoints;         // Commission in equivalent symbol points
   double                mEffectiveFrictionPoints;  // Total friction (Spread + Commission + Slippage) in points
   bool                  mIsSafeToTrade;            // Pre-trade boolean evaluation result
   bool                  mIsScaleValid;             // Out-of-scale validation status
   string                mScaleWarningMessage;      // Diagnostic guidance message if out-of-scale
   ulong                 mTotalEvaluations;         // Total pre-trade evaluations requested
   ulong                 mApprovedEvaluations;      // Pre-trade evaluations that passed
   ulong                 mBlockedEvaluations;       // Pre-trade evaluations that failed
  };

#endif // SCALPER_GUARD_CORE_DEFINES_MQH

SSymbolProfile captures instrument metadata queried during initialization, including converted commission points. STickSample provides a lightweight representation of price, spread, and volume for circular buffer storage. SFrictionMetrics acts as a unified telemetry payload, passing statistical indicators, kinetic velocity, and gating flags directly to the trading strategy. The evaluation counters (mTotalEvaluations, mApprovedEvaluations, mBlockedEvaluations) track calls to IsSafeToTrade().

Pre-Allocated Circular Buffer: CircularTickBuffer.mqh

CCircularTickBuffer stores raw tick samples in pre-allocated contiguous memory. It advances its write pointer in constant O(1) time, eliminating dynamic array reallocations during live execution:

//+------------------------------------------------------------------+
//|                                           CircularTickBuffer.mqh |
//|                          Copyright 2026, Wanateki Solutions Ltd. |
//|                                         https://www.wanateki.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Wanateki Solutions Ltd."
#property link      "https://www.wanateki.com"

#ifndef SCALPER_GUARD_CIRCULAR_TICK_BUFFER_MQH
#define SCALPER_GUARD_CIRCULAR_TICK_BUFFER_MQH

#include "CoreDefines.mqh"

//+------------------------------------------------------------------+
//| Class: CCircularTickBuffer                                       |
//| Pre-allocated circular ring buffer for millisecond tick data     |
//+------------------------------------------------------------------+
class CCircularTickBuffer
  {
private:
   STickSample       mBuffer[];     // Pre-allocated internal buffer array
   int               mCapacity;     // Maximum sample capacity
   int               mHead;         // Index of most recent sample
   int               mCount;        // Total active valid samples populated

Memory is pre-allocated once during Initialize():

public:
   //+------------------------------------------------------------------+
   //| Constructor                                                      |
   //+------------------------------------------------------------------+
                     CCircularTickBuffer(void)
     {
      mCapacity = 0;
      mHead = -1;
      mCount = 0;
     }

   //+------------------------------------------------------------------+
   //| Destructor                                                       |
   //+------------------------------------------------------------------+
                    ~CCircularTickBuffer(void)
     {
      ArrayFree(mBuffer);
      mCapacity = 0;
      mHead = -1;
      mCount = 0;
     }

   //+------------------------------------------------------------------+
   //| Pre-allocates buffer memory upfront to avoid dynamic resizing    |
   //+------------------------------------------------------------------+
   bool              Initialize(const int capacity)
     {
      if(capacity <= 0)
        {
         return false;
        }
      mCapacity = capacity;
      mHead = -1;
      mCount = 0;
      int resized = ArrayResize(mBuffer, mCapacity);
      return (resized == mCapacity);
     }

   //+------------------------------------------------------------------+
   //| Resets internal pointer indices without deallocating buffer      |
   //+------------------------------------------------------------------+
   void              Reset(void)
     {
      mHead = -1;
      mCount = 0;
     }

Ticks are inserted in O(1) time by advancing the rolling head index:

   //+------------------------------------------------------------------+
   //| Pushes a new tick sample into the circular buffer in O(1) time   |
   //+------------------------------------------------------------------+
   void              Push(const STickSample &sample)
     {
      if(mCapacity <= 0)
        {
         return;
        }
      mHead = (mHead + 1) % mCapacity;
      mBuffer[mHead] = sample;
      if(mCount < mCapacity)
        {
         mCount++;
        }
     }

Window lookups traverse backward from the head pointer in O(k) time without heap allocations:

   //+------------------------------------------------------------------+
   //| Retrieves the most recent tick sample                            |
   //+------------------------------------------------------------------+
   bool              GetLatest(STickSample &sample) const
     {
      if(mCount == 0 || mHead < 0 || mHead >= mCapacity)
        {
         return false;
        }
      sample = mBuffer[mHead];
      return true;
     }

   //+------------------------------------------------------------------+
   //| Finds the oldest tick sample within a rolling time window        |
   //| Complexity: O(k) where k is the number of ticks in the window   |
   //+------------------------------------------------------------------+
   bool              GetOldestInWindow(const ulong windowMs, const ulong currentTimestampMs, STickSample &sample, int &outTicksInWindow) const
     {
      outTicksInWindow = 0;
      if(mCount == 0 || mHead < 0)
        {
         return false;
        }
      ulong cutoffTime = (currentTimestampMs > windowMs) ? (currentTimestampMs - windowMs) : 0;
      int oldestValidIndex = mHead;

      for(int i = 0; i < mCount; i++)
        {
         int physicalIndex = (mHead - i + mCapacity) % mCapacity;
         if(mBuffer[physicalIndex].mTimestampMs < cutoffTime)
           {
            break;
           }
         oldestValidIndex = physicalIndex;
         outTicksInWindow++;
        }

      sample = mBuffer[oldestValidIndex];
      return true;
     }

   //+------------------------------------------------------------------+
   //| Backward-compatible three-parameter overload                        |
   //+------------------------------------------------------------------+
   bool              GetOldestInWindow(const ulong windowMs, const ulong currentTimestampMs, STickSample &sample) const
     {
      int dummyCount = 0;
      return GetOldestInWindow(windowMs, currentTimestampMs, sample, dummyCount);
     }

   //+------------------------------------------------------------------+
   //| Gets a sample by reverse-chronological offset (0 = latest)       |
   //+------------------------------------------------------------------+
   bool              GetSampleByOffset(const int offset, STickSample &sample) const
     {
      if(offset < 0 || offset >= mCount || mHead < 0)
        {
         return false;
        }
      int physicalIndex = (mHead - offset + mCapacity) % mCapacity;
      sample = mBuffer[physicalIndex];
      return true;
     }

   //+------------------------------------------------------------------+
   //| Getters for buffer status                                        |
   //+------------------------------------------------------------------+
   int               GetCount(void) const
     {
      return mCount;
     }
   int               GetCapacity(void) const
     {
      return mCapacity;
     }
   bool              IsReady(const int minSamplesRequired) const
     {
      return (mCount >= minSamplesRequired);
     }
  };

#endif // SCALPER_GUARD_CIRCULAR_TICK_BUFFER_MQH

GetOldestInWindow() traverses backward across active samples (where sample count k <= active count) to locate the oldest tick within the requested window duration and report the tick arrival count (outTicksInWindow). Reverse chronological access is provided by GetSampleByOffset().

Kinetic Velocity Tracking: TickVelocityTracker.mqh

CTickVelocityTracker computes mid-price displacement per second and tick arrival frequency over rolling sliding windows to separate momentum impulses from microstructure noise.

//+------------------------------------------------------------------+
//|                                          TickVelocityTracker.mqh |
//|                          Copyright 2026, Wanateki Solutions Ltd. |
//|                                         https://www.wanateki.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Wanateki Solutions Ltd."
#property link      "https://www.wanateki.com"

#ifndef SCALPER_GUARD_TICK_VELOCITY_TRACKER_MQH
#define SCALPER_GUARD_TICK_VELOCITY_TRACKER_MQH

#include "CoreDefines.mqh"
#include "CircularTickBuffer.mqh"

//+------------------------------------------------------------------+
//| Class: CTickVelocityTracker                                      |
//| Computes kinetic price displacement and tick frequency in O(k)   |
//+------------------------------------------------------------------+
class CTickVelocityTracker
  {
private:
   double            mSymbolPoint;             // Symbol point size (e.g. 0.00001 or 0.01)
   double            mRollingWindowSeconds;    // Rolling time window duration in seconds
   double            mMinVelocityThreshold;    // Minimum allowable velocity in points/sec
   double            mLastCalculatedVelocity;  // Cached absolute velocity (points/sec)
   double            mLastSignedVelocity;      // Cached signed velocity (points/sec)
   double            mLastTickFrequency;       // Cached tick frequency (ticks/sec)

Initialization sets calculation window duration and minimum velocity thresholds:

public:
   //+------------------------------------------------------------------+
   //| Constructor                                                      |
   //+------------------------------------------------------------------+
                     CTickVelocityTracker(void)
     {
      mSymbolPoint = 0.00001;
      mRollingWindowSeconds = DEFAULT_VELOCITY_WINDOW_SEC;
      mMinVelocityThreshold = DEFAULT_MIN_VELOCITY_PTS_SEC;
      mLastCalculatedVelocity = 0.0;
      mLastSignedVelocity = 0.0;
      mLastTickFrequency = 0.0;
     }

   //+------------------------------------------------------------------+
   //| Destructor                                                       |
   //+------------------------------------------------------------------+
                    ~CTickVelocityTracker(void)
     {
     }

   //+------------------------------------------------------------------+
   //| Initializes tracker parameters                                   |
   //+------------------------------------------------------------------+
   bool              Initialize(const double pointSize, const double windowSeconds, const double minVelocityThreshold)
     {
      if(pointSize <= 0.0 || windowSeconds <= 0.0)
        {
         return false;
        }
      mSymbolPoint = pointSize;
      mRollingWindowSeconds = windowSeconds;
      mMinVelocityThreshold = minVelocityThreshold;
      mLastCalculatedVelocity = 0.0;
      mLastSignedVelocity = 0.0;
      mLastTickFrequency = 0.0;
      return true;
     }

Velocity and tick arrival frequency are evaluated over the rolling buffer:

   //+------------------------------------------------------------------+
   //| Computes price velocity and arrival frequency over rolling window|
   //+------------------------------------------------------------------+
   bool              ComputeVelocity(const CCircularTickBuffer *buffer, double &outAbsVelocity, double &outSignedVelocity)
     {
      if(CheckPointer(buffer) == POINTER_INVALID)
        {
         outAbsVelocity = 0.0;
         outSignedVelocity = 0.0;
         mLastTickFrequency = 0.0;
         return false;
        }

      STickSample latestSample;
      if(!buffer.GetLatest(latestSample))
        {
         outAbsVelocity = 0.0;
         outSignedVelocity = 0.0;
         mLastTickFrequency = 0.0;
         return false;
        }

      ulong windowMs = (ulong)(mRollingWindowSeconds * 1000.0);
      STickSample oldestSample;
      int ticksInWindow = 0;
      if(!buffer.GetOldestInWindow(windowMs, latestSample.mTimestampMs, oldestSample, ticksInWindow))
        {
         outAbsVelocity = 0.0;
         outSignedVelocity = 0.0;
         mLastTickFrequency = 0.0;
         return false;
        }

      double actualElapsedSeconds = (double)(latestSample.mTimestampMs - oldestSample.mTimestampMs) / 1000.0;
      double elapsedSeconds = actualElapsedSeconds;
      if(elapsedSeconds < 0.10)
        {
         elapsedSeconds = 0.10; // Floor safeguard (100 ms) against artificial velocity spikes from sub-millisecond tick bursts
        }

      double deltaPricePoints = (latestSample.mMidPrice - oldestSample.mMidPrice) / mSymbolPoint;
      outSignedVelocity = deltaPricePoints / elapsedSeconds;
      outAbsVelocity = MathAbs(outSignedVelocity);

      //--- Arrival frequency: ticks received divided by window time span (minimum 0.10s)
      mLastTickFrequency = (actualElapsedSeconds > 0.0) ? ((double)ticksInWindow / actualElapsedSeconds) : ((double)ticksInWindow / elapsedSeconds);

      mLastCalculatedVelocity = outAbsVelocity;
      mLastSignedVelocity = outSignedVelocity;
      return true;
     }

Diagnostic getters expose calculated kinetic velocity, signed velocity, and arrival frequency:

   //+------------------------------------------------------------------+
   //| Getters for internal states                                      |
   //+------------------------------------------------------------------+
   double            GetLastVelocity(void) const
     {
      return mLastCalculatedVelocity;
     }
   double            GetLastSignedVelocity(void) const
     {
      return mLastSignedVelocity;
     }
   double            GetLastTickFrequency(void) const
     {
      return mLastTickFrequency;
     }
   double            GetMinVelocityThreshold(void) const
     {
      return mMinVelocityThreshold;
     }
   double            GetRollingWindowSeconds(void) const
     {
      return mRollingWindowSeconds;
     }
   bool              IsVelocitySufficient(void) const
     {
      return (mLastCalculatedVelocity >= mMinVelocityThreshold);
     }
  };

#endif // SCALPER_GUARD_TICK_VELOCITY_TRACKER_MQH

The 100 ms minimum elapsed time floor safeguards against artificial velocity spikes from sub-millisecond tick bursts. Mid-price displacement ((Bid + Ask) / 2.0) / Point filters out bid-ask bounce noise.

Statistical Spread Profiler: SpreadAnalyzer.mqh

CSpreadAnalyzer implements Welford's single-pass online algorithm to update the mean and variance in O(1) time per tick. After the transition threshold, it uses exponentially weighted moving variance to adapt to nonstationary volatility regimes.

//+------------------------------------------------------------------+
//|                                               SpreadAnalyzer.mqh |
//|                          Copyright 2026, Wanateki Solutions Ltd. |
//|                                         https://www.wanateki.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Wanateki Solutions Ltd."
#property link      "https://www.wanateki.com"

#ifndef SCALPER_GUARD_SPREAD_ANALYZER_MQH
#define SCALPER_GUARD_SPREAD_ANALYZER_MQH

#include "CoreDefines.mqh"

//+---------------------------------------------------------------------+
//| Class: CSpreadAnalyzer                                              |
//| Online Welford and EWMA spread-variance profiler and spike          |
//| detector calculating rolling mean, standard deviation, and Z-score  |
//+---------------------------------------------------------------------+
class CSpreadAnalyzer
  {
private:
   ulong             mSampleCount;            // Running count of processed samples
   double            mMeanSpread;             // Running sample mean spread (points)
   double            mM2;                     // Running sum of squared differences from mean
   double            mVariance;               // Running sample variance
   double            mStdDev;                 // Running sample standard deviation
   double            mZScoreLimit;            // Threshold Z-score indicating a statistical anomaly
   double            mHardSpreadLimitPoints;  // Hard absolute cap on spread in points
   double            mMinStdDevFloor;         // Floor to prevent division by zero in calm markets
   int               mWarmupTicks;            // Minimum ticks required before evaluating anomaly Z-score
   double            mCurrentSpreadPoints;    // Cached current spread
   double            mCurrentZScore;          // Cached current Z-score

Initialization establishes statistical limits and the minimum warm-up sample count:

public:
   //+------------------------------------------------------------------+
   //| Constructor                                                      |
   //+------------------------------------------------------------------+
                     CSpreadAnalyzer(void)
     {
      mSampleCount = 0;
      mMeanSpread = 0.0;
      mM2 = 0.0;
      mVariance = 0.0;
      mStdDev = 0.0;
      mZScoreLimit = DEFAULT_SPREAD_ZSCORE_LIMIT;
      mHardSpreadLimitPoints = DEFAULT_HARD_SPREAD_MAX_PTS;
      mMinStdDevFloor = 0.1;
      mWarmupTicks = DEFAULT_WARMUP_TICKS;
      mCurrentSpreadPoints = 0.0;
      mCurrentZScore = 0.0;
     }

   //+------------------------------------------------------------------+
   //| Destructor                                                       |
   //+------------------------------------------------------------------+
                    ~CSpreadAnalyzer(void)
     {
     }

   //+------------------------------------------------------------------+
   //| Initializes statistical bounds and limits                        |
   //+------------------------------------------------------------------+
   bool              Initialize(const double zScoreLimit, const double hardSpreadLimitPoints, const double minStdDevFloor = 0.1, const int warmupTicks = DEFAULT_WARMUP_TICKS)
     {
      if(zScoreLimit <= 0.0 || hardSpreadLimitPoints <= 0.0 || minStdDevFloor <= 0.0 || warmupTicks <= 1)
        {
         return false;
        }
      mSampleCount = 0;
      mMeanSpread = 0.0;
      mM2 = 0.0;
      mVariance = 0.0;
      mStdDev = 0.0;
      mZScoreLimit = zScoreLimit;
      mHardSpreadLimitPoints = hardSpreadLimitPoints;
      mMinStdDevFloor = minStdDevFloor;
      mWarmupTicks = warmupTicks;
      mCurrentSpreadPoints = 0.0;
      mCurrentZScore = 0.0;
      return true;
     }

   //+------------------------------------------------------------------+
   //| Resets statistical accumulators                                  |
   //+------------------------------------------------------------------+
   void              Reset(void)
     {
      mSampleCount = 0;
      mMeanSpread = 0.0;
      mM2 = 0.0;
      mVariance = 0.0;
      mStdDev = 0.0;
      mCurrentSpreadPoints = 0.0;
      mCurrentZScore = 0.0;
     }

Online statistical updates follow Welford recurrence during calibration, switching to EWMA after transition ticks:

   //+------------------------------------------------------------------+
   //| Updates distribution parameters using Adaptive Welford algorithm |
   //+------------------------------------------------------------------+
   void              UpdateDistribution(const double currentSpreadPoints)
     {
      mCurrentSpreadPoints = currentSpreadPoints;
      mSampleCount++;

      if(mSampleCount == 1)
        {
         mMeanSpread = currentSpreadPoints;
         mM2 = 0.0;
         mVariance = 0.0;
         mStdDev = 0.0;
         mCurrentZScore = 0.0;
         return;
        }

      //--- Before the transition observation, use the standard Welford accumulator. (Welford 1962)
      if(mSampleCount < (ulong)DEFAULT_EWMA_TRANSITION_TICKS)
        {
         double delta = currentSpreadPoints - mMeanSpread;
         mMeanSpread += delta / (double)mSampleCount;
         double delta2 = currentSpreadPoints - mMeanSpread;
         mM2 += delta * delta2;
         mVariance = mM2 / (double)(mSampleCount - 1);
         mStdDev = MathSqrt(mVariance);
        }
      else
        {
         //--- Beyond the transition threshold, switch to exponentially weighted moving variance
         //--- with alpha = 0.002 and an effective memory horizon of approximately 500 ticks.
         //--- Online sample variance with moving mean recurrence follows Tony Finch (2009).
         //--- Prevents sample saturation while enabling gradual adaptation to nonstationary market regimes.
         double alpha = 0.002;
         double delta = currentSpreadPoints - mMeanSpread;
         mMeanSpread += alpha * delta;
         mVariance = (1.0 - alpha) * (mVariance + alpha * delta * delta);
         mStdDev = MathSqrt(mVariance);
        }

      mCurrentZScore = ComputeZScore(currentSpreadPoints);
     }

Spike detection computes instantaneous Z-scores against a standard deviation floor:

   //+------------------------------------------------------------------+
   //| Computes the statistical Z-score of a given spread               |
   //+------------------------------------------------------------------+
   double            ComputeZScore(const double currentSpreadPoints) const
     {
      if(mSampleCount < (ulong)mWarmupTicks)
        {
         return 0.0; // Calibration warmup period
        }
      //--- On zero-spread / raw ECN accounts, rolling mean spread is often 0.0 to 2.0 points.
      //--- If mean is low, dynamically elevate effective standard deviation floor to prevent false explosions on 0.1 pip ticks.
      double effectiveFloor = mMinStdDevFloor;
      if(mMeanSpread <= 2.0 && effectiveFloor < 1.0)
        {
         effectiveFloor = 1.0;
        }
      double denominator = (mStdDev > effectiveFloor) ? mStdDev : effectiveFloor;
      return (currentSpreadPoints - mMeanSpread) / denominator;
     }

   //+------------------------------------------------------------------+
   //| Evaluates if the current spread is a statistical or hard anomaly |
   //+------------------------------------------------------------------+
   bool              IsSpreadSpike(const double currentSpreadPoints, double &outZScore)
     {
      outZScore = ComputeZScore(currentSpreadPoints);
      mCurrentZScore = outZScore;

      //--- Hard limit check
      if(currentSpreadPoints > mHardSpreadLimitPoints)
        {
         return true;
        }

      //--- Statistical outlier check (after initial warmup)
      if(mSampleCount >= (ulong)mWarmupTicks && outZScore > mZScoreLimit)
        {
         //--- On zero-spread / tight ECN accounts (mean spread <= 2.0 points),
         //--- require an absolute spread expansion above 5.0 points and (mean + 3.0 points)
         //--- so micro-pip fluctuations (e.g. 0.0 to 1.0 pt) do not trigger false emergency lockouts.
         if(mMeanSpread <= 2.0)
           {
            if(currentSpreadPoints > 5.0 && currentSpreadPoints > (mMeanSpread + 3.0))
              {
               return true;
              }
            return false;
           }
         return true;
        }

      return false;
     }

   //+------------------------------------------------------------------+
   //| Indicates whether symbol displays zero-spread / raw ECN dynamics |
   //+------------------------------------------------------------------+
   bool              IsZeroSpreadProfile(void) const
     {
      return (mSampleCount >= (ulong)mWarmupTicks && mMeanSpread <= 2.0);
     }

Public accessors expose current spread distribution metrics:

   //+------------------------------------------------------------------+
   //| Getters                                                          |
   //+------------------------------------------------------------------+
   double            GetMean(void) const
     {
      return mMeanSpread;
     }
   double            GetStdDev(void) const
     {
      return mStdDev;
     }
   double            GetVariance(void) const
     {
      return mVariance;
     }
   ulong             GetSampleCount(void) const
     {
      return mSampleCount;
     }
   int               GetWarmupTicks(void) const
     {
      return mWarmupTicks;
     }
   double            GetHardSpreadLimit(void) const
     {
      return mHardSpreadLimitPoints;
     }
   double            GetZScoreLimit(void) const
     {
      return mZScoreLimit;
     }
   double            GetCurrentSpread(void) const
     {
      return mCurrentSpreadPoints;
     }
   double            GetCurrentZScore(void) const
     {
      return mCurrentZScore;
     }
  };

#endif // SCALPER_GUARD_SPREAD_ANALYZER_MQH

The dynamic floor prevents zero-division explosions on tight ECN pairs (mean spread <= 2.0 points), requiring both an elevated Z-score and an absolute expansion over 5.0 points before triggering an emergency lockout.

Operational Regime Classifier: FrictionClassifier.mqh

CFrictionClassifier evaluates the Friction Ratio, spread Z-score, and kinetic price velocity to map execution conditions into the four operational regimes.

//+------------------------------------------------------------------+
//|                                           FrictionClassifier.mqh |
//|                          Copyright 2026, Wanateki Solutions Ltd. |
//|                                         https://www.wanateki.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Wanateki Solutions Ltd."
#property link      "https://www.wanateki.com"

#ifndef SCALPER_GUARD_FRICTION_CLASSIFIER_MQH
#define SCALPER_GUARD_FRICTION_CLASSIFIER_MQH

#include "CoreDefines.mqh"

//+------------------------------------------------------------------+
//| Class: CFrictionClassifier                                       |
//| Friction ratio calculator and 4-regime operational state machine |
//+------------------------------------------------------------------+
class CFrictionClassifier
  {
private:
   double            mMaxAllowableFrictionRatio; // Maximum permitted friction ratio (e.g. 0.65)
   double            mExpectedHoldingTimeSec;    // Strategy target trade duration in seconds (e.g. 10.0s)
   double            mMinExpectedMovePoints;     // Strategy baseline profit target in points (e.g. 10.0 pts)
   double            mSlippageEstimatePoints;    // Base expected one-way slippage penalty in points (e.g. 2.0 pts)
   double            mMinVelocityPointsPerSec;   // Minimum required price velocity threshold (e.g. 0.5 pts/s)
   double            mCommissionEquivalentPoints;// Commission cost converted into equivalent symbol points
   double            mSpreadZScoreLimit;         // Configurable Z-score anomaly limit (e.g. 2.5)

Initialization configures friction ceilings, velocity hurdles, and commission offsets:

public:
   //+------------------------------------------------------------------+
   //| Constructor                                                      |
   //+------------------------------------------------------------------+
                     CFrictionClassifier(void)
     {
      mMaxAllowableFrictionRatio = DEFAULT_MAX_FRICTION_RATIO;
      mExpectedHoldingTimeSec = DEFAULT_EXPECTED_HOLDING_SEC;
      mMinExpectedMovePoints = DEFAULT_MIN_EXPECTED_MOVE_PTS;
      mSlippageEstimatePoints = DEFAULT_SLIPPAGE_ESTIMATE_PTS;
      mMinVelocityPointsPerSec = DEFAULT_MIN_VELOCITY_PTS_SEC;
      mCommissionEquivalentPoints = 0.0;
      mSpreadZScoreLimit = DEFAULT_SPREAD_ZSCORE_LIMIT;
     }

   //+------------------------------------------------------------------+
   //| Destructor                                                       |
   //+------------------------------------------------------------------+
                    ~CFrictionClassifier(void)
     {
     }

   //+------------------------------------------------------------------+
   //| Initializes friction classification parameters                   |
   //+------------------------------------------------------------------+
   bool              Initialize(const double maxFrictionRatio,
                   const double expectedHoldingTimeSec,
                   const double minExpectedMovePoints,
                   const double slippageEstimatePoints,
                   const double minVelocityPointsPerSec = DEFAULT_MIN_VELOCITY_PTS_SEC,
                   const double commissionEquivalentPoints = 0.0,
                   const double spreadZScoreLimit = DEFAULT_SPREAD_ZSCORE_LIMIT)
     {
      if(maxFrictionRatio <= 0.0 || expectedHoldingTimeSec <= 0.0 || minExpectedMovePoints <= 0.0)
        {
         return false;
        }
      mMaxAllowableFrictionRatio = maxFrictionRatio;
      mExpectedHoldingTimeSec = expectedHoldingTimeSec;
      mMinExpectedMovePoints = minExpectedMovePoints;
      mSlippageEstimatePoints = slippageEstimatePoints;
      mMinVelocityPointsPerSec = (minVelocityPointsPerSec > 0.0) ? minVelocityPointsPerSec : DEFAULT_MIN_VELOCITY_PTS_SEC;
      mCommissionEquivalentPoints = (commissionEquivalentPoints >= 0.0) ? commissionEquivalentPoints : 0.0;
      mSpreadZScoreLimit = (spreadZScoreLimit > 0.0) ? spreadZScoreLimit : DEFAULT_SPREAD_ZSCORE_LIMIT;
      return true;
     }

   //+------------------------------------------------------------------+
   //| Sets the commission cost in equivalent symbol points             |
   //+------------------------------------------------------------------+
   void              SetCommissionEquivalentPoints(const double points)
     {
      mCommissionEquivalentPoints = (points >= 0.0) ? points : 0.0;
     }

   //+------------------------------------------------------------------+
   //| Retrieves the active commission cost in points                   |
   //+------------------------------------------------------------------+
   double            GetCommissionEquivalentPoints(void) const
     {
      return mCommissionEquivalentPoints;
     }

The Friction Ratio divides total expected transaction friction by expected kinetic price displacement:

   //+------------------------------------------------------------------+
   //| Computes the dimensionless Friction Ratio (FR)                   |
   //+------------------------------------------------------------------+
   double            CalculateFrictionRatio(const double currentSpreadPoints, const double tickVelocityPointsPerSec) const
     {
      double totalFrictionCost = currentSpreadPoints + mCommissionEquivalentPoints + mSlippageEstimatePoints;
      double expectedPriceMovement = tickVelocityPointsPerSec * mExpectedHoldingTimeSec;

      //--- Enforce an epsilon floor to prevent division by zero while preserving authentic friction penalty
      if(expectedPriceMovement < 0.1)
        {
         expectedPriceMovement = 0.1;
        }

      return (totalFrictionCost / expectedPriceMovement);
     }

Market regimes are resolved through the hierarchical decision tree:

   //+------------------------------------------------------------------+
   //| Classifies the market state into one of four operational regimes |
   //+------------------------------------------------------------------+
   ENUM_MARKET_REGIME ClassifyRegime(const double currentSpreadPoints,
                                     const double spreadZScore,
                                     const double tickVelocityPointsPerSec,
                                     const bool isSpike,
                                     ENUM_REJECTION_REASON &outReason,
                                     double &outFrictionRatio) const
     {
      outFrictionRatio = CalculateFrictionRatio(currentSpreadPoints, tickVelocityPointsPerSec);

      //--- 1. Check for statistical spread spike or hard spread ceiling breach
      if(isSpike)
        {
         outReason = (spreadZScore > mSpreadZScoreLimit) ? REASON_SPREAD_SPIKE : REASON_HARD_SPREAD_MAX;
         return REGIME_SPREAD_SPIKE;
        }

      //--- 2. Check for insufficient kinetic price velocity
      if(tickVelocityPointsPerSec < mMinVelocityPointsPerSec)
        {
         outReason = REASON_LOW_VELOCITY;
         return REGIME_FRICTION_LOCKED;
        }

      //--- 3. Check for prohibitive friction ratio
      if(outFrictionRatio > mMaxAllowableFrictionRatio)
        {
         //--- Differentiate between spread friction and fixed commission hurdle on zero-spread accounts
         if(mCommissionEquivalentPoints > 0.0 && currentSpreadPoints <= 2.0)
           {
            outReason = REASON_COMMISSION_HURDLE;
           }
         else
           {
            outReason = REASON_HIGH_FRICTION;
           }
         return REGIME_FRICTION_LOCKED;
        }

      //--- 4. Optimal execution conditions: tight spread and low friction
      if(outFrictionRatio <= 0.35 && spreadZScore <= 1.0)
        {
         outReason = REASON_NONE;
         return REGIME_OPTIMAL;
        }

      //--- 5. Normal / Moderate conditions
      outReason = REASON_NONE;
      return REGIME_MODERATE;
     }

Asset scale validation catches digit and volatility misconfigurations at startup:

   //+------------------------------------------------------------------+
   //| Validates input parameter scaling against symbol precision & targets |
   //+------------------------------------------------------------------+
   bool              ValidateScale(const SSymbolProfile &profile,
                      const double takeProfitPoints,
                      const double minVelocityPointsPerSec,
                      const double hardSpreadLimitPoints,
                      string &outWarningMessage) const
     {
      outWarningMessage = "";

      //--- 1. Check Take Profit vs MinExpectedMove consistency
      if(takeProfitPoints > 0.0)
        {
         if(mMinExpectedMovePoints < (takeProfitPoints * 0.20))
           {
            outWarningMessage = StringFormat(
                                   "MinExpectedMove (%.1f pts) is < 20%% of TP (%.1f pts). Set MinExpectedMove >= %.1f pts.",
                                   mMinExpectedMovePoints, takeProfitPoints, takeProfitPoints * 0.20
                                );
            return false;
           }
        }

      //--- 2. Check 2-digit / 1-digit / 0-digit scale mismatch
      if(profile.mDigits <= 2)
        {
         if(minVelocityPointsPerSec < 1.0)
           {
            outWarningMessage = StringFormat(
                                   "MinVelocity (%.2f pts/s) is scaled for 5-digit FX. For %d-digit %s, set MinVelocity >= 5.0 - 15.0 pts/s.",
                                   minVelocityPointsPerSec, profile.mDigits, profile.mSymbol
                                );
            return false;
           }
         if(mMinExpectedMovePoints < 20.0 && takeProfitPoints >= 50.0)
           {
            outWarningMessage = StringFormat(
                                   "MinExpectedMove (%.1f pts) is too low for %d-digit asset. Set MinExpectedMove >= 50.0 - 150.0 pts.",
                                   mMinExpectedMovePoints, profile.mDigits
                                );
            return false;
           }
        }

      //--- 3. Check hard spread limit vs observed spread
      if(profile.mTypicalSpreadPts > 0.0 && hardSpreadLimitPoints < profile.mTypicalSpreadPts)
        {
         outWarningMessage = StringFormat(
                                "HardSpreadMax (%.1f pts) is less than live spread (%.1f pts). Set HardSpreadMax > %.1f pts.",
                                hardSpreadLimitPoints, profile.mTypicalSpreadPts, profile.mTypicalSpreadPts * 1.5
                             );
         return false;
        }

      return true;
     }

Accessors provide external inspection of classification parameters:

   //+------------------------------------------------------------------+
   //| Getters                                                          |
   //+------------------------------------------------------------------+
   double            GetMaxAllowableFrictionRatio(void) const
     {
      return mMaxAllowableFrictionRatio;
     }
   double            GetExpectedHoldingTimeSec(void) const
     {
      return mExpectedHoldingTimeSec;
     }
   double            GetMinExpectedMovePoints(void) const
     {
      return mMinExpectedMovePoints;
     }
   double            GetSlippageEstimatePoints(void) const
     {
      return mSlippageEstimatePoints;
     }
   double            GetSpreadZScoreLimit(void) const
     {
      return mSpreadZScoreLimit;
     }
   void              SetSpreadZScoreLimit(const double limit)
     {
      mSpreadZScoreLimit = (limit > 0.0) ? limit : DEFAULT_SPREAD_ZSCORE_LIMIT;
     }
  };

#endif // SCALPER_GUARD_FRICTION_CLASSIFIER_MQH

Note that in this article (Part 1), mMinExpectedMovePoints serves as an input-scale sanity check; the Friction Ratio denominator is driven by the short-horizon kinetic velocity projection.


Master Facade: FrictionGuard.mqh

CFrictionGuard encapsulates the circular buffer, kinetic velocity tracker, adaptive spread profiler, and regime classifier behind the Facade pattern. The Expert Advisor interacts with the friction engine through three calls:

  1. Initialize() during OnInit() to configure thresholds and normalize broker fees.
  2. ProcessTick() during OnTick() to stream market ticks into the analytical pipeline.
  3. IsSafeToTrade() before dispatching an order to evaluate execution clearance.

Architecture and Subsystem Aggregation

//+------------------------------------------------------------------+
//|                                                FrictionGuard.mqh |
//|                          Copyright 2026, Wanateki Solutions Ltd. |
//|                                         https://www.wanateki.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Wanateki Solutions Ltd."
#property link      "https://www.wanateki.com"

#ifndef SCALPER_GUARD_FRICTION_GUARD_MQH
#define SCALPER_GUARD_FRICTION_GUARD_MQH

#include "CoreDefines.mqh"
#include "CircularTickBuffer.mqh"
#include "TickVelocityTracker.mqh"
#include "SpreadAnalyzer.mqh"
#include "FrictionClassifier.mqh"

//--- Telemetry dashboard integration with standalone fallback stub
#ifndef DISABLE_TELEMETRY_DASHBOARD
#include "TelemetryDashboard.mqh"
#else
//--- Lightweight headless stub dashboard for standalone or test execution
class CTelemetryDashboard
  {
public:
   bool              Initialize(const string, const bool, const string, const string, const int, const int, const int, const int) { return true; }
   void              Render(const SFrictionMetrics&) {}
   void              Clear(void) {}
   void              OnChartEvent(const int, const long&, const double&, const string&) {}
  };
#endif

//+------------------------------------------------------------------+
//| Class: CFrictionGuard                                            |
//| Master Pre-Trade Friction Gatekeeper & Analytical Facade         |
//+------------------------------------------------------------------+
class CFrictionGuard
  {
private:
   string                mSymbol;                 // Monitored symbol name
   double                mPoint;                  // Symbol point size
   int                   mDigits;                 // Symbol digits precision
   int                   mMinWarmupTicks;         // Minimum samples needed before gating
   double                mSlippageEstimatePoints; // Base slippage points configured
   bool                  mIsInitialized;          // Status flag

   SSymbolProfile        mProfile;            // Auto-detected symbol profile metadata
   CCircularTickBuffer  *mBuffer;             // Pre-allocated ring buffer
   CTickVelocityTracker *mVelocityTracker;    // Price velocity and momentum tracker
   CSpreadAnalyzer      *mSpreadAnalyzer;     // Welford variance spread profiler
   CFrictionClassifier  *mClassifier;         // Friction ratio and regime classifier
   CTelemetryDashboard  *mDashboard;          // On-chart visual telemetry HUD

   SFrictionMetrics      mCurrentMetrics;     // Cached telemetry and performance snapshot

   //+------------------------------------------------------------------+
   //| Safely releases all heap-allocated objects and nulls pointers    |
   //+------------------------------------------------------------------+
   void              CleanMemory(void)
     {
      if(CheckPointer(mBuffer) == POINTER_DYNAMIC)
        {
         delete mBuffer;
         mBuffer = NULL;
        }
      if(CheckPointer(mVelocityTracker) == POINTER_DYNAMIC)
        {
         delete mVelocityTracker;
         mVelocityTracker = NULL;
        }
      if(CheckPointer(mSpreadAnalyzer) == POINTER_DYNAMIC)
        {
         delete mSpreadAnalyzer;
         mSpreadAnalyzer = NULL;
        }
      if(CheckPointer(mClassifier) == POINTER_DYNAMIC)
        {
         delete mClassifier;
         mClassifier = NULL;
        }
      if(CheckPointer(mDashboard) == POINTER_DYNAMIC)
        {
         delete mDashboard;
         mDashboard = NULL;
        }
      mIsInitialized = false;
     }

The destructor and CleanMemory() method audit pointers with CheckPointer() and delete dynamic heap objects to prevent memory leaks upon EA deinitialization:

public:
   //+------------------------------------------------------------------+
   //| Constructor                                                      |
   //+------------------------------------------------------------------+
                     CFrictionGuard(void)
     {
      mSymbol = "";
      mPoint = 0.00001;
      mDigits = 5;
      mMinWarmupTicks = DEFAULT_WARMUP_TICKS;
      mSlippageEstimatePoints = DEFAULT_SLIPPAGE_ESTIMATE_PTS;
      mIsInitialized = false;

      ZeroMemory(mProfile);
      mBuffer = NULL;
      mVelocityTracker = NULL;
      mSpreadAnalyzer = NULL;
      mClassifier = NULL;
      mDashboard = NULL;

      ZeroMemory(mCurrentMetrics);
     }

   //+------------------------------------------------------------------+
   //| Destructor                                                       |
   //+------------------------------------------------------------------+
                    ~CFrictionGuard(void)
     {
      CleanMemory();
     }

Initialization queries symbol metadata and converts lot commissions into equivalent symbol points:

   //+------------------------------------------------------------------+
   //| Initializes the master controller and all sub-components         |
   //+------------------------------------------------------------------+
   bool              Initialize(const string symbol,
                   const int    bufferCapacity           = DEFAULT_BUFFER_CAPACITY,
                   const double velocityWindowSeconds    = DEFAULT_VELOCITY_WINDOW_SEC,
                   const double minVelocityPointsPerSec  = DEFAULT_MIN_VELOCITY_PTS_SEC,
                   const double spreadZScoreLimit        = DEFAULT_SPREAD_ZSCORE_LIMIT,
                   const double hardSpreadLimitPoints    = DEFAULT_HARD_SPREAD_MAX_PTS,
                   const double maxFrictionRatio         = DEFAULT_MAX_FRICTION_RATIO,
                   const double expectedHoldingSeconds   = DEFAULT_EXPECTED_HOLDING_SEC,
                   const double minExpectedMovePoints    = DEFAULT_MIN_EXPECTED_MOVE_PTS,
                   const double slippageEstimatePoints   = DEFAULT_SLIPPAGE_ESTIMATE_PTS,
                   const bool   enableDashboard          = true,
                   const string dashboardTitle           = "INTELLIGENT FRICTION GUARD",
                   const double strategyTakeProfitPoints = 0.0,
                   const double commissionEquivalentPoints = 0.0,
                   const double commissionPerLot         = 0.0,
                   const int    warmupTicks              = DEFAULT_WARMUP_TICKS,
                   const int    dashboardX               = 15,
                   const int    dashboardY               = 20,
                   const int    dashboardWidth           = 540,
                   const int    dashboardFontSize        = 8)
     {
      //--- Clean any previously active instances
      CleanMemory();

      mSymbol = (symbol == "" || symbol == NULL) ? _Symbol : symbol;
      mPoint = SymbolInfoDouble(mSymbol, SYMBOL_POINT);
      mDigits = (int)SymbolInfoInteger(mSymbol, SYMBOL_DIGITS);
      mSlippageEstimatePoints = slippageEstimatePoints;

      if(mPoint <= 0.0)
        {
         mPoint = (_Point > 0.0) ? _Point : 0.00001;
        }

      //--- Auto-detect symbol profile metadata
      mProfile.mSymbol = mSymbol;
      mProfile.mDigits = mDigits;
      mProfile.mPoint = mPoint;
      mProfile.mTickSize = SymbolInfoDouble(mSymbol, SYMBOL_TRADE_TICK_SIZE);
      mProfile.mTickValue = SymbolInfoDouble(mSymbol, SYMBOL_TRADE_TICK_VALUE);
      mProfile.mSpreadType = (SymbolInfoInteger(mSymbol, SYMBOL_SPREAD_FLOAT) != 0) ? SPREAD_TYPE_FLOATING : SPREAD_TYPE_FIXED;
      mProfile.mStopsLevel = (int)SymbolInfoInteger(mSymbol, SYMBOL_TRADE_STOPS_LEVEL);
      mProfile.mFreezeLevel = (int)SymbolInfoInteger(mSymbol, SYMBOL_TRADE_FREEZE_LEVEL);

      MqlTick curTick;
      if(SymbolInfoTick(mSymbol, curTick) && mPoint > 0.0)
        {
         mProfile.mTypicalSpreadPts = (curTick.ask - curTick.bid) / mPoint;
        }
      else
        {
         mProfile.mTypicalSpreadPts = (double)SymbolInfoInteger(mSymbol, SYMBOL_SPREAD);
        }
      mProfile.mIsZeroSpreadAccount = (mProfile.mTypicalSpreadPts <= 2.0);
      mProfile.mCommissionPerLot = commissionPerLot;

      //--- Calculate commission equivalent points from per-lot value if points not explicitly supplied
      if(commissionEquivalentPoints > 0.0)
        {
         mProfile.mCommissionPoints = commissionEquivalentPoints;
        }
      else
         if(commissionPerLot > 0.0 && mProfile.mTickValue > 0.0 && mProfile.mTickSize > 0.0)
           {
            double pointsPerUnit = mPoint / mProfile.mTickSize;
            double pointValue = mProfile.mTickValue * pointsPerUnit;
            if(pointValue > 0.0)
              {
               mProfile.mCommissionPoints = commissionPerLot / pointValue;
              }
            else
              {
               mProfile.mCommissionPoints = 0.0;
              }
           }
         else
           {
            mProfile.mCommissionPoints = 0.0;
           }

The second phase allocates analytical modules and validates parameter scaling against symbol precision:

   //--- 1. Allocate and initialize Circular Buffer
      mBuffer = new CCircularTickBuffer();
      if(CheckPointer(mBuffer) == POINTER_INVALID || !mBuffer.Initialize(bufferCapacity))
        {
         Print("[-] CFrictionGuard: Failed to allocate/initialize CCircularTickBuffer.");
         CleanMemory();
         return false;
        }

      //--- 2. Allocate and initialize Velocity Tracker
      mVelocityTracker = new CTickVelocityTracker();
      if(CheckPointer(mVelocityTracker) == POINTER_INVALID ||
         !mVelocityTracker.Initialize(mPoint, velocityWindowSeconds, minVelocityPointsPerSec))
        {
         Print("[-] CFrictionGuard: Failed to allocate/initialize CTickVelocityTracker.");
         CleanMemory();
         return false;
        }

      //--- 3. Allocate and initialize Spread Analyzer
      mSpreadAnalyzer = new CSpreadAnalyzer();
      if(CheckPointer(mSpreadAnalyzer) == POINTER_INVALID ||
         !mSpreadAnalyzer.Initialize(spreadZScoreLimit, hardSpreadLimitPoints, 0.1, warmupTicks))
        {
         Print("[-] CFrictionGuard: Failed to allocate/initialize CSpreadAnalyzer.");
         CleanMemory();
         return false;
        }

      //--- 4. Allocate and initialize Friction Classifier
      mClassifier = new CFrictionClassifier();
      if(CheckPointer(mClassifier) == POINTER_INVALID ||
         !mClassifier.Initialize(maxFrictionRatio, expectedHoldingSeconds, minExpectedMovePoints, slippageEstimatePoints, minVelocityPointsPerSec, mProfile.mCommissionPoints, spreadZScoreLimit))
        {
         Print("[-] CFrictionGuard: Failed to allocate/initialize CFrictionClassifier.");
         CleanMemory();
         return false;
        }

      //--- 5. Validate parameter scaling against symbol digits and strategy targets
      string scaleWarning = "";
      bool isScaleValid = mClassifier.ValidateScale(mProfile, strategyTakeProfitPoints, minVelocityPointsPerSec, hardSpreadLimitPoints, scaleWarning);

      //--- 6. Allocate and initialize Telemetry Dashboard (if enabled)
      if(enableDashboard)
        {
         mDashboard = new CTelemetryDashboard();
         if(CheckPointer(mDashboard) == POINTER_INVALID ||
            !mDashboard.Initialize(mSymbol, true, dashboardTitle, "FrictionHUD_", dashboardX, dashboardY, dashboardWidth, dashboardFontSize))
           {
            Print("[-] CFrictionGuard: Failed to allocate/initialize CTelemetryDashboard.");
            CleanMemory();
            return false;
           }
        }

      mMinWarmupTicks = (warmupTicks > 1) ? warmupTicks : DEFAULT_WARMUP_TICKS;
      if(bufferCapacity < mMinWarmupTicks)
        {
         mMinWarmupTicks = bufferCapacity;
        }

      ZeroMemory(mCurrentMetrics);
      mCurrentMetrics.mDigits = mDigits;
      mCurrentMetrics.mPoint = mPoint;
      mCurrentMetrics.mSpreadType = mProfile.mSpreadType;
      mCurrentMetrics.mCommissionPoints = mProfile.mCommissionPoints;
      mCurrentMetrics.mIsScaleValid = isScaleValid;
      mCurrentMetrics.mScaleWarningMessage = scaleWarning;

      if(!isScaleValid)
        {
         PrintFormat("[!] CFrictionGuard WARNING on %s: %s (Trading PAUSED)", mSymbol, scaleWarning);
         mCurrentMetrics.mRegime = REGIME_FRICTION_LOCKED;
         mCurrentMetrics.mReason = REASON_SCALE_MISMATCH;
         mCurrentMetrics.mIsSafeToTrade = false;
        }
      else
        {
         mCurrentMetrics.mRegime = REGIME_OPTIMAL;
         mCurrentMetrics.mReason = REASON_NONE;
         mCurrentMetrics.mIsSafeToTrade = true;
        }

      mIsInitialized = true;
      return true;
     }

Real-Time Tick Ingestion and Pre-Trade Clearance

ProcessTick() ingests raw quotes, screens for spread spikes before distribution updates, and recalculates velocity and market regimes in O(k) time:

   //+------------------------------------------------------------------+
   //| Ingests raw market tick into analytical pipeline                 |
   //+------------------------------------------------------------------+
   void              ProcessTick(const MqlTick &tick)
     {
      if(!mIsInitialized || CheckPointer(mBuffer) == POINTER_INVALID)
        {
         return;
        }

      //--- Pre-ingestion quote sanity validation: reject corrupted or crossed quotes
      if(tick.bid <= 0.0 || tick.ask <= 0.0 || tick.ask < tick.bid)
        {
         mCurrentMetrics.mReason = REASON_INVALID_TICK;
         mCurrentMetrics.mIsSafeToTrade = false;
         return;
        }

      //--- Pack raw tick into STickSample
      STickSample sample;
      sample.mTimestampMs = (tick.time_msc > 0) ? (ulong)tick.time_msc : ((ulong)tick.time * 1000);
      sample.mBid = tick.bid;
      sample.mAsk = tick.ask;
      sample.mMidPrice = (tick.bid + tick.ask) / 2.0;
      sample.mSpreadPoints = (mPoint > 0.0) ? (tick.ask - tick.bid) / mPoint : 0.0;
      sample.mVolume = (tick.volume_real > 0.0) ? tick.volume_real : (double)tick.volume;

      //--- 1. Evaluate spread anomaly BEFORE updating distribution so outlier tick does not pollute baseline
      double zScore = 0.0;
      bool isSpike = false;
      if(CheckPointer(mSpreadAnalyzer) != POINTER_INVALID)
        {
         isSpike = mSpreadAnalyzer.IsSpreadSpike(sample.mSpreadPoints, zScore);
        }

      //--- 2. Push sample into ring buffer
      mBuffer.Push(sample);

      //--- 3. Update Welford / EWMA spread distribution
      if(CheckPointer(mSpreadAnalyzer) != POINTER_INVALID)
        {
         mSpreadAnalyzer.UpdateDistribution(sample.mSpreadPoints);
        }

      //--- 4. Compute real-time kinetic velocity
      double absVelocity = 0.0;
      double signedVelocity = 0.0;
      if(CheckPointer(mVelocityTracker) != POINTER_INVALID)
        {
         mVelocityTracker.ComputeVelocity(mBuffer, absVelocity, signedVelocity);
        }

      //--- 5. Update cached metrics
      mCurrentMetrics.mCurrentSpreadPoints = sample.mSpreadPoints;
      if(CheckPointer(mSpreadAnalyzer) != POINTER_INVALID)
        {
         mCurrentMetrics.mMeanSpreadPoints = mSpreadAnalyzer.GetMean();
         mCurrentMetrics.mStdDevSpreadPoints = mSpreadAnalyzer.GetStdDev();
         mCurrentMetrics.mSpreadZScore = zScore;
         mCurrentMetrics.mIsZeroSpreadAccount = (mProfile.mIsZeroSpreadAccount || mSpreadAnalyzer.IsZeroSpreadProfile());
        }
      mCurrentMetrics.mTickVelocityPointsPerSec = absVelocity;
      mCurrentMetrics.mSignedVelocity = signedVelocity;
      if(CheckPointer(mVelocityTracker) != POINTER_INVALID)
        {
         mCurrentMetrics.mTickFrequency = mVelocityTracker.GetLastTickFrequency();
        }
      if(CheckPointer(mClassifier) != POINTER_INVALID)
        {
         mCurrentMetrics.mCommissionPoints = mClassifier.GetCommissionEquivalentPoints();
        }
      mCurrentMetrics.mEffectiveFrictionPoints = sample.mSpreadPoints + mCurrentMetrics.mCommissionPoints + mSlippageEstimatePoints;

      //--- 6. Compute friction ratio and test regime
      if(CheckPointer(mClassifier) != POINTER_INVALID)
        {
         ENUM_REJECTION_REASON reason = REASON_NONE;
         double frictionRatio = 0.0;

         ENUM_MARKET_REGIME regime = mClassifier.ClassifyRegime(
                                        sample.mSpreadPoints,
                                        zScore,
                                        absVelocity,
                                        isSpike,
                                        reason,
                                        frictionRatio
                                     );

         mCurrentMetrics.mRegime = regime;
         mCurrentMetrics.mReason = reason;
         mCurrentMetrics.mFrictionRatio = frictionRatio;
         mCurrentMetrics.mIsSafeToTrade = (regime == REGIME_OPTIMAL || regime == REGIME_MODERATE);

         //--- 6. If ring buffer has not accumulated warmup samples, mark unready
         if(CheckPointer(mBuffer) == POINTER_INVALID || !mBuffer.IsReady(mMinWarmupTicks))
           {
            mCurrentMetrics.mReason = REASON_BUFFER_NOT_READY;
            mCurrentMetrics.mIsSafeToTrade = false;
           }

         //--- 7. If input scaling is invalid, permanently lock out trading until parameters are corrected
         if(!mCurrentMetrics.mIsScaleValid)
           {
            mCurrentMetrics.mRegime = REGIME_FRICTION_LOCKED;
            mCurrentMetrics.mReason = REASON_SCALE_MISMATCH;
            mCurrentMetrics.mIsSafeToTrade = false;
           }
        }
     }

IsSafeToTrade() evaluates current regime clearance and optional directional momentum alignment:

   //+------------------------------------------------------------------+
   //| Pre-trade gatekeeper check: boolean clearance decision           |
   //| Supports optional directional momentum verification              |
   //+------------------------------------------------------------------+
   bool              IsSafeToTrade(const ENUM_ORDER_TYPE orderType = (ENUM_ORDER_TYPE)-1)
     {
      if(!mIsInitialized)
        {
         return false;
        }

      mCurrentMetrics.mTotalEvaluations++;

      //--- 1. Check out-of-scale safety lock
      if(!mCurrentMetrics.mIsScaleValid)
        {
         mCurrentMetrics.mReason = REASON_SCALE_MISMATCH;
         mCurrentMetrics.mBlockedEvaluations++;
         mCurrentMetrics.mIsSafeToTrade = false;
         return false;
        }

      //--- 2. Check if buffer has sufficient warmup ticks
      if(CheckPointer(mBuffer) == POINTER_INVALID || !mBuffer.IsReady(mMinWarmupTicks))
        {
         mCurrentMetrics.mReason = REASON_BUFFER_NOT_READY;
         mCurrentMetrics.mBlockedEvaluations++;
         mCurrentMetrics.mIsSafeToTrade = false;
         return false;
        }

      //--- 3. Evaluate current regime; a rejected quote invalidates the cached snapshot
      bool isSafe = (mCurrentMetrics.mRegime == REGIME_OPTIMAL || mCurrentMetrics.mRegime == REGIME_MODERATE)
                    && mCurrentMetrics.mReason != REASON_INVALID_TICK;

      //--- 4. Directional momentum check (if specified)
      if(isSafe && orderType != (ENUM_ORDER_TYPE)-1)
        {
         if(orderType == ORDER_TYPE_BUY && mCurrentMetrics.mSignedVelocity < 0.0)
           {
            isSafe = false;
            mCurrentMetrics.mReason = REASON_ADVERSE_MOMENTUM;
           }
         else
            if(orderType == ORDER_TYPE_SELL && mCurrentMetrics.mSignedVelocity > 0.0)
              {
               isSafe = false;
               mCurrentMetrics.mReason = REASON_ADVERSE_MOMENTUM;
              }
        }

      if(isSafe)
        {
         mCurrentMetrics.mApprovedEvaluations++;
         mCurrentMetrics.mIsSafeToTrade = true;
        }
      else
        {
         mCurrentMetrics.mBlockedEvaluations++;
         mCurrentMetrics.mIsSafeToTrade = false;
        }

      return isSafe;
     }

A rejected or invalid quote invalidates the cached regime snapshot for execution purposes. Even if the last valid analytical regime was REGIME_OPTIMAL or REGIME_MODERATE, IsSafeToTrade() explicitly rejects the current tick when mReason is REASON_INVALID_TICK. This prevents a stale regime classification from authorizing order dispatch after a corrupted Bid/Ask update. The regime field therefore represents the last valid analytical classification, while mReason provides the current-tick execution veto.

Diagnostic accessors, chart event dispatching, and explicit shutdown handlers conclude the facade:

   //+------------------------------------------------------------------+
   //| Renders the non-blocking HUD telemetry to chart                  |
   //+------------------------------------------------------------------+
   void              RenderTelemetry(void)
     {
      if(mIsInitialized && CheckPointer(mDashboard) == POINTER_DYNAMIC)
        {
         mDashboard.Render(mCurrentMetrics);
        }
     }

   //+------------------------------------------------------------------+
   //| Clears HUD from chart                                            |
   //+------------------------------------------------------------------+
   void              ClearTelemetry(void)
     {
      if(CheckPointer(mDashboard) == POINTER_DYNAMIC)
        {
         mDashboard.Clear();
        }
     }

   //+------------------------------------------------------------------+
   //| Dispatches chart GUI interactions (dragging, resizing)           |
   //+------------------------------------------------------------------+
   void              OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
     {
      if(mIsInitialized && CheckPointer(mDashboard) == POINTER_DYNAMIC)
        {
         mDashboard.OnChartEvent(id, lparam, dparam, sparam);
        }
     }

   //+------------------------------------------------------------------+
   //| Returns pointer to internal telemetry dashboard instance         |
   //+------------------------------------------------------------------+
   CTelemetryDashboard* GetDashboard(void)
     {
      return mDashboard;
     }

   //+------------------------------------------------------------------+
   //| Sets commission cost in equivalent symbol points                 |
   //+------------------------------------------------------------------+
   void              SetCommissionEquivalentPoints(const double points)
     {
      if(CheckPointer(mClassifier) != POINTER_INVALID)
        {
         mClassifier.SetCommissionEquivalentPoints(points);
        }
      mProfile.mCommissionPoints = points;
      mCurrentMetrics.mCommissionPoints = points;
     }

   //+------------------------------------------------------------------+
   //| Retrieves current commission cost in equivalent symbol points    |
   //+------------------------------------------------------------------+
   double            GetCommissionEquivalentPoints(void) const
     {
      if(CheckPointer(mClassifier) != POINTER_INVALID)
        {
         return mClassifier.GetCommissionEquivalentPoints();
        }
      return 0.0;
     }

   //+------------------------------------------------------------------+
   //| Retrieves consolidated snapshot of analytical metrics            |
   //+------------------------------------------------------------------+
   void              GetMetrics(SFrictionMetrics &outMetrics) const
     {
      outMetrics = mCurrentMetrics;
     }

   //+------------------------------------------------------------------+
   //| Retrieves auto-detected symbol profile metadata                  |
   //+------------------------------------------------------------------+
   void              GetProfile(SSymbolProfile &outProfile) const
     {
      outProfile = mProfile;
     }

   //+------------------------------------------------------------------+
   //| Resets cumulative trade evaluation counters                      |
   //+------------------------------------------------------------------+
   void              ResetCounters(void)
     {
      mCurrentMetrics.mTotalEvaluations = 0;
      mCurrentMetrics.mApprovedEvaluations = 0;
      mCurrentMetrics.mBlockedEvaluations = 0;
     }

   //+------------------------------------------------------------------+
   //| Explicit shutdown for lifecycle termination                      |
   //+------------------------------------------------------------------+
   void              Shutdown(void)
     {
      ClearTelemetry();
      CleanMemory();
     }

   //+------------------------------------------------------------------+
   //| Status check                                                     |
   //+------------------------------------------------------------------+
   bool              IsInitialized(void) const
     {
      return mIsInitialized;
     }
  };

#endif // SCALPER_GUARD_FRICTION_GUARD_MQH

The unified facade isolates strategy logic from raw microstructure computation, delivering an evaluated execution clearance with zero dynamic allocations inside the tick loop.


Live Verification: MicrostructureRadarTest.mq5

With the engine assembled, we verify tick ingestion and regime classification on a live chart using a dedicated test script: MicrostructureRadarTest.mq5.

Diagnostic Script Implementation

The script configures observation duration, warm-up samples, and analytical thresholds:

//+------------------------------------------------------------------+
//|                                      MicrostructureRadarTest.mq5 |
//|                          Copyright 2026, Wanateki Solutions Ltd. |
//|                                         https://www.wanateki.com |
//+------------------------------------------------------------------+
#property copyright   "Copyright 2026, Wanateki Solutions Ltd."
#property link        "https://www.wanateki.com"
#property version     "1.00"
#property description "Diagnostic Test Script for Real-Time Microstructure Regime Auditing"
#property script_show_inputs

//--- Include the master Friction Guard facade
#include <Why_Backtested_Scalpers_Diverge_in_Live_Trading\ScalperGuard\FrictionGuard.mqh>

//+------------------------------------------------------------------+
//| Script Input Parameters                                          |
//+------------------------------------------------------------------+
input group "=== Observation Session Settings ===";
input int      inputObservationSeconds    = 30;       // Session Duration (Seconds)
input int      inputMaxTicksToAudit       = 100;      // Maximum Ticks to Audit
input int      inputWarmupTicks           = 15;       // Required Warmup Samples

input group "=== Microstructure Engine Calibration ===";
input int      inputBufferSize            = 100;      // Circular Buffer Capacity
input double   inputVelocityWindowSec     = 5.0;      // Velocity Rolling Window (Seconds)
input double   inputMinVelocityPtsSec     = 1.0;      // Min Permissible Velocity (Points/Sec)
input double   inputSpreadZScoreLimit     = 2.0;      // Spread Spike Z-Score Limit
input double   inputHardSpreadMaxPts      = 25.0;     // Absolute Hard Spread Ceiling (Points)
input double   inputMaxFrictionRatio      = 0.40;     // Stricter diagnostic threshold (library default: 0.65)

In OnStart(), CFrictionGuard is initialized on the stack:

//+------------------------------------------------------------------+
//| Script Program Start Function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
   Print("=============================================================================================================");
   PrintFormat("[+] Starting Microstructure Radar Test on %s", _Symbol);
   PrintFormat("    Digits: %d | Point: %g | Session Window: %d sec | Max Ticks: %d",
               _Digits, _Point, inputObservationSeconds, inputMaxTicksToAudit);
   Print("=============================================================================================================");
   Print("");

//--- Instantiate master gatekeeper on the stack
   CFrictionGuard guard;

//--- Initialize engine without GUI dashboard (pure terminal telemetry)
   if(!guard.Initialize(
         _Symbol,
         inputBufferSize,
         inputVelocityWindowSec,
         inputMinVelocityPtsSec,
         inputSpreadZScoreLimit,
         inputHardSpreadMaxPts,
         inputMaxFrictionRatio,
         5.0,              // Expected holding seconds
         10.0,             // Minimum expected displacement points
         2.0,              // Base slippage estimate points
         false,            // Disable GUI telemetry HUD
         "RADAR TEST",     // Dashboard title
         0.0,              // Take profit points
         0.0,              // Commission equivalent points
         0.0,              // Commission per lot
         inputWarmupTicks  // Required warmup ticks
      ))
     {
      Print("[-] Failed to initialize CFrictionGuard engine. Terminating script.");
      return;
     }

   Print("[*] Engine initialized. Ingesting live market ticks...");

Note: PrintFormat() and Sleep(10) are used intentionally in this educational diagnostic script to maintain readable terminal output. Production EAs should avoid per-tick synchronous journal logging in their critical execution path.

The diagnostic loop samples incoming ticks and logs regime classifications:

   MqlTick lastTick;
   ZeroMemory(lastTick);

   ulong startTimestampMs = GetTickCount64();
   ulong maxDurationMs    = (ulong)inputObservationSeconds * 1000;
   int   auditedTicks     = 0;

//--- Pre-build format string with dynamic symbol precision (recommended MQL5 pattern)
   string logFormat = StringFormat("[#%%03d] Bid: %%.%df | Ask: %%.%df | Spread: %%4.1f pt (Z: %%+4.2f) | Vel: %%4.2f pt/s | Freq: %%4.1f t/s \n       F-Ratio: %%4.2f | %%s | Trade: %%s (%%s)", _Digits, _Digits);

//--- Diagnostic tick acquisition and classification loop
   while(!IsStopped() && (GetTickCount64() - startTimestampMs < maxDurationMs) && (auditedTicks < inputMaxTicksToAudit))
     {
      MqlTick currentTick;
      if(SymbolInfoTick(_Symbol, currentTick))
        {
         //--- Process new tick arrivals only
         if(currentTick.time_msc != lastTick.time_msc)
           {
            lastTick = currentTick;
            auditedTicks++;

            //--- Ingest raw tick into analytical pipeline
            guard.ProcessTick(currentTick);

            //--- Evaluate pre-trade execution clearance (updates evaluation counters and rejection reason)
            bool isPermitted = guard.IsSafeToTrade();

            //--- Retrieve consolidated metrics snapshot
            SFrictionMetrics metrics;
            guard.GetMetrics(metrics);

            //--- Format regime and reason strings
            string regimeName = EnumToString(metrics.mRegime);
            string reasonName = EnumToString(metrics.mReason);

            //--- Log analytical state to the Experts journal
            PrintFormat(logFormat,
                        auditedTicks,
                        currentTick.bid,
                        currentTick.ask,
                        metrics.mCurrentSpreadPoints,
                        metrics.mSpreadZScore,
                        metrics.mTickVelocityPointsPerSec,
                        metrics.mTickFrequency,
                        metrics.mFrictionRatio,
                        regimeName,
                        isPermitted ? "APPROVED" : "BLOCKED ",
                        reasonName);
            Print("");
           }
        }
      Sleep(10); // Yield CPU to maintain terminal responsiveness
     }

The script logs final session metrics and performs memory cleanup:

   Print("=============================================================================================================");
   PrintFormat("[+] Microstructure Radar Session Complete. Total Ticks Audited: %d", auditedTicks);
   Print("=============================================================================================================");

//--- Explicit shutdown and memory cleanup
   guard.Shutdown();
  }

Live Terminal Observations

To verify that the circular buffer, kinetic velocity tracker, spread profiler, and regime state machine function correctly under live market conditions, MicrostructureRadarTest.mq5 is attached to a live GBPUSD M1 chart.

The live observation was performed on a standard retail MetaTrader 5 account using market execution for GBPUSD. The test was run during an active London trading session on a live broker-side aggregated quote feed. Exact observation date, server timestamp, average spread, and commission schedule are not disclosed because the demonstration is intended to validate the terminal-side diagnostic workflow rather than establish a broker-independent performance benchmark.

You should expect tick arrival patterns, spread distributions, and commission treatment to vary across brokers, account types, servers, and market sessions. The attached script parameters define the observation window and audit depth, but comparable live results require matching the symbol, execution model, session liquidity, and broker cost structure.

Screenshot 1: Attaching MicrostructureRadarTest.mq5 to the Live GBPUSD M1 Chart

Screenshot 1: Attaching MicrostructureRadarTest.mq5 to the Live GBPUSD M1 Chart

Screenshot 2: Script Input Parameters Configuration Dialog in MetaTrader 5

Screenshot 2: Script Input Parameters Configuration Dialog in MetaTrader 5

Screenshot 3: Real-Time Microstructure Telemetry and Trade Gating Decisions in the MT5 Experts Tab

Screenshot 3: Real-Time Microstructure Telemetry and Trade Gating Decisions in the MT5 Experts Tab

As shown in Screenshot 3, the script streams incoming market ticks, updates spread variance and kinetic velocity, and outputs execution clearance (APPROVED vs. BLOCKED) alongside active rejection reasons. This verifies the operational integrity of the analytical pipeline before live Expert Advisor deployment.


Conclusion

In this article, we decomposed the "scalper reality gap" into measurable components — tick velocity, adaptive spread statistics, and the Friction Ratio — and implemented a deterministic, low-latency pre-trade gate. The resulting CFrictionGuard class, together with its test harness, provides a reusable component for evaluating execution conditions before order submission.

The guard can be integrated into an existing Expert Advisor by initializing it in OnInit(), processing incoming ticks through ProcessTick(), and calling IsSafeToTrade() before placing an order. Each evaluation identifies one of four operational regimes (OPTIMAL, MODERATE, FRICTION-LOCKED, or SPREAD-SPIKE) and provides diagnostic telemetry, including spread Z-score, tick velocity, tick frequency, effective friction, and the Friction Ratio. These measurements make pre-trade decisions transparent and help identify potentially unfavorable execution conditions.

The current implementation has important limitations. Part 1 focuses on quote-side execution safety without explicitly modeling network transit latency or full limit-order-book depth, while slippage is estimated empirically. ScalperGuard should therefore be treated as a conservative risk filter rather than a guarantee of profitable trading. It can help reject trades under potentially unfavorable market conditions, but it does not eliminate execution risk or establish a trading edge.

By shifting friction management from a static broker constraint into a dynamic, pre-trade quantitative filter, this architecture bridges the dangerous gap between idealized Strategy Tester backtests and live ECN reality. In Part 2 of this series, we will build directly upon this telemetry foundation, assembling a complete scalping Expert Advisor that leverages these operational regimes to govern automated order entry and adaptive position sizing. Thank you for following along and I wish you happy and profitable trades ahead.


Source Code and Repository Access

The complete multi-part codebase is available on MQL5 AlgoForge and as an attached archive:

Component File Role and Responsibility
CoreDefines.mqh Foundation constants, data structures, and operational enumerations
CircularTickBuffer.mqh A pre-allocated circular tick buffer with O(1) push ingestion and O(k) rolling window lookups
TickVelocityTracker.mqh Kinetic price velocity and tick arrival frequency calculation over rolling sliding windows
SpreadAnalyzer.mqh Hybrid Adaptive Welford and EWMA spread variance profiler and dynamic Z-score spike detector
FrictionClassifier.mqh Friction Ratio calculation and 4-regime operational state machine
FrictionGuard.mqh Master facade class and pre-trade execution clearance gatekeeper
TelemetryDashboard.mqh Real-time on-chart HUD dashboard renderer (detailed in Part 2 article)
MicrostructureRadarTest.mq5 Diagnostic test script for live microstructure auditing and regime verification
MQL5.zip Complete ready-to-download project archive containing all source files, analytical headers, and scripts organized and ready-to-extract in the default MQL5 directory

References

  1. Welford, B. P. (1962). "Note on a Method for Calculating Corrected Sums of Squares and Products." *Technometrics*, 4(3), 419-420. 
  2. Finch, T. (2009). "Incremental Calculation of Weighted Mean and Variance." University of Cambridge Computing Service Technical Report, 1-8.
  3. Roll, R. (1984). "A Simple Implicit Measure of the Effective Bid-Ask Spread in an Efficient Market." The Journal of Finance, 39(4), 1127-1139. 
  4. Kyle, A. S. (1985). "Continuous Auctions and Insider Trading." Econometrica, 53(6), 1315-1335. 
  5. Harris, L. (2003). Trading and Exchanges: Market Microstructure for Practitioners. Oxford University Press, New York. ISBN: 978-0195144703.
  6. RiskMetrics Group (1996). RiskMetrics: Technical Document (4th ed.). J.P. Morgan / Reuters, New York.


Creating a Cairo-Inspired Graphics Library for MetaTrader 5 (Part 8): Paint, from Solid Colors to Gradients Creating a Cairo-Inspired Graphics Library for MetaTrader 5 (Part 8): Paint, from Solid Colors to Gradients
Gradients are introduced by making the source a polymorphic paint: the rasterizer calls ColorAt(x, y) for each pixel. This enables solid, linear and radial fills on any path with a single inner‑loop change, keeping Path.mqh untouched. The article details color stops and padding, corner‑based normalization for linear gradients, normalized centers/radii for radial ones, a CCairoFillStyle handle, and compatibility with the legacy uint Fill().
From Basic to Intermediate: Objects and Subwindows (II) From Basic to Intermediate: Objects and Subwindows (II)
This article explains how to track and handle the removal of objects from the chart in MQL5 using MetaTrader 5 events. When the code detects that an object created by an indicator has been deleted, it deletes the corresponding instance to avoid inconsistencies and prevent subwindows from being left behind. This approach uses unique names and a header file, prioritizing simplicity and predictable behavior.
Features of Experts Advisors Features of Experts Advisors
Creation of expert advisors in the MetaTrader trading system has a number of features.
Neural Networks in Trading: A Spatio-Temporal State Space Model for Financial Data Analysis (STSSM Block) Neural Networks in Trading: A Spatio-Temporal State Space Model for Financial Data Analysis (STSSM Block)
The article explains the internal mechanics of the STSSM block and demonstrates how modern SSM approaches can be adapted to the event-driven logic of spiking models while maintaining high speed and expressive representations. Step by step, we work our way through the architecture, transforming the rigorous theory behind the authors' approach into a practical financial instrument for analyzing financial time series.