Building a Research-Grounded Grid EA in MQL5: Why Most Grid EAs Fail and What Taranto Proved
Introduction
Grid trading has a reputation problem. Ask any experienced trader about grid Expert Advisors and the response is predictable: "It looks great until it blows your account." They are not wrong. The well-known MQL5 article Grid and martingale: what are they and how to use them? demonstrates mathematically that a naive grid converges toward ruin. Countless forum threads and blown demo accounts confirm the pattern: steady profits for weeks or months, then a catastrophic loss that wipes everything in days.
But dismissing grid trading entirely based on these failures is, to borrow a phrase from the academic literature, "a gross over-simplification of the resulting stochastic system." The same mathematics that proves why unconstrained grids fail also reveals exactly what makes a constrained, restartable grid survivable and under what conditions it can be profitably operated.
This article is based on Aldo Taranto's PhD research at the University of Southern Queensland (2020–2022). The research formalized grid trading as a Bi-Directional Grid Constrained (BGC) stochastic process, derived conditions for positive expected value, and proved that unconstrained operation leads to almost sure ruin. It also identified the mechanism that prevents ruin: finite, restartable cycles with regime-aware gating.
The Expert Advisor presented here is not a naive grid. It is a regime-adaptive cycle manager with three modes (BGT for ranging, TGT for trending, and MGT for post-trend mean reversion). It also includes ATR-dynamic spacing, equity-based lot sizing, CUSUM structural-break detection, and a diagnostics pipeline with companion Python tools.
We will cover:
- The Grid Trading Problem Is Not the Gambler's Ruin Problem
- The Loss Formula and the Live Kill Switch
- Variance Growth, Almost Sure Ruin, and the Restart Mechanism
- ATR-Dynamic Spacing and Dynamic Lot Sizing
- Regime Detection — VolatilityGate and Change Point Detection
- Three Grid Modes — BGT, TGT, and MGT
- Production Design — Start Gates, Persistence, and Diagnostics
- Strategy Testing
- The Diagnostics Pipeline — Analyzing and Calibrating Your Grid
- Conclusion
The complete source code for the EA, all nine header modules, and both companion Python scripts are attached at the end of this article.
The Grid Trading Problem Is Not the Gambler's Ruin Problem
The most consequential mistake in the grid-trading discourse is the assumption that grid EAs are martingale systems with a different skin. They are not. Taranto & Khan (2020) proved that the Grid Trading Problem (GTP) and the Gambler's Ruin Problem (GRP) are mathematically distinct processes with fundamentally different loss accumulation rates.
Martingale: Exponential Loss Growth
In a classical martingale (the Gambler's Ruin Problem), the bet is doubled after every loss. After n consecutive adverse steps, the cumulative loss is 2^n. This is exponential growth. Ten adverse steps require 1,024 units of capital to survive. Twenty steps require over a million. The death spiral is fast and unforgiving; every serious trader correctly warns against martingale strategies.
Grid Trading: Triangular Loss Growth
In a bi-directional grid (the Grid Trading Problem), the strategy does not double the bet. Instead, it accumulates one new losing position at each grid level the price traverses. After n levels of directional movement, the cumulative loss is not 2^n but the triangular number n(n+1)/2. This is quadratic growth, substantially slower than exponential.
The practical difference is dramatic:

Fig. 1. Martingale vs. Grid loss growth
| Adverse levels reached | Martingale loss (2^n) | Grid loss (n(n+1)/2) | Grid survival advantage |
|---|---|---|---|
| 1 | 2 | 1 | 2x |
| 3 | 8 | 6 | 1.3x |
| 5 | 32 | 15 | 2.1x |
| 8 | 256 | 36 | 7.1x |
| 10 | 1,024 | 55 | 18.6x |
At 10 adverse moves, a martingale EA needs 1,024 units of capital to survive. A grid EA needs only 55, roughly 18 times less. This is the fundamental survival advantage of grid trading over martingale. It does not make grids safe. It makes them engineerable: the slower loss growth gives you time to detect adverse conditions and intervene before the damage becomes terminal.
The Formal Distinction: Semimartingale
Taranto formalized this difference through the BGC stochastic differential equation:
dX = [f(X,t) - sgn[X,t] · Ψ(X,t)] dt + g(X,t) dW_t
The key term here is the BGC constraining function Ψ(X,t) . This captures the mathematical effect of the grid: the further price drifts from the grid origin, the more the structure resists. This is not a hard barrier; it is a soft, graduated resistance that increases with distance. The grid creates hidden reflective barriers that emerge from the interaction between the grid structure and price movement.
Because of this constraining function, the GTP is classified as a semimartingale — a stochastic process that can have genuine positive expectation under the right conditions. A pure martingale (like the Gambler's Ruin) has zero expected drift by definition. A semimartingale can have positive drift. This is the mathematical foundation for why grid trading can work when properly constrained, while martingale strategies cannot.
The GTP probability of the trader winning, starting with i coins at grid level x out of a total n coins, was derived as Theorem 52 in the thesis:
P(Trader wins | grid level x) = [1 - ((x+1)/2)^i] / [1 - ((x+1)/2)^n]
The Loss Formula and the Live Kill Switch
The single most practically powerful result in the research is the total loss formula. It tells you your entire floating loss exposure at any moment from just two measurements: how far price has fallen from its session high, and how far it has risen from its session low.
Definitions
Drawdown D_t — the distance that the price has fallen from its highest point since the grid cycle started, measured in grid levels:
D_t = floor((SessionHigh - CurrentPrice) / g) — where g is the grid spacing in price units.
Drawup U_t — the distance that the price has risen from its lowest point since the grid cycle started, measured in grid levels:
U_t = floor((CurrentPrice - SessionLow) / g)
From these two numbers, the total loss exposure is:
L_t = 1/2 * (D_t^2 + D_t + U_t^2 + U_t) — the Taranto & Khan loss formula, giving total losing trade exposure in grid-level units.
A Concrete Example
Consider a grid with spacing g = 50 pips, anchored at 1.1000. Price rises to a session high of 1.1250, then falls to a session low of 1.0850, and currently sits at 1.0900. Note that D_t is measured from session high to current price, not from the anchor, so it spans both the upside excursion and the partial retracement.
From the current price: D_t = floor((1.1250 - 1.0900) / 0.0050) = 7 levels (session high to current). U_t = floor((1.0900 - 1.0850) / 0.0050) = 1 level (session low to current).
Therefore: L_t = 1/2 * (49 + 7 + 1 + 1) = 29 losing trade units.
No position-by-position inspection is required. SessionHigh, SessionLow, CurrentPrice, and g are sufficient to compute total basket exposure. This is what makes L_t powerful — it is a constant-time O(1) risk calculation.
The diagram below illustrates how D_t and U_t map onto a price path crossing grid levels:

Fig. 2. Lt formula visualization showing Dt, Ut, and losing positions on grid levels
The Ruin Proximity Ratio
L_t alone tells you the number of losing trade units. To make it actionable, we normalize it against the equity available for the current cycle:
RPR = L_t / E0_cycle — the Ruin Proximity Ratio, measuring how close the current cycle is to margin exhaustion.
Theory vs. Reality: Why We Use Equity Instead of L_t
Taranto's L_t formula is mathematically correct under the model's assumptions: flat lots at every level, each level fills exactly once, and the grid does not re-arm closed levels. Our EA violates all three.
With re-arming, the same grid level can fill, take profit, re-arm, fill again, and lose again, all within one cycle. The L_t formula counts each level once based on session extremes; the actual damage can be higher. Conversely, the Refraction Principle (Theorem 57 in the thesis) proves that for oscillatory paths, L_t overestimates exposure because it uses session extremes rather than tracking the specific order in which levels were crossed.
The resolution: L_t and RPR are computed and logged as diagnostics, valuable for post-run analysis and calibration. But the live kill switch uses direct equity measurement, which captures everything: floating losses, realized losses within the cycle, swaps, commissions, and all re-arming damage.
Here is how the EA computes L_t for diagnostic purposes:
//+------------------------------------------------------------------+ //| Compute theoretical Lt in grid-level units | //| (Taranto & Khan formula: Lt = 1/2(Dt^2 + Dt + Ut^2 + Ut)) | //+------------------------------------------------------------------+ double ComputeLt(const GridSession &session, double bid) { double dtPrice = session.sessionHigh - bid; double utPrice = bid - session.sessionLow; if(dtPrice < 0) dtPrice = 0; if(utPrice < 0) utPrice = 0; double dtLevels = MathFloor(dtPrice / session.gridSpacing); double utLevels = MathFloor(utPrice / session.gridSpacing); //--- Cap to actual grid depth dtLevels = MathMin(dtLevels, (double)session.totalLevels); utLevels = MathMin(utLevels, (double)session.totalLevels); return 0.5 * (dtLevels * dtLevels + dtLevels + utLevels * utLevels + utLevels); }
We compute dtPrice and utPrice as the raw price distance from session high and session low to the current bid, clamping negative values to zero. We convert to grid levels using MathFloor, cap to the actual grid depth with MathMin, and apply the triangular loss formula.
And here is the live kill switch, direct equity-based drawdown, running every tick:
//--- Equity-based cycle drawdown (runs every tick) double cycleDrawdown = 0; if(g_session.cycleStartEquity > 0) { double currentEquity = AccountInfoDouble(ACCOUNT_EQUITY); cycleDrawdown = (g_session.cycleStartEquity - currentEquity) / g_session.cycleStartEquity; } //--- Tighter threshold for MGT (post-trend recovery) double effectiveKillThreshold = InpRuinThreshold; if(g_session.gridMode == GRID_MODE_MGT && InpMGTKillThreshold > 0) effectiveKillThreshold = InpMGTKillThreshold; //--- Only fire when real money is at risk (not a blocked zero-position cycle) bool cycleIsEmpty = (g_cycleBlocked && openPositionCount == 0); if(!cycleIsEmpty && cycleDrawdown >= effectiveKillThreshold) { if(g_session.gridMode == GRID_MODE_TGT) g_lastCycleWasTGTKill = true; //--- triggers MGT next cycle g_gridCore.CloseAllPositions(g_session, killOrphans); g_session.cycleCount++; g_stateManager.ClearState(); StartNewCycle(); return; }
The kill switch computes cycleDrawdown as the percentage drop from cycle-start equity, using AccountInfoDouble with ACCOUNT_EQUITY. Two nuances are worth noting. First, MGT mode (the post-trend mean-reversion mode covered in Section 6) supports a tighter kill threshold because MGT cycles carry higher structural risk. Second, the cycleIsEmpty guard prevents the kill switch from firing on a blocked cycle with zero open positions. Without this, an empty paused cycle would repeatedly trigger exit logic and generate runaway fake cycle counts. When the kill fires on a TGT cycle, the flag g_lastCycleWasTGTKill tells the next cycle to enter MGT mode.
Variance Growth, Almost Sure Ruin, and the Restart Mechanism
Understanding why grid EAs fail is the foundation on which every safety mechanism is designed. The research identifies two mathematical processes that cause failure, and proving both rigorously is what makes the survival mechanism defensible rather than hopeful.
Equity Variance Grows Exponentially With Time
Taranto derived the full equity solution for a bi-directional grid system. The expected equity follows:
E[E_t] = E_0 · exp([v·t + (2v·mu_t/g) - (v·sigma_t^2/g) - (2v^2·sigma_t^2/g^2)] · t)
The parameters: E_0 is starting equity, v is profit per grid level, g is grid spacing, mu_t is drift (trend strength), sigma_t is diffusion (volatility, measurable as ATR), and t is time elapsed. But the critical finding is the variance:
Var[E_t] grows as exp(4v^2 · sigma_t^2 / g^2 · t)
This proves mathematically what experienced grid traders observe: a grid running for 200 hours is dramatically more dangerous than one running for 20 hours, even if the equity curve looks similar. The variance, the range of possible outcomes expands exponentially. The probability of a catastrophic event increases the longer a single cycle persists.
Almost Sure Ruin When x Approaches Infinity
The thesis proves formally (Section 10.1.1) that if a single grid cycle runs indefinitely:
- lim (x approaches infinity) P(Trader wins) = 0
- lim (x approaches infinity) P(Broker wins) = 1
If price keeps trending and the grid keeps extending, the trader will be ruined with probability 1. The thesis qualifies this: the result "assumes that the trend continues indefinitely, which is not the case in practice." The proof is an upper bound on ruin probability. But the structural implication is clear:
No grid EA can run indefinitely without a restart mechanism. This is a mathematical proof, not conjecture. The restart is not a stop-loss — it is a variance reset that restores finite ruin probability for the next cycle.
The Restart Mechanism
The research is explicit: "The ability to close down some or all trades when the system is in profit and then start the grid system again forms a favorable positive cash-flow cycle." When a cycle ends and restarts, two things reset simultaneously. The time variable t goes back to zero, resetting the exponential variance accumulation. The grid level x goes back to zero, resetting the loss accumulation and re-anchoring at the current price.
The restart works because the grid has positive expected value during favorable conditions. The profitability condition from the equity solution is:
v·t + (2v·mu_t/g) > (v·sigma_t^2/g) + (2v^2·sigma_t^2/g^2)
The left side is accumulation — time-based wins plus drift contribution. The right side is the variance penalty. When volatility sigma_t is moderate and drift mu_t is low (ranging market), the left side dominates. When a strong trend emerges, the condition fails. Restart ensures profitable periods are crystallized before variance erodes the gains.
Theory vs. reality: Like L_t, the profitability condition is directionally correct but not quantitatively precise for production. The exact threshold where it flips depends on re-arming frequency, spread costs, and the specific path through the grid. We use the condition to understand which regime the grid should operate in (the sigma_t/mu_t ratio), not to compute a go/no-go signal per tick.
Here is the profit-target basket close — the core restart logic:
//--- Profit Monitor (throttled every 10 ticks) double cycleProfit = g_profitMonitor.GetCycleProfit(g_session.cycleStartEquity); if(g_profitMonitor.CheckProfitTarget(cycleProfit, g_session.cycleStartEquity, InpUseProfitPercent, InpProfitTargetDollars, InpProfitTargetPercent)) { //--- Close the entire basket in one shot g_session.state = GRID_STATE_CLOSING; int closedCount = g_gridCore.CloseAllPositions(g_session, profitOrphans); //--- Guard: if broker didn't close everything, retry next tick int remaining = g_profitMonitor.GetPositionCount(); if(remaining > 0) { g_session.state = GRID_STATE_ACTIVE; return; } //--- Book profit, increment cycle, wipe state double finalProfit = g_profitMonitor.GetCycleProfit(g_session.cycleStartEquity); g_session.cumulativeProfit += finalProfit; g_session.cycleCount++; g_stateManager.ClearState(); //--- Restart — variance clock resets here StartNewCycle(); return; }
The profit check compares current equity with cycle-start equity, using either a percentage target or a fixed dollar target. The default is a 3% profit target per cycle. When the target is hit, CloseAllPositions() closes the basket, with a remaining-positions guard that retries on the next tick if the broker did not execute all closures in one pass. After confirmation, the cycle counter increments, saved state clears, and StartNewCycle() re-anchors at the current price with fresh ATR-computed spacing and equity-based lot size. The variance clock resets here.

Fig. 3. Variance Growth and Restart Mechanism
Three Cycle-Ending Conditions
The profit-target close is the desirable ending. But the EA has three main cycle-ending mechanisms: (1) equity-based drawdown exit (the kill switch from Section 2), the primary protective exit; (2) profit-target exit (this section), the positive restart; and (3) variance age exit, which fires unconditionally when the cycle exceeds InpMaxCycleHours (default: 72 hours). The age exit is unconditional because the exponential variance growth proof means an older cycle is statistically more dangerous regardless of its current P&L. Another mechansim: (4) weekend flat close — forces all positions closed on Friday to protect against gap risk.
The Four Market States
The interplay between drift mu_t and volatility sigma_t determines whether the grid operates in positive or negative expected value territory:
| Market state | Drift mu_t | Volatility sigma_t | Grid outcome |
|---|---|---|---|
| Ranging + volatile | Low | High | Best — rapid profit collection, frequent level closures |
| Ranging + quiet | Low | Low | Good — slow but steady profit accumulation |
| Trending + volatile | High | High | Survivable — counter-trend chop provides relief |
| Trending + quiet | High | Low | Worst — clean trend, maximum loss accumulation |
The danger scenario is not high volatility that generates the counter-trend oscillations that close winners. The danger is a low-volatility directional trend: price moves cleanly through level after level, accumulating losses at the triangular rate without counter-moves. This "quiet trend" is the specific condition the regime detection system in Section 5 is designed to identify. The "trending + volatile" state being survivable rather than fatal is the insight that motivates TGT mode in Section 6 — if the trend can be identified, the grid can harvest it rather than fight it.
The Dominant Sensitivity Finding
The thesis presents a full sensitivity analysis (Chapter 6, Figure 6.6). The finding drives the entire architecture:
| Parameter | Impact on equity | Implication |
|---|---|---|
| sigma_t (volatility) | Dominant | The single most important runtime variable |
| mu_t (drift) | Dominant | Second most important — trending is the primary danger |
| g (grid width) | Moderate | Affects when ruin occurs, not whether it occurs |
| v (profit per level) | Low | Changes dollar amounts, not structural dynamics |
This has a direct architectural consequence: the EA's design effort must be concentrated on measuring and responding to sigma_t and mu_t. Optimizing grid spacing or lot size without addressing the regime is working on the wrong problem. This is why the system dedicates VolatilityGate and CPDEngine to regime detection, and why it has three grid algorithms for different sigma_t/mu_t conditions.
ATR-Dynamic Spacing and Dynamic Lot Sizing
Although the sensitivity analysis shows grid spacing g has only moderate impact compared to sigma_t and mu_t, a fixed spacing that was safe at last month's volatility can be dangerously tight at this month's. The thesis (GA optimization study, Section 10.6.2) found that the optimal static spacing for forex corresponded to roughly 10–15% of the Average Daily Range — implying that spacing should scale with the instrument's current volatility, not be a hardcoded constant.
ATR-Dynamic Grid Spacing
The EA recalculates grid spacing at each cycle restart from a dedicated D1-timeframe ATR handle. The formula is straightforward: g = ATR(period, D1) × multiplier, clamped to a configurable floor and ceiling. Spacing is not changed mid-cycle — the grid level structure would become inconsistent. The cycle restart is the natural recalibration point.
An important design decision: the spacing ATR uses the daily timeframe (D1) while the regime detection ATR uses H4. These are different questions. Spacing wants a broad daily-range estimate to size the grid ladder. Regime detection wants a multi-hour signal responsive enough to matter within a cycle that typically lives hours to a few days.
//+------------------------------------------------------------------+ //| ATR-dynamic grid spacing — recalculated at each cycle start | //+------------------------------------------------------------------+ double ComputeATRSpacing() { if(!InpUseATRSpacing || g_spacingATRHandle == INVALID_HANDLE) return InpGridSpacing * SymbolInfoDouble(_Symbol, SYMBOL_POINT); double atrBuffer[]; ArraySetAsSeries(atrBuffer, true); if(CopyBuffer(g_spacingATRHandle, 0, 1, 1, atrBuffer) <= 0) { PrintFormat("[BGC Grid] ATR spacing buffer copy failed; using fixed spacing"); return InpGridSpacing * SymbolInfoDouble(_Symbol, SYMBOL_POINT); } double atrValue = atrBuffer[0]; double spacingPrice = atrValue * InpATRSpacingMultiplier; //--- Clamp to [min, max] in price units double pointSize = SymbolInfoDouble(_Symbol, SYMBOL_POINT); double minSpacingPrice = InpMinGridSpacingPts * pointSize; double maxSpacingPrice = InpMaxGridSpacingPts * pointSize; spacingPrice = MathMax(spacingPrice, minSpacingPrice); spacingPrice = MathMin(spacingPrice, maxSpacingPrice); int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); spacingPrice = NormalizeDouble(spacingPrice, digits); return spacingPrice; }
The function reads the completed D1 ATR (bar index 1, avoiding the incomplete bar 0) via CopyBuffer, multiplies by InpATRSpacingMultiplier (default 0.12), and clamps to a [min, max] point range. It falls back to fixed spacing if the ATR handle is invalid or the buffer copy fails. The result is normalized to the symbol's digit precision with NormalizeDouble.
A subtle consequence: when ATR-dynamic spacing is active, the ATR/spacing magnitude check inside VolatilityGate becomes self-calibrating (ATR / (ATR × multiplier) = 1/multiplier), making it uninformative. The implementation skips that check and relies solely on the drift-based sigma/mu test.
Dynamic Base Lot Sizing
Remark 42 of the thesis — titled "Dynamic Trade Sizing" — is one of the most practically significant findings. It contains two ideas. First, upward compounding: when a cycle ends profitably, the next cycle's base lot should reflect the larger account. A fixed BaseLot = 0.01 regardless of account growth means the EA never compounds, it produces the same dollar return per cycle forever. Second, downward protection: as losses accumulate, lot sizes should shrink to slow the triangular loss accumulation.
The EA implements upward compounding by recalculating the base lot at each cycle restart:
//+------------------------------------------------------------------+ //| Dynamic base lot — equity-compounding at cycle restart | //+------------------------------------------------------------------+ double ComputeDynamicBaseLot(double cycleEquity) { if(!InpUseDynamicBaseLot) return InpBaseLot; double lot = cycleEquity * InpLotFraction; //--- Normalise to broker constraints double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); double maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); double step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); double cap = MathMin(maxLot, InpMaxLotSize); lot = MathMax(lot, minLot); lot = MathMin(lot, cap); if(step > 0) lot = MathFloor(lot / step) * step; lot = NormalizeDouble(lot, 2); if(lot < minLot) lot = minLot; return lot; }
The effective base lot is cycleEquity × InpLotFraction, re-computed at each cycle start. After a profitable cycle, the larger equity produces a larger lot compounding gains. After a losing cycle, the smaller equity produces a smaller lot reducing exposure. The floor/step normalization via SymbolInfoDouble ensures the lot conforms to broker volume constraints (SYMBOL_VOLUME_MIN, SYMBOL_VOLUME_MAX, SYMBOL_VOLUME_STEP).
Regime Detection — VolatilityGate and Change Point Detection
The sensitivity finding from Section 3 established that sigma_t and mu_t dominate the equity process. This section implements the response: a two-layer regime detection system that answers two different questions. VolatilityGate asks "are conditions favorable for BGT re-arming right now?" CPDEngine asks "has the process structurally shifted?"
VolatilityGate — The Continuous Filter
VolatilityGate computes three things on the regime timeframe (H4 by default): ATR (the sigma_t proxy), drift magnitude (the mu_t proxy), and the sigma_t/mu_t ratio. High ratio means volatility dominates drift, safe for bi-directional grid trading. Low ratio means drift dominates, dangerous.
The drift formula deserves particular attention. It uses the average absolute bar-by-bar MA change over the lookback window:
mu_t = (1/N) × Σ |MA[i] - MA[i+1]| for i = 0...N-1
This is the research-correct proxy for instantaneous drift magnitude from the Itô SDE. The critical detail: it uses absolute changes, not net displacement. Net displacement can cancel to zero when a trend reverses within the window, falsely reporting "no drift" during a whipsaw period. The absolute version accumulates the true magnitude of directional movement.
//+------------------------------------------------------------------+ //| Compute drift (mu_t) as average absolute bar-by-bar MA change | //+------------------------------------------------------------------+ double ComputeDrift() { if(m_maHandle == INVALID_HANDLE) return 0; double maBuffer[]; ArraySetAsSeries(maBuffer, true); if(CopyBuffer(m_maHandle, 0, 1, m_maPeriod + 1, maBuffer) < m_maPeriod + 1) return m_lastDrift; double sumAbsChanges = 0; for(int i = 0; i < m_maPeriod; i++) sumAbsChanges += MathAbs(maBuffer[i] - maBuffer[i + 1]); m_lastDrift = sumAbsChanges / m_maPeriod; return m_lastDrift; } //+------------------------------------------------------------------+ //| Compute sigma_t/mu_t ratio | //| High = ranging (safe), Low = trending (dangerous) | //+------------------------------------------------------------------+ double ComputeSigmaMuRatio() { double atr = GetATRValue(); double drift = ComputeDrift(); if(drift < 0.001) { m_lastSigmaMuRatio = 999; //--- effectively no drift = pure ranging return m_lastSigmaMuRatio; } m_lastSigmaMuRatio = MathMin(atr / drift, 999.0); return m_lastSigmaMuRatio; }
When drift is extremely small, sigma/mu is capped at 999, effectively indicating pure ranging. The ratio feeds into a cooldown mechanism: when sigma/mu drops below InpMinSigmaMuRatio (default 5.0), the system marks conditions as unfavorable. BGT re-arming pauses. N consecutive favorable regime-timeframe bars must clear before resuming, preventing rapid pause/resume oscillation. Crucially, the drift gate applies to BGT only, TGT and MGT bypass it because they are designed for the very conditions the gate detects.
CPDEngine — The Structural Break Detector
VolatilityGate is a continuous, smoothed signal that transitions gradually. CPD (Change Point Detection) adds a discrete structural break signal: it detects the moment when the underlying process shifts from one regime to another. The EA implements a frozen-baseline CUSUM algorithm: it collects an initial baseline window, freezes that baseline, then accumulates deviations. When the cumulative sum crosses a scaled threshold, a structural break is declared.
The baseline is intentionally frozen after initialization, not recomputed on a rolling basis. A rolling baseline would adapt toward the very trend it is supposed to detect, defeating the purpose. The frozen baseline keeps the reference regime fixed long enough for genuine structural drift to accumulate.
When CPD fires, it sets g_cpdBlockActive. If TGT is enabled, the next cycle is forced into TGT mode (the trend-following algorithm in Section 6). If TGT is disabled, the next cycle is blocked entirely. The EA refuses to deploy a fresh BGT grid into a confirmed trend. The block clears only after VolatilityGate reports recovery through the full cooldown period.
Three Grid Modes — BGT, TGT, and MGT
The thesis formalized three distinct grid algorithms, each designed for a specific market regime. The current EA pauses during unfavorable conditions (via VolatilityGate). The thesis implies that pausing is conservative, not optimal. During a detected trend, a trend-following grid can harvest the directional movement rather than sitting idle.
BGT — Bi-Directional Grid Trading
The default ranging-market mode. Both buy and sell sides are armed at each grid level above and below the anchor, with fixed take-profit at the next adjacent level. An optional buy-bias mode (InpBuyBiasAboveAnchor) can skip sell orders above the anchor for instruments with a structural upward drift. BGT is the mode described by the mathematical framework in Sections 1–3. Its failure mode is directional trends that create one profitable side and one accumulating losing side.
TGT — Trending Grid Trading
The trend-following mode. Only stop orders in the trend direction are placed. Uptrend means buy stops above the anchor, downtrend means sell stops below. No trade is placed at the origin (avoiding an immediately losing position against the trend). The defining characteristic: no per-trade take-profit. Positions remain open and accumulate as the trend continues. The basket exits only through the profit target, kill switch, age limit, or weekend close. This variable-TP design allows TGT to behave like a scaled pyramid entry rather than a grid that closes winners too early.
TGT activates through two paths: a soft trigger when sigma/mu drops below InpTGTSigmaMuThreshold (drift dominating volatility), or a hard trigger when CPD fires a structural break. Its failure mode is a false trend signal followed by immediate reversal, carrying open positions in the trend direction with no hedge.
MGT — Mean-Reversion Grid Trading
The temporary post-trend recovery mode. Counter-trend limit orders only: sell limits above the anchor, buy limits below. Fixed take-profit toward the anchor. MGT is the structural inverse of TGT. It anticipates that an extended trend will revert. It triggers when the previous TGT cycle ended via kill switch or regime restart (g_lastCycleWasTGTKill = true). MGT is intentionally exempt from mid-cycle regime checks, otherwise DetermineGridMode() would often flip it out of MGT on its first new bar, destroying the intended recovery behavior.

Fig. 4. BGT, TGT, and MGT mode behavior
| Property | BGT | TGT | MGT |
|---|---|---|---|
| Direction | Both | Trend only | Counter-trend only |
| Entry type | Stop + Limit | Stop only | Limit only |
| TP per trade | Fixed (next level) | Variable (basket) | Fixed (toward anchor) |
| Best regime | Ranging | Trending | Post-trend reversion |
Mode Selection Logic
At each cycle start, DetermineGridMode() selects the appropriate algorithm through a three-tier priority:
//+------------------------------------------------------------------+ //| Regime-adaptive mode selection — three-tier priority | //+------------------------------------------------------------------+ ENUM_GRID_MODE DetermineGridMode() { //--- Priority 1: MGT — previous TGT cycle killed -> post-trend recovery if(InpEnableMGT && g_lastCycleWasTGTKill) { g_lastCycleWasTGTKill = false; //--- consume the trigger return GRID_MODE_MGT; } //--- Priority 2: TGT — CUSUM detected regime change (hard signal) if(g_cpdBlockActive && InpEnableTGT) return GRID_MODE_TGT; //--- Priority 2b: TGT — sigma/mu below threshold (soft signal) if(InpEnableTGT) { double sigmaMu = g_volatilityGate.ComputeSigmaMuRatio(); if(sigmaMu < InpTGTSigmaMuThreshold) return GRID_MODE_TGT; } return GRID_MODE_BGT; //--- Default: ranging conditions }
(1) MGT — if the previous TGT cycle was killed (kill switch or regime restart), enter mean-reversion mode. The trigger flag is consumed so it fires once. (2) TGT — two paths: a hard signal from CPD forces TGT regardless of sigma/mu, or a soft signal when sigma/mu drops below the threshold indicating drift dominates. (3) BGT — the default for ranging conditions. This implements the thesis's regime-adaptive cycling: BGT → TGT → MGT → BGT.
Production Design — Start Gates, Persistence, and Diagnostics
A critical design principle is that a new cycle does not always start immediately. The research establishes that deploying a grid into a confirmed trend is the primary ruin scenario. StartNewCycle() runs through three gate checks before arming the grid:
void StartNewCycle() { //--- Gate 1: CPD hard block — don't deploy BGT into a confirmed trend if(g_cpdBlockActive && !InpEnableTGT) { g_cycleBlocked = true; g_session.state = GRID_STATE_PAUSED; return; } //--- Gate 2: Price-level filter — don't anchor at a recent N-bar extreme if(IsPriceAtExtreme(plDistHigh, plDistLow, plMinDist)) { g_priceLevelBlocked = true; g_cycleBlocked = true; g_session.state = GRID_STATE_PAUSED; return; } g_priceLevelBlocked = false; g_cycleBlocked = false; //--- ... ATR spacing, session init, dynamic lot sizing ... //--- Gate 3: BGT disabled — wait for TGT/MGT conditions ENUM_GRID_MODE mode = DetermineGridMode(); if(InpDisableBGT && mode == GRID_MODE_BGT) { g_cycleBlocked = true; g_session.state = GRID_STATE_PAUSED; return; } //--- All gates passed — arm the grid g_session.gridMode = mode; //--- ... }
Gate 1 checks the CPD block: if CUSUM has fired and TGT is disabled, the EA refuses to deploy BGT into a confirmed trend. Gate 2 checks the price-level filter: if price is near a recent N-bar extreme, the grid's mean-reversion constraint is weakest and anchoring is dangerous. Gate 3 handles optional BGT-disable mode: if the user has disabled BGT, the EA waits for TGT or MGT conditions. Each gate implements a specific research finding — CPD from Thesis 10.6.1, the hidden barrier weakness from the LIL bounds analysis, and the mode-switching architecture from Thesis 10.3–10.5.
Persistence and Recovery
StateManager saves cycle state (anchor, spacing, session extremes, equity baseline, cycle count, grid mode, trend direction) to MQL5 global variables. Tickets are not persisted. On resume, GridCore::RecoverPositions() scans actual open positions from the broker and maps them back by price proximity. This makes the recovery robust to broker-side ticket reassignment. StateManager also supports ATR-dynamic spacing mismatches so that a spacing recalculation between sessions does not falsely invalidate resumable state.
Weekend Rules
A weekend flat close forces all positions closed on Friday at a configurable hour and delays restart until Monday. This protects against gap risk that the continuous diffusion model which underpins the entire mathematical framework does not account for. The weekend close is implemented as a cycle-ending event: it increments the cycle counter, clears saved state, and the Monday restart goes through all the same start gates as any normal cycle.
The Diagnostics Pipeline
The EA relies on structured CSV diagnostics instead of journal-only inspection. DiagnosticLog.mqh buffers three datasets in memory: BarSnapshot (30 fields per bar), DiagCycleSummary (28 fields per completed cycle), and DiagEventRecord (7 fields per event: covering cycle starts/ends, drift pauses, CPD fires, mode switches, regime restarts, weekend events, and more). These flush to CSV in MetaTrader's common files directory during OnDeinit() or OnTester().
Two companion Python scripts complete the pipeline. analyze_backtest.py loads the latest CSV outputs, runs 10 bug-detection checks, prints 13 text-analysis sections, and optionally generates 8 diagnostic charts. calibrate_parameters.py loads all CSV files across all backtest dates, optionally pulls MT5 historical data, and runs six calibration passes: variance age curves, Theorem 53 kill-switch validation, sigma/mu threshold derivation, profitability-condition zone analysis, expected-steps validation, and multi-timeframe regime detection. Together they provide a research-grade toolkit for validating and refining the EA's parameters against empirical data.
Strategy Testing
The full system was backtested on the Strategy Tester with the following configuration. The input settings and results are shown below.

Fig. 5. Strategy Tester equity curve

Fig. 6. Strategy Tester results summary
The Diagnostics Pipeline — Analyzing and Calibrating Your Grid
The EA writes structured CSV diagnostics automatically during every backtest and live session. Two companion Python scripts transform that raw data into actionable analysis. This section shows how to use them and what they produce.
Both scripts expect the CSV files in MetaTrader's common files directory (typically %APPDATA%/MetaQuotes/Terminal/Common/Files on Windows). The EA writes three files per run with a BGC prefix: bar snapshots (30 fields per bar), cycle summaries (28 fields per completed cycle), and an event log (7 fields per event covering cycle starts, kills, drift pauses, CPD fires, mode switches, weekend events, and more).
analyze_backtest.py — Post-Run Diagnostics
This script loads the latest set of CSVs and produces immediate diagnostic output. Run it from the command line:
python analyze_backtest.py [csv_dir] [--no-charts] [--date YYYYMMDD]
With no arguments, it defaults to the MetaTrader common files directory and picks the latest CSVs by date suffix. The --date flag selects a specific run. The --no-charts flag skips chart generation for fast text-only analysis.
The script prints 13 text-analysis sections covering cycle statistics (count, duration, profit distribution by mode), drawdown analysis, regime detection behavior, kill switch activations, and re-arming patterns. It also runs 10 automated bug-detection checks that flag anomalies such as zombie cycles (excessively long duration), orphaned positions, or mode classification inconsistencies. When charts are enabled, it generates 8 diagnostic PNGs into an analysis_output/ directory — including equity curves with cycle boundaries marked, drawdown heatmaps, and mode distribution histograms.
calibrate_parameters.py — Cross-Run Parameter Calibration
While analyze_backtest.py examines a single run, calibrate_parameters.py loads all CSVs across all dates — aggregating data from multiple backtests to derive statistically robust parameter recommendations. Run it with:
python calibrate_parameters.py [csv_dir] [--no-mt5]
The --no-mt5 flag skips direct MT5 historical data access when it is unavailable (e.g., running on a machine without MetaTrader installed). The script runs six calibration passes:
- Variance age curve — plots realized cycle variance against cycle age in hours, validating the exponential variance growth finding from Section 3 against your actual backtest data. This is what calibrates InpMaxCycleHours.
- Theorem 53 kill-switch validation — computes the theoretical ruin probability at your current grid configuration and compares it against the observed kill-switch firing rate. This validates whether InpRuinThreshold is in the correct region for your account size and grid depth.
- Sigma/mu threshold derivation — analyzes the relationship between the sigma_t/mu_t ratio at cycle start and eventual cycle outcome (profit vs kill). This calibrates InpMinSigmaMuRatio and InpTGTSigmaMuThreshold.
- Profitability-condition zone analysis — maps which combinations of volatility and drift produced profitable cycles versus losing cycles, validating the four-quadrant market states from Section 3.
- Expected-steps validation — compares the theoretical expected number of grid-level crossings before absorption against the observed cycle lengths.
- Multi-timeframe regime detection — evaluates whether the current InpRegimeTF choice (default H4) produces the best regime classification accuracy across the historical data.
The Workflow
The intended usage is a feedback loop: run a backtest in MetaTrader (CSVs are written automatically) → run analyze_backtest.py for immediate diagnostics and bug detection → accumulate multiple backtests across different parameter settings or time periods → run calibrate_parameters.py to derive refined parameters → apply the recommendations and repeat. This positions the EA not as a black box but as a research instrument with a built-in empirical validation pipeline.
Conclusion
This article presented a research-grounded approach to grid trading that addresses the known mathematical failure modes through constrained, restartable cycles with regime-aware gating. The key findings that drive the architecture are:
- Grid trading is not martingale. Loss accumulation follows the triangular number series (quadratic), not exponential doubling. This makes grid EAs engineerable — the slower loss growth provides time for detection and intervention.
- The L_t formula gives constant-time risk visibility. Total loss exposure can be computed from two measurements without inspecting individual positions. In practice, we use L_t for diagnostics and direct equity measurement for the live kill switch, because the formula's assumptions (flat lots, no re-arming) differ from the production EA.
- Equity variance grows exponentially with cycle time. An older cycle is mathematically more dangerous than a younger one. This justifies unconditional age-based restart as a structural necessity.
- Unconstrained grid operation leads to almost sure ruin. This is a proven theorem. Restart is the structural response — resetting both the variance clock and the grid level.
- sigma_t and mu_t dominate everything. Grid spacing and lot size are secondary. The architecture is centered on measuring and responding to the volatility/drift regime through VolatilityGate and CPDEngine.
- Three algorithms for three markets. BGT harvests ranging conditions, TGT follows confirmed trends, MGT captures post-trend reversion. The mode selection logic adapts at each cycle restart based on the detected regime.
- ATR-dynamic spacing and equity-compounding lots make the grid adaptive to current market conditions rather than dependent on hardcoded constants.
- Restart is not failure. The research proves that a single indefinite cycle converges to ruin. Profitable restart cycles are the mechanism of survival — they reset variance, crystallize gains, and re-enter the favorable zone.
Disclaimer: This article is for educational purposes. Grid trading involves significant financial risk. The mathematical proofs presented here demonstrate both the potential and the limitations of constrained grid systems. Always test thoroughly on demo accounts before risking live funds. Past backtest performance does not guarantee future results.
All code referenced in the article is attached below. The following table describes the source code files.
| File name | Description |
|---|---|
| GridEA.mq5 | Main EA file — orchestrator, inputs, cycle management, runtime policy |
| Defines.mqh | Shared enums, structs, constants, and session model |
| GridCore.mqh | Order placement, sync, re-arm, close, and recovery for BGT/TGT/MGT |
| LotEngine.mqh | Per-level lot sizing with mild multiplier and dynamic shrink modes |
| LtMonitor.mqh | L_t computation, RPR, Theorem 52/53 diagnostics, path-aware tracking |
| ProfitMonitor.mqh | Cycle profit logic and position counting |
| VolatilityGate.mqh | ATR/drift regime filter with cooldown mechanism |
| CPDEngine.mqh | Frozen-baseline CUSUM structural break detector |
| StateManager.mqh | Cycle state persistence via MQL5 global variables |
| DiagnosticLog.mqh | CSV diagnostic logging (bar snapshots, cycle summaries, events) |
| analyze_backtest.py | Post-run analysis: 13 text sections, 10 bug checks, 8 chart outputs |
| calibrate_parameters.py | Cross-run calibration: variance age curves, sigma/mu threshold derivation, Theorem 53 validation |
- Gambler's Ruin Problem and Bi-Directional Grid Constrained Trading and Investment Strategies — IMFI, 2020
- Drawdown and Drawup of Bi-Directional Grid Constrained Stochastic Processes — JMS, 2020
- Application of Bi-Directional Grid Constrained Stochastic Processes to Algorithmic Trading — JMS, 2021
- Iterated Logarithm Bounds of Bi-Directional Grid Constrained Stochastic Processes — GSA, 2021
- Bi-Directional Grid Constrained Stochastic Processes and Their Applications in Mathematical Finance — PhD Thesis, 2022
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.
Introduction to MQL5 (Part 43): Beginner Guide to File Handling in MQL5 (V)
Neural Networks in Trading: Adaptive Detection of Market Anomalies (DADA)
Developing Market Entropy Indicator: Trading System Based on Information Theory
MetaTrader 5 Machine Learning Blueprint (Part 9): Integrating Bayesian HPO into the Production Pipeline
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use