Why Backtested Scalpers Diverge in Live Trading (Part 1): Building an Intelligent Friction Guard for Dynamic Spread and Micro-Volatility Filtering
Table of Contents
- Introduction: The Scalper's Reality Gap
- The Mathematics and Mechanics of Market Friction
- Software Architecture and Safe Memory Design
- Implementation Overview
- Master Facade: FrictionGuard.mqh
- Live Verification: MicrostructureRadarTest.mq5
- 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
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
- 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.
- 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.
- 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
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:

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):

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):

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:

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:

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):

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:
- OPTIMAL (Green): Normal spreads (Z-score <= 1.0) and low friction (Friction Ratio <= 0.35). Order dispatch approved.
- 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.
- 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.
- 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:
- Scale Mismatch Lockout (REASON_SCALE_MISMATCH)
- Buffer Warm-up Incomplete (REASON_BUFFER_NOT_READY)
- Spread Spike or Hard Ceiling Breach (REASON_SPREAD_SPIKE / REASON_HARD_SPREAD_MAX)
- Insufficient Kinetic Velocity (REASON_LOW_VELOCITY)
- Prohibitive Friction Ratio or Commission Hurdle (REASON_HIGH_FRICTION / REASON_COMMISSION_HURDLE)
- Adverse Directional Momentum (REASON_ADVERSE_MOMENTUM)

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
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
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:
- Dynamic objects instantiated with new are immediately validated using CheckPointer(). If an allocation fails, initialization aborts safely.
- Destructors systematically release allocated instances via delete and nullify pointers to prevent dangling references.
- 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:
- Collect tick, spread, friction, and execution data separately for each symbol, broker, account type, and trading session.
- Reserve an out-of-sample period for validation.
- Measure the distribution of spreads, friction ratios, slippage, and directional velocity.
- Select thresholds from the training data; for example, using percentile-based limits or a target blocker rate.
- Test the selected thresholds on the out-of-sample period, measuring blocked-trade percentage, missed opportunities, realized friction, and net expectancy.
- Avoid optimizing thresholds directly on the final test period.
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:
- Initialize() during OnInit() to configure thresholds and normalize broker fees.
- ProcessTick() during OnTick() to stream market ticks into the analytical pipeline.
- 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 2: Script Input Parameters Configuration Dialog in MetaTrader 5

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:
- AlgoForge Repository: forge.mql5.io/kelmut/Why_Backtested_Scalpers_Diverge_in_Live_Trading
- Clone via Git:
git clone https://forge.mql5.io/kelmut/Why_Backtested_Scalpers_Diverge_in_Live_Trading.git
| 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
- Welford, B. P. (1962). "Note on a Method for Calculating Corrected Sums of Squares and Products." *Technometrics*, 4(3), 419-420.
- Finch, T. (2009). "Incremental Calculation of Weighted Mean and Variance." University of Cambridge Computing Service Technical Report, 1-8.
- 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.
- Kyle, A. S. (1985). "Continuous Auctions and Insider Trading." Econometrica, 53(6), 1315-1335.
- Harris, L. (2003). Trading and Exchanges: Market Microstructure for Practitioners. Oxford University Press, New York. ISBN: 978-0195144703.
- RiskMetrics Group (1996). RiskMetrics: Technical Document (4th ed.). J.P. Morgan / Reuters, New York.
Warning: All rights to these materials are reserved by MetaQuotes Ltd. Copying or reprinting of these materials in whole or in part is prohibited.
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.