Market Microstructure in MQL5 (Part 5): Microstructure Noise
Introduction
Part 1 built a defensive foundation: guarded math, validated price feeds, and stable statistical primitives. Part 2 added confidence-weighted Hurst estimation, establishing that NQ M1 Globex operates near the random-walk boundary. Part 3 added the GPH estimator for the fractional differencing parameter d. Part 4 added a full volatility suite — realized vol, fractional vol, a FIGARCH-inspired proxy, GJR-GARCH leverage, bipower jump detection, and multifractal spectrum width.
Parts 1–4 establish what volatility is doing and whether it has memory. They do not explain where that volatility comes from. At the one-minute frequency, price variation has two distinct sources. The first is genuine information — informed traders acting on knowledge not yet reflected in the price, moving it toward fair value. The second is noise — the mechanical friction of trading: the bid-ask bounce, the cost of immediacy, and the variance generated by the order-processing mechanism rather than by price discovery.
A system that cannot separate these two components is systematically miscalibrated. A high noise ratio means most price variation is transient and reverting — using the clustering index from Part 4 on noisy data overstates the ARCH effect. A wide quoted spread proxy indicates that execution costs are elevated, independently of whether volatility is high or low. Using fractional volatility on a session dominated by bid-ask friction produces a number that reflects transaction costs, not information flow.
This article adds two measurement families to MicroStructure_Foundation.mqh. First, it introduces a MicrostructureAnalysis struct and five functions for quoted spread proxy, Roll-implied spread, OHLC noise ratio, order imbalance, and adverse selection. Second, it adds PopulateMicrostructureAnalysis(), which links these outputs to the Part 4 volatility measures and populates the microstructure_noise field reserved in Part 1.
The empirical study in this article uses the same NQ E-mini Nasdaq 100 futures dataset as Part 4, extended to 602 NY sessions covering January 2024 through June 2026. This extension adds the full April 2025 tariff shock window and captures all five stress regimes relevant to the series. The companion SSRN paper documenting the empirical foundation is available at SSRN 6847024.
Before the implementation sections, one point requires explicit framing. The Roll-implied spread returns values approaching zero on NQ M1 data. This is the empirically correct result, not a bug. At one-minute resolution on a futures contract with hundreds of trades per bar and a minimum tick size of 0.25 points, the serial covariance of close-to-close returns is dominated by intrabar price discovery rather than by the bid-ask bounce. The function is retained in the toolkit for instruments or timeframes where the bounce is detectable, and as the theoretically correct foundation for users applying it to sub-minute bar data. The empirical section states this plainly.
Deliverable: five functions added to MicroStructure_Foundation.mqh, a new MicrostructureAnalysis struct, and the microstructure_noise field in RobustFractalAnalysis fully populated. No new include file is created.
The Theory of Microstructure Noise
Two sources of price variationThe market microstructure literature distinguishes two components of observed price changes: the efficient price innovation (genuine information) and the noise term (transient frictions). Roll (1984) showed that even in an efficient market, the simple act of trading between bid and ask generates negative serial covariance in returns. That negative covariance is the fingerprint of noise. A return series with negative lag-1 autocorrelation is mean-reverting at the bar-level — not because of informed anticipation of price reversals, but because the mechanical alternation between bid and ask pulls adjacent prices apart and then back together.
The Roll modelThe Roll (1984) model derives the implied spread from the serial covariance of price changes. If c is the half-spread and trades alternate randomly between bid and ask, the implied spread is:
Roll spread = 2 × √(max(0, −Cov(Δp_t, Δp_{t−1})))
When the serial covariance is negative, the Roll spread is positive and the bid-ask bounce is detectable. When the covariance is positive — trending behavior dominates — the estimator floors at zero. The Roll model makes three assumptions: the efficient price follows a random walk, the probability of a buy versus sell is equal, and the spread is constant. None of these holds perfectly on NQ M1, but the estimator remains the standard first-order approximation for spread estimation from price data alone. Its failure on M1 bars, documented in the empirical section, is itself informative: it tells us that intrabar price discovery has completely absorbed the bounce signal at one-minute aggregation.
Adverse selectionThe adverse selection component is the difference between the quoted spread and the Roll-implied spread. When the quoted spread substantially exceeds the Roll spread, market makers are pricing in the risk of trading against better-informed counterparties — the Kyle (1985) mechanism operating in observable data. High adverse selection relative to the quoted spread is a warning: informed flow is present, and naive directional strategies may be systematically on the wrong side. On NQ M1, where the Roll spread approaches zero, the adverse selection component collapses to the quoted spread proxy. This is reported honestly in the empirical section.
OHLC noise decompositionThe close-to-midpoint variance measures how far the closing price moves from the bar midpoint (H+L)/2. A pure noise process produces closes uniformly distributed around the midpoint. A directional process produces closes consistently near one extreme. The ratio of close-to-midpoint variance to total bar variance is the basic noise ratio.
The enhanced version combines three components. The first is the close-to-midpoint variance fraction — the primary noise signal. The second is the inverted body-to-range ratio (1 − |C−O|/(H−L)): when the bar moves cleanly from open to close in one direction, the body fills the range (low noise); when the bar oscillates without direction, the body is small relative to the range (high noise). The third is the open-to-close return variance as a fraction of the total range variance. The three components are combined with weights of 40%, 40%, and 20% respectively, producing a single noise ratio in [0, 1]. Higher values indicate more noise and less directional signal.
The quoted spread proxyOn M1 bars without live bid-ask data, the mean bar range is used as a spread proxy. The justification follows Parkinson (1980) and Garman and Klass (1980): under the assumption that prices follow a random walk within the bar, the high-low range is proportional to the standard deviation of the process. When market makers widen their quotes during stress, consecutive executions occur at more dispersed prices, and the bar range widens proportionally. The proxy is not a direct measure of the bid-ask spread — it conflates spread widening with directional intrabar movement — but it is a consistent relative indicator that tracks execution cost across sessions. This is stated explicitly in the function header and in the limitations section.
Order imbalanceOrder imbalance measures directional pressure within a bar. On OHLCV data without tick detail, the volume-weighted proxy uses the bar's close position within its range: (Close − Low)/(High − Low) gives a value in [0, 1], centered at 0.5 for a midpoint close. Values above 0.5 indicate buyer pressure; below 0.5 indicate seller pressure. The volume-weighted mean across the window, centered at zero, gives order imbalance in [−1, +1]. Bars where High = Low are excluded — their directional signal is undefined.
Connection to Parts 2–4Three connections to earlier parts are explicit. First, when microstructure_noise is high, the Hurst estimate from Part 2 is biased toward 0.5 — noise suppresses the long-memory signal, making H appear more random-walk-like than the true efficient price process. Second, the Roll covariance computation uses the two-pass variance primitive from Part 1, ensuring numerical stability on near-zero variances. Third, noise and volatility are distinct: the correlation between the enhanced noise ratio and realized vol from Part 4 is only 0.159 across 602 sessions. This non-redundancy is the empirical justification for computing both families of metrics.
Implementation
New struct: MicrostructureAnalysis//+------------------------------------------------------------------+ //| MicrostructureAnalysis: noise and spread decomposition results. | //| Populated by PopulateMicrostructureAnalysis(). | //| adverse_selection = quoted_spread - roll_spread. | //| roll_confidence = |correlation| of adjacent returns [0,1]. | //| On NQ M1 data, roll_spread approaches zero (see empirical study) | //| because M1 aggregation absorbs the bid-ask bounce signal. | //+------------------------------------------------------------------+ struct MicrostructureAnalysis { double quoted_spread; // Mean bar range as spread proxy (points) double roll_spread; // Roll-implied spread (points) double noise_ratio; // Enhanced OHLC noise ratio [0,1] double order_imbalance; // Volume-weighted directional pressure [-1,+1] double adverse_selection; // quoted_spread minus roll_spread (points) double roll_confidence; // |correlation| of adjacent returns [0,1] };BidAskSpread()
Fetches the current quoted spread from SymbolInfoDouble when called on a live chart, and falls back to the mean bar range on historical data. Returns the spread in points. Validated against the 10,000-point sanity check inherited from ValidateSymbolV2(). On M1 historical bars where live bid-ask is unavailable, the bar range is used as the proxy with the same caveat described in the struct header.
//+------------------------------------------------------------------+ //| BidAskSpread: quoted spread in points. | //| Live: SymbolInfoDouble ASK - BID, divided by point size. | //| Historical proxy: mean bar range over the window (points). | //| The historical proxy conflates spread widening with directional | //| intrabar movement but is consistent as a relative indicator. | //+------------------------------------------------------------------+ double BidAskSpread(const string symbol, const int tf, const int window) { if(!ValidateSymbolV2(symbol) || window < 2) return 0.0; double point = SymbolInfoDouble(symbol, SYMBOL_POINT); if(point <= DBL_MIN_POSITIVE) return 0.0; //--- Attempt live bid-ask first (works on current bar) double ask = SymbolInfoDouble(symbol, SYMBOL_ASK); double bid = SymbolInfoDouble(symbol, SYMBOL_BID); if(ask > bid && bid > 0) { double spread = (ask - bid) / point; if(spread < 10000.0) return spread; } //--- Fallback: mean bar range as historical proxy double high[], low[]; ArraySetAsSeries(high, true); ArraySetAsSeries(low, true); if(CopyHigh(symbol, (ENUM_TIMEFRAMES)tf, 0, window, high) < window) return 0.0; if(CopyLow (symbol, (ENUM_TIMEFRAMES)tf, 0, window, low) < window) return 0.0; double sum_range = 0.0; int valid = 0; for(int i = 0; i < window; i++) { double r = (high[i] - low[i]) / point; if(r > 0 && r < 10000.0) { sum_range += r; valid++; } } if(valid == 0) return 0.0; return sum_range / valid; }MicrostructureNoise()
Computes the close-to-midpoint variance as a fraction of total bar variance — the basic noise ratio. Dimensionless and comparable across instruments and price levels. Guarded by DBL_MIN_POSITIVE at the denominator.
//+------------------------------------------------------------------+ //| MicrostructureNoise: close-to-midpoint variance fraction. | //| Returns noise_basic in [0,1]. Higher = more noise. | //| Midpoint = (High + Low) / 2. Measures how far Close deviates | //| from midpoint relative to total bar range variance. | //+------------------------------------------------------------------+ double MicrostructureNoise(const string symbol, const int tf, const int window) { if(!ValidateSymbolV2(symbol) || window < 3) return 0.0; double high[], low[], close[]; ArraySetAsSeries(high, true); ArraySetAsSeries(low, true); ArraySetAsSeries(close, true); if(CopyHigh (symbol,(ENUM_TIMEFRAMES)tf,0,window,high) < window) return 0.0; if(CopyLow (symbol,(ENUM_TIMEFRAMES)tf,0,window,low) < window) return 0.0; if(SafeCopyClose(symbol,tf,0,window,close) < window) return 0.0; double sum_diff = 0.0, sum_rng = 0.0; double diff_vals[], rng_vals[]; ArrayResize(diff_vals, window); ArrayResize(rng_vals, window); int valid = 0; for(int i = 0; i < window; i++) { double mid = (high[i] + low[i]) / 2.0; if(mid <= DBL_MIN_POSITIVE) continue; diff_vals[valid] = (close[i] - mid) / mid; rng_vals [valid] = (high[i] - low[i]) / mid; valid++; } if(valid < 3) return 0.0; //--- Two-pass variance for numerical stability double mean_d = 0.0, mean_r = 0.0; for(int i=0;i<valid;i++){mean_d+=diff_vals[i];mean_r+=rng_vals[i];} mean_d/=valid; mean_r/=valid; double var_d = 0.0, var_r = 0.0; for(int i=0;i<valid;i++) { var_d+=(diff_vals[i]-mean_d)*(diff_vals[i]-mean_d); var_r+=(rng_vals[i] -mean_r)*(rng_vals[i] -mean_r); } var_d/=valid; var_r/=valid; if(var_r <= DBL_MIN_POSITIVE) return 0.0; return MathMin(1.0, var_d / (var_r + DBL_MIN_POSITIVE)); }EnhancedMicrostructureNoise()
Three-component OHLC noise ratio combining close-to-midpoint variance (40%), inverted body-to-range ratio (40%), and open-to-close variance fraction (20%). All three components guarded individually before combination. Writes the result to RobustFractalAnalysis.microstructure_noise.
//+------------------------------------------------------------------+ //| EnhancedMicrostructureNoise: three-component OHLC noise ratio. | //| Component 1 (40%): close-to-midpoint variance fraction. | //| Component 2 (40%): 1 - body_to_range (Garman-Klass efficiency). | //| Component 3 (20%): OC variance as fraction of range variance. | //| Returns noise in [0,1]. Writes to rfa.microstructure_noise. | //+------------------------------------------------------------------+ double EnhancedMicrostructureNoise(const string symbol, const int tf, const int window, RobustFractalAnalysis &rfa) { if(!ValidateSymbolV2(symbol) || window < 3) return 0.0; double open[], high[], low[], close[]; ArraySetAsSeries(open,true); ArraySetAsSeries(high,true); ArraySetAsSeries(low,true); ArraySetAsSeries(close,true); if(CopyOpen (symbol,(ENUM_TIMEFRAMES)tf,0,window,open) < window) return 0.0; if(CopyHigh (symbol,(ENUM_TIMEFRAMES)tf,0,window,high) < window) return 0.0; if(CopyLow (symbol,(ENUM_TIMEFRAMES)tf,0,window,low) < window) return 0.0; if(SafeCopyClose(symbol,tf,0,window,close) < window) return 0.0; double diff_v[],rng_v[],btr_v[],oc_v[]; ArrayResize(diff_v,window); ArrayResize(rng_v,window); ArrayResize(btr_v,window); ArrayResize(oc_v,window); int valid = 0; for(int i = 0; i < window; i++) { double br = high[i] - low[i]; double mid = (high[i] + low[i]) / 2.0; if(mid <= DBL_MIN_POSITIVE || br <= DBL_MIN_POSITIVE) continue; diff_v[valid] = (close[i] - mid) / mid; rng_v [valid] = br / mid; btr_v [valid] = MathAbs(close[i] - open[i]) / br; oc_v [valid] = (close[i] - open[i]) / mid; valid++; } if(valid < 3) return 0.0; //--- Two-pass variance for each component double mean_d=0,mean_r=0,mean_oc=0,mean_btr=0; for(int i=0;i<valid;i++){mean_d+=diff_v[i];mean_r+=rng_v[i]; mean_oc+=oc_v[i]; mean_btr+=btr_v[i];} mean_d/=valid; mean_r/=valid; mean_oc/=valid; mean_btr/=valid; double var_d=0,var_r=0,var_oc=0; for(int i=0;i<valid;i++) { var_d +=(diff_v[i]-mean_d) *(diff_v[i]-mean_d); var_r +=(rng_v[i] -mean_r) *(rng_v[i] -mean_r); var_oc+=(oc_v[i] -mean_oc)*(oc_v[i] -mean_oc); } var_d/=valid; var_r/=valid; var_oc/=valid; //--- Component 1: close-to-midpoint fraction double c1 = (var_r > DBL_MIN_POSITIVE) ? MathMin(1.0, var_d/(var_r+DBL_MIN_POSITIVE)) : 0.0; //--- Component 2: inverted body-to-range (Garman-Klass efficiency) double c2 = MathMax(0.0, 1.0 - mean_btr); //--- Component 3: OC variance as fraction of range variance double c3 = (var_r > DBL_MIN_POSITIVE) ? MathMin(1.0, var_oc/(var_r+DBL_MIN_POSITIVE)) : 0.0; double noise = MathMax(0.0, MathMin(1.0, 0.40*c1 + 0.40*c2 + 0.20*c3)); //--- Write to shared struct field reserved in Part 1 rfa.microstructure_noise = noise; if(!MathIsValidNumber(rfa.microstructure_noise)) rfa.microstructure_noise = 0.0; return noise; }BidAskBounceEffect() — Roll Spread
Computes the Roll-implied spread from the serial covariance of close-to-close log returns. Uses the two-pass covariance primitive consistent with Part 1. The floor at zero prevents complex-valued results when the covariance is positive. Returns the implied spread in points. Also returns a confidence value equal to the absolute correlation of the two adjacent return series — values below 0.0083 (the 10th percentile from the NQ empirical study) indicate the estimate is unreliable.
//+------------------------------------------------------------------+ //| BidAskBounceEffect: Roll-implied spread. | //| spread = 2 * sqrt(max(0, -Cov(Δp_t, Δp_{t-1}))) | //| confidence = |correlation| of adjacent returns [0,1]. | //| | //| On NQ M1, this returns values near zero because intrabar price | //| discovery absorbs the bid-ask bounce signal at 1-min aggregation.| //| The function is retained for sub-minute timeframes and for | //| instruments where the bounce is detectable. | //+------------------------------------------------------------------+ double BidAskBounceEffect(const string symbol, const int tf, const int window, double &confidence) { confidence = 0.0; if(!ValidateSymbolV2(symbol) || window < 5) return 0.0; double close[]; if(SafeCopyClose(symbol, tf, 0, window + 1, close) < window + 1) return 0.0; double point = SymbolInfoDouble(symbol, SYMBOL_POINT); if(point <= DBL_MIN_POSITIVE) return 0.0; //--- Build log-return array; filter gross outliers int n = ArraySize(close) - 1; double ret[]; ArrayResize(ret, n); int valid = 0; for(int i = 0; i < n; i++) { if(close[i] <= 0 || close[i+1] <= 0) continue; double r = SafeLog(close[i+1]) - SafeLog(close[i]); if(MathAbs(r) < 0.1) ret[valid++] = r; } if(valid < 4) return 0.0; ArrayResize(ret, valid); //--- Two-pass covariance of adjacent returns double mean1 = 0.0, mean2 = 0.0; int pairs = valid - 1; for(int i = 0; i < pairs; i++) { mean1 += ret[i]; mean2 += ret[i+1]; } mean1 /= pairs; mean2 /= pairs; double cov = 0.0, var1 = 0.0, var2 = 0.0; for(int i = 0; i < pairs; i++) { cov += (ret[i]-mean1) * (ret[i+1]-mean2); var1 += (ret[i]-mean1) * (ret[i] -mean1); var2 += (ret[i+1]-mean2) * (ret[i+1]-mean2); } cov /= pairs; var1 /= pairs; var2 /= pairs; //--- Confidence: absolute correlation of the two series double denom_conf = MathSqrt(MathMax(0,var1) * MathMax(0,var2)); if(denom_conf > DBL_MIN_POSITIVE) confidence = MathMin(1.0, MathAbs(cov / denom_conf)); //--- Roll spread: floor at zero when covariance is positive if(cov >= 0) return 0.0; double spread_price = 2.0 * MathSqrt(-cov); return MathMax(0.0, spread_price / point); }OrderImbalance()
Volume-weighted directional proxy. For each bar, (Close − Low)/(High − Low) gives the fractional position of the close within the range. The volume-weighted mean, centered at zero, gives order imbalance in [−1, +1]. Bars with zero range are excluded.
//+------------------------------------------------------------------+ //| OrderImbalance: volume-weighted directional pressure. | //| Returns (Close-Low)/(High-Low) centered at zero, volume-weighted.| //| +1 = all closes at high (buyer dominated). | //| -1 = all closes at low (seller dominated). | //| Bars with High == Low excluded (zero range = undefined signal). | //+------------------------------------------------------------------+ double OrderImbalance(const string symbol, const int tf, const int window) { if(!ValidateSymbolV2(symbol) || window < 3) return 0.0; double high[], low[], close[]; long vol[]; ArraySetAsSeries(high,true); ArraySetAsSeries(low,true); ArraySetAsSeries(close,true); ArraySetAsSeries(vol,true); if(CopyHigh (symbol,(ENUM_TIMEFRAMES)tf,0,window,high) < window) return 0.0; if(CopyLow (symbol,(ENUM_TIMEFRAMES)tf,0,window,low) < window) return 0.0; if(SafeCopyClose(symbol,tf,0,window,close) < window) return 0.0; if(CopyTickVolume(symbol,(ENUM_TIMEFRAMES)tf,0,window,vol) < window) return 0.0; double weighted_sum = 0.0; double total_vol = 0.0; for(int i = 0; i < window; i++) { double br = high[i] - low[i]; if(br <= DBL_MIN_POSITIVE) continue; // exclude zero-range bars double frac = (close[i] - low[i]) / br; // [0,1] double imb = 2.0 * frac - 1.0; // center at zero [-1,+1] double v = (vol[i] > 0) ? (double)vol[i] : 1.0; weighted_sum += imb * v; total_vol += v; } if(total_vol <= DBL_MIN_POSITIVE) return 0.0; return MathMax(-1.0, MathMin(1.0, weighted_sum / total_vol)); }PopulateMicrostructureAnalysis()
Wrapper that calls all five functions, fills the MicrostructureAnalysis struct, computes adverse_selection = quoted_spread − roll_spread, and writes microstructure_noise into RobustFractalAnalysis. Applies the same NaN guard pattern as PopulateVolatilityAnalysis() from Part 4.
//+------------------------------------------------------------------+ //| PopulateMicrostructureAnalysis: fills MicrostructureAnalysis and | //| writes microstructure_noise into RobustFractalAnalysis. | //| adverse_selection = quoted_spread - roll_spread. | //| On NQ M1, roll_spread ≈ 0 so adverse_selection ≈ quoted_spread. | //+------------------------------------------------------------------+ void PopulateMicrostructureAnalysis(const string symbol, const int tf, const int window, MicrostructureAnalysis &msa, RobustFractalAnalysis &rfa) { msa.quoted_spread = BidAskSpread(symbol, tf, window); msa.noise_ratio = EnhancedMicrostructureNoise(symbol, tf, window, rfa); msa.roll_spread = BidAskBounceEffect(symbol, tf, window, msa.roll_confidence); msa.order_imbalance = OrderImbalance(symbol, tf, window); msa.adverse_selection = MathMax(0.0, msa.quoted_spread - msa.roll_spread); //--- NaN guards if(!MathIsValidNumber(msa.quoted_spread)) msa.quoted_spread = 0.0; if(!MathIsValidNumber(msa.roll_spread)) msa.roll_spread = 0.0; if(!MathIsValidNumber(msa.noise_ratio)) msa.noise_ratio = 0.0; if(!MathIsValidNumber(msa.order_imbalance)) msa.order_imbalance = 0.0; if(!MathIsValidNumber(msa.adverse_selection)) msa.adverse_selection = 0.0; if(!MathIsValidNumber(msa.roll_confidence)) msa.roll_confidence = 0.0; }
Empirical Study: NQ M1 Microstructure Noise
All estimators were applied to 602 NY sessions of NQ E-mini Nasdaq 100 futures (CME Globex), covering January 2024 through June 2026. The NY session filter retains bars with open time in [14:30, 21:00) UTC. Sessions with fewer than 300 M1 bars are excluded. The extended dataset adds 88 sessions relative to the Part 4 study, capturing a longer normal baseline and the full April 2025 tariff shock window. All metrics are computed fresh from the raw OHLCV bars using the Python implementation of the functions above.
Figure 1 — Enhanced noise ratio (top) and quoted spread proxy (bottom) across 602 NQ sessions, January 2024–June 2026. Regime shading marks the four identified stress episodes. The April 2025 tariff shock produces the largest quoted spread elevation (2.6× normal). The BoJ shock produces the highest noise ratio.
Table 1 – Summary Statistics (602 NY sessions, NQ M1 futures)
| Metric | Mean | Std Dev | Min | 25% | Median | 75% | Max |
|---|---|---|---|---|---|---|---|
| Quoted spread proxy (pts) | 13.82 | 6.47 | 5.34 | 9.51 | 12.41 | 16.34 | 74.18 |
| Roll-implied spread (pts) | 0.0001 | 0.0001 | 0.0000 | 0.0000 | 0.0000 | 0.0002 | 0.0013 |
| Roll confidence [0,1] | 0.060 | 0.046 | 0.000 | 0.025 | 0.050 | 0.086 | 0.248 |
| Noise ratio (basic) | 0.466 | 0.132 | 0.202 | 0.372 | 0.458 | 0.541 | 1.000 |
| Body-to-range ratio | 0.453 | 0.019 | 0.396 | 0.440 | 0.452 | 0.466 | 0.513 |
| Noise ratio (enhanced) | 0.604 | 0.053 | 0.438 | 0.570 | 0.602 | 0.632 | 0.815 |
| Order imbalance [-1,+1] | 0.021 | 0.056 | −0.185 | −0.016 | 0.024 | 0.061 | 0.158 |
| Adverse selection (pts) | 13.82 | 6.47 | 5.34 | 9.51 | 12.41 | 16.34 | 74.18 |
| Realized vol (per bar) | 0.00042 | 0.00018 | 0.00013 | 0.00029 | 0.00038 | 0.00050 | 0.00310 |
The Roll-implied spread row warrants immediate comment. The near-zero values — mean 0.0001 points against a minimum tick of 0.25 points — confirm that the bid-ask bounce is undetectable at one-minute aggregation on NQ futures. The Roll confidence averages 0.060, well below the 0.65 threshold used for MFDFA in Part 4. This is the expected result on a market where individual bars aggregate hundreds of individual trades: the serial covariance of close-to-close returns is dominated by genuine intrabar price discovery, not by the alternation between bid and ask. The function is retained for completeness and for users applying it to sub-minute data or instruments with wider spreads. The adverse selection column is numerically identical to the quoted spread proxy because Roll spread ≈ 0; both rows are reported for transparency.
The enhanced noise ratio has mean 0.604 and a notably low standard deviation of 0.053. A typical NQ M1 session has approximately 60% of its price variation attributable to noise by the OHLC decomposition. The low dispersion indicates that noise is a stable structural feature of the instrument at this timeframe, not an episodic phenomenon.
Figure 2 — Left: enhanced noise ratio against realized vol per bar across 602 sessions (r = 0.159, p < 0.001), colored by regime. Noise and volatility are distinct measures. Right: intraday noise profile (blue, left axis) and bar range (red, right axis) by minute from NY open. Range declines sharply through the session; noise increases modestly.
Two findings from the cross-metric correlations and regime comparison are worth highlighting before the regime table.
First, noise and volatility are distinct. The correlation between the enhanced noise ratio and realized vol is only 0.159 across 602 sessions (p < 0.001). The two measures carry different information and justify separate computation. The correlation between the quoted spread proxy and realized vol is 0.959 — but this reflects the fact that the spread proxy is essentially a vol measure on M1 data, not that spread and vol are the same concept. The genuine spread (the Roll implied value) has near-zero correlation with vol, as expected.
Second, the intraday noise profile is slightly increasing from open to close (0.532 at open versus 0.551 at close), while bar range declines sharply from 26.1 to 11.5 points. This is the opposite of what a simple U-shaped liquidity story would predict. The interpretation is that later-session bars are narrower but not proportionally more directional — they are quieter in absolute terms but not cleaner in signal terms. This may be consistent with end-of-session position squaring generating small reverting moves, though the sample is insufficient to establish causation.
Table 2 – Regime Comparison (602 sessions)
| Regime | Sessions | Noise ratio | vs Normal | Spread proxy (pts) | vs Normal | Order imbalance | Realized vol |
|---|---|---|---|---|---|---|---|
| Normal | 556 | 0.603 | — | 13.14 | — | 0.021 | 0.00040 |
| BoJ shock (Aug 2024) | 8 | 0.677 | +12% | 23.16 | 1.76× | 0.010 | 0.00082 |
| Fed Dec (Dec 2024) | 6 | 0.593 | −2% | 15.55 | 1.18× | 0.028 | 0.00054 |
| Tariff Apr 2025 | 14 | 0.645 | +7% | 34.12 | 2.60× | 0.008 | 0.00119 |
| Tariff Apr 2026 | 18 | 0.564 | −6% | 14.23 | 1.08× | 0.031 | 0.00040 |
The regime table produces three findings consistent with the Part 4 volatility analysis. The BoJ carry-trade unwind of August 2024 is the highest-noise episode (0.677, +12% above normal), suggesting that the extreme volatility of that period was accompanied by significant transient price variation rather than clean directional movement. The April 2025 tariff shock produces the widest quoted spread proxy (34.1 points, 2.6× normal) while noise is elevated but not extreme (0.645). Order imbalance drops toward zero in both stress episodes (0.010 and 0.008 respectively), consistent with the Part 4 finding that acute stress produces two-sided flow rather than directional dominance. The April 2026 tariff episode shows noise and spread below or near normal — again consistent with the cross-paper finding that markets may have largely adapted to the tariff regime by 2026.
Figure 3 — Left: distribution of session-mean order imbalance across 602 sessions. The distribution is centred slightly above zero (mean 0.021), indicating a mild systematic buyer bias on NQ over the sample period. Right: enhanced noise ratio by regime. The BoJ shock is the clear outlier; the April 2026 tariff episode is the quietest.
Limitations
The functions implemented in this article operate on one-minute OHLCV bars. This is a meaningful constraint that readers should understand before applying these measures in production.
Genuine microstructure analysis is conducted at the tick level. The canonical measures — effective spread, price impact, adverse selection cost, VPIN — are defined in terms of individual trades: the signed volume of each transaction, the exact bid and ask at the moment of execution, and the price response in the bars immediately following. At this level, the Roll spread is estimated from actual transaction prices rather than bar closes, and order imbalance reflects the true aggressor side of each trade rather than a bar-level proxy.
The NQ empirical study confirms this directly. The Roll-implied spread returns values near zero across all 602 sessions (mean 0.0001 points, maximum 0.0013 points, against a minimum tick of 0.25 points). This is the correct result at one-minute aggregation: intrabar price discovery, which involves hundreds of individual executions per bar, completely absorbs the serial covariance that the Roll model requires to detect the bid-ask bounce. The function produces a meaningful signal at sub-minute timeframes and on instruments with wider effective spreads, but not on NQ M1. Users applying BidAskBounceEffect() to M1 NQ data should treat the output as approximately zero regardless of the computed value. The function is included for completeness and cross-timeframe consistency. It is intended for sub-minute bars or instruments where the bid-ask bounce is detectable. For NQ M1, do not use it for trading decisions unless roll_confidence exceeds a meaningful threshold for the target instrument.
The OHLCV approximations also introduce a specific bias in the order imbalance measure. The volume-weighted proxy using (Close − Low)/(High − Low) correctly identifies bars that close strongly in one direction, but misclassifies bars where price moved directionally during the minute and then reversed, closing near the midpoint despite net buying or selling pressure throughout the session.
A theoretically correct approach is to aggregate tick data into volume or dollar buckets before computing these measures. This is the methodology underlying VPIN (Easley et al., 2012) and the bar construction research of López de Prado (2018), where each bucket contains a fixed quantity of market activity rather than a fixed unit of time. A practical MQL5 implementation of activity and imbalance bars — the direct alternative to the time-bar approximations used here — is described by Njoroge (2026) in the MQL5 Community Articles series. Bucket-based aggregation eliminates the OHLCV bias entirely: every trade contributes to the spread and imbalance estimates, and the resulting measures are stationary in activity rather than in clock time.
The M1 OHLCV implementations in this article are appropriate for live MQL5 indicators that do not have access to tick history, and for instruments where tick data is unavailable or prohibitively large. They provide directionally correct session-level signals even when the Roll decomposition fails. This is consistent with the rest of the series, which uses M1 data throughout. Part 6 will introduce VPIN_OHLC(), which represents the closest available approximation to tick-level VPIN without individual trade data. The same limitation applies there and will be restated for clarity.
Practical Thresholds
All thresholds below are derived from the empirical distribution across 602 NQ M1 sessions, January 2024–June 2026. They are heuristic operational cutoffs based on empirical quantiles, not formal hypothesis-test critical values. They are specific to NQ M1 and this sample period. Users should treat them as starting points and re-derive them on their own instrument, timeframe, and data window.
| Metric | Threshold | Basis | Interpretation | Action |
|---|---|---|---|---|
| Noise ratio (enhanced) | > 0.63 | 75th pct | Elevated noise; vol estimates less reliable | Widen stops; discount clustering index from Part 4 |
| Noise ratio (enhanced) | < 0.57 | 25th pct | Cleaner price discovery than typical | Vol and Hurst estimates more trustworthy |
| Quoted spread proxy | > 16.3 pts | 75th pct | Elevated execution cost | Widen stops; reduce position size |
| Quoted spread proxy | > 20.8 pts | 90th pct | Stress-level spread widening | Reduce size materially; validate data feed |
| Roll confidence | < 0.008 | 10th pct | Roll estimate unreliable (typical on NQ M1) | Treat roll_spread as unavailable |
| Order imbalance | > 0.09 | 90th pct | Strong buyer pressure | Consider aligning with imbalance direction |
| Order imbalance | < −0.06 | 10th pct | Strong seller pressure | Consider aligning with imbalance direction |
//--- Example: Part 5 metrics gating the Part 4 volatility signal RobustFractalAnalysis rfa; MicrostructureAnalysis msa; VolatilityAnalysis va; PopulateHurstAnalysis(Symbol(), PERIOD_M1, 90, rfa); PopulateVolatilityAnalysis(Symbol(), PERIOD_M1, 90, va, rfa); PopulateMicrostructureAnalysis(Symbol(), PERIOD_M1, 90, msa, rfa); //--- Select volatility estimate; discount when noise is elevated double current_vol = va.realized_vol; if(rfa.multifractal_width > 0.99 && rfa.dimension_confidence > 0.65) current_vol = va.figarch_vol; else if(va.clustering_index > 0.20) current_vol = va.fractional_vol; //--- Gate clustering signal on noise quality bool noise_clean = (msa.noise_ratio < 0.63); double target_vol = 0.0004; // calibrated to NQ M1 mean realized vol double base_pos = (current_vol > DBL_MIN_POSITIVE) ? target_vol / current_vol : 1.0; //--- Scale down for spread elevation if(msa.quoted_spread > 20.8) base_pos *= 0.5; else if(msa.quoted_spread > 16.3) base_pos *= 0.75; //--- Scale down for jump activity (Part 4) if(va.jump_intensity > 0.021) base_pos *= MathMax(0.3, 1.0 - va.jump_intensity * 10.0); //--- Align with order imbalance if strong and noise is clean double oi_signal = 0.0; if(noise_clean && MathAbs(msa.order_imbalance) > 0.09) oi_signal = MathSign(msa.order_imbalance);
Folder Structure and Include Path
All Part 5 functions are added to the existing foundation header. No new files are created. The folder structure remains unchanged from Parts 1–4:
MQL5\
└── Indicators\
└── HurstProfile\
├── HurstProfile.mq5
└── Includes\
└── MicroStructure_Foundation.mqh #include "Includes\MicroStructure_Foundation.mqh"
Conclusion
This article added five microstructure noise functions to MicroStructure_Foundation.mqh and introduced the MicrostructureAnalysis struct. BidAskSpread() provides the quoted spread proxy. BidAskBounceEffect() implements the Roll-implied spread — and the empirical study demonstrates that it returns near-zero values on NQ M1, confirming that tick-level data is required to detect the bid-ask bounce at this instrument and timeframe. EnhancedMicrostructureNoise() gives a three-component OHLC noise ratio that is informative and stable across sessions. OrderImbalance() measures bar-level directional pressure. PopulateMicrostructureAnalysis() connects all five to the volatility measures from Part 4 and populates the microstructure_noise field reserved in Part 1.
The empirical study on 602 NQ M1 sessions shows that approximately 60% of price variation is attributable to noise by the OHLC decomposition, with low session-to-session dispersion (SD = 0.053). Noise is elevated during stress episodes — the BoJ shock reaches 0.677, 12% above the normal baseline — but remains distinct from volatility (r = 0.159). Order imbalance is near-zero on average and drops further during acute stress, consistent with the two-sided flow finding in Part 4. The April 2026 tariff episode shows noise and spread at or below normal, again consistent with cross-paper evidence of market adaptation.
As noted in the limitations section, the Roll decomposition partially fails at M1 resolution on NQ futures. This is an honest empirical result, not a flaw in the implementation. The noise ratio, quoted spread proxy, and order imbalance remain informative and usable. Part 6 will use all five metrics as inputs to the order flow signal, alongside the volatility measures from Part 4.
Getting the Source Code via MQL5 Algo Forge
Algo Forge provides Git-based version control in the cloud, so you will always have access to the latest version of the code, including any updates or fixes made after this article was published. The full repository is available at MQL5 Algo Forge.
- Brown, M. (2026a). Market Microstructure in MQL5: Robust Foundation (Part 1). MQL5 Community Articles.
- Brown, M. (2026b). Market Microstructure in MQL5: Measuring Long Memory (Part 2). MQL5 Community Articles.
- Brown, M. (2026c). Market Microstructure in MQL5: Estimating ARFIMA d with GPH (Part 3). MQL5 Community Articles.
- Brown, M. (2026d). Market Microstructure in MQL5: Volatility That Remembers (Part 4). MQL5 Community Articles.
- Brown, Max (2026). Intraday Microstructure Dynamics of E-mini S&P 500 Futures: Volatility Regimes, Liquidity Decay, and Long Memory in Realized Volatility (May 29, 2026). Available at SSRN: https://ssrn.com/abstract=6847024.
- Easley, D., López de Prado, M.M. & O'Hara, M. (2012). Flow toxicity and liquidity in a high-frequency world. Review of Financial Studies, 25(5), 1457–1493.
- Engle, R.F. (2000). The econometrics of ultra-high-frequency data. Econometrica, 68(1), 1–22.
- Garman, M.B. & Klass, M.J. (1980). On the estimation of security price volatilities from historical data. Journal of Business, 53(1), 67–78.
- Hasbrouck, J. (1993). Assessing the quality of a security market. Review of Financial Studies, 6(3), 405–434.
- Kyle, A.S. (1985). Continuous auctions and insider trading. Econometrica, 53(6), 1315–1335.
- Parkinson, M. (1980). The extreme value method for estimating the variance of the rate of return. Journal of Business, 53(1), 61–65.
- Njoroge, P.M. (2026). Beyond the Clock (Part 1): Building Activity and Imbalance Bars in Python and MQL5. MQL5 Community Articles.
- Roll, R. (1984). A simple implicit measure of the effective bid-ask spread in an efficient market. Journal of Finance, 39(4), 1127–1139.
- Brown, Max (2026). Measuring the Memory Structure of Intraday Returns: Evidence from E-mini S&P 500 Futures (April 01, 2026). Available at SSRN: https://ssrn.com/abstract=6809080 or http://dx.doi.org/10.2139/ssrn.6809080.
- López de Prado, M.M. (2018). Advances in Financial Machine Learning. Wiley.
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.
A Generic Object Pool in MQL5: Eliminating Heap Fragmentation in High-Frequency Indicators
CSV Data Analysis (Part 3): Engineering a Python Analytics Pipeline for MetaTrader 5 CSV Exports
Features of Experts Advisors
MQL5 Wizard Techniques you should know (Part 95): Using Disjoint Set Union and Deep Belief Network in a Custom Signal Class
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use


