Why Martingale Deserves a Second Look
Every trading forum has that thread. Someone posts their "100% win rate" martingale EA, gets roasted by the purists, and the thread devolves into warnings about account blowups. The criticism is mostly deserved. A naked martingale that doubles position size after every loss is a one-way ticket to a margin call. But here's the thing nobody tells you: martingale isn't inherently broken. It's just usually implemented without risk controls, which is like driving a car with no brakes and blaming the engine when you crash.
I've spent years building EAs for clients, and a well-constrained martingale recovery strategy can actually produce a smooth equity curve. The key is treating it as a recovery tool, not a money printer. You're not trying to win every trade. You're trying to survive the losing streaks long enough for the law of large numbers to work in your favor. This article walks through a practical MQL5 implementation with the safety rails that keep your account alive.
Before we dive into code, one honest disclaimer: no martingale variant is "safe" in absolute terms. What we're building here is a system with defined, bounded risk. The equity stop guarantees you can't lose more than a set percentage in a single basket cycle. That's a massive improvement over the naive version, but it's still possible to hit that stop repeatedly if market conditions are brutal. Treat this as a tool with known failure modes, not a holy grail.
How a Martingale Recovery Strategy Works
A martingale recovery strategy flips the standard position sizing logic. Instead of risking a fixed percentage per trade, you increase your position size after each loss. The idea is that one winning trade recovers all previous losses plus a small profit. In a grid martingale EA, this often means adding positions at set intervals against the current trend, averaging down until price pulls back to your average entry.
The math is seductive. Start with 0.01 lots. Lose, go to 0.02. Lose again, 0.04. A single win at 0.04 doesn't just cover the 0.03 you lost — it puts you ahead. The problem is the exponential curve. After ten consecutive losses on a 0.01 base, you're trading 10.24 lots. That's not a recovery; that's a liquidation event waiting to happen.
So the real question isn't whether martingale works. It's whether you can control the downside when price moves against you further than expected. That's where risk controls come in. You need three things: a maximum number of recovery trades, an equity stop that kills the whole experiment, and a recovery multiplier that doesn't grow too aggressively.
The Core Logic
Here's the fundamental structure of any martingale recovery EA:
- Open an initial position with base lot size
- If price moves against you by a defined distance, open another position with increased lot size
- Repeat until price returns to the average entry price of all open positions
- Close everything when the basket reaches a target profit
- If the maximum trade count or equity drawdown limit is hit, stop and accept the loss
The recovery multiplier is the most critical parameter. A 2.0 multiplier is the classic martingale, but it's brutal. I prefer 1.3 to 1.5 in practice. It means you need more trades to recover, but each step is less violent. A 1.5 multiplier after five losses on 0.01 lots puts you at 0.075 lots, not 0.32. Much easier to manage.
Why Grid Spacing Matters More Than You Think
Most discussions focus on the multiplier, but grid spacing is equally important. Too tight — say 100 points on EURUSD — and you'll stack positions quickly, eating into your MaxTrades limit before price has room to breathe. Too wide — 800 points or more — and each basket takes forever to reach take profit, tying up margin and exposing you to swap costs for days.
There's a tradeoff between frequency and survivability. A tight grid recovers faster but hits MaxTrades more often. A wide grid survives longer but locks capital in floating loss for extended periods. I like to set the grid step so that the maximum number of trades spans roughly 1.5 to 2 times the average daily range of the pair. On EURUSD, with an average daily range around 700–900 points, a 300-point step across 7 trades covers 2,100 points. That's more than two days of typical movement — enough buffer for most pullbacks, but not so wide that you're waiting a week per basket.
Practical MQL5 Implementation
Let's get into the code. I'm assuming you're working in MetaEditor with MQL5, not MQL4. The API is different enough that porting requires more than a find-and-replace. If you're on MT4, the logic translates, but you'll need to adapt the trade functions — OrderSend in MQL4 becomes CTrade::Buy/CTrade::Sell in MQL5, and position management works differently.
First, the input parameters. This is where most martingale EAs fail — they don't expose enough controls. You need to be able to tune the recovery behavior without recompiling.
| Parameter | Type | Default | Description |
|---|---|---|---|
| BaseLotSize | double | 0.01 | Starting lot size for the initial position. |
| LotMultiplier | double | 1.5 | Multiplier applied to lot size after each loss. Lower is safer. |
| GridStepPoints | int | 300 | Distance in points between grid levels. 300 points = 30 pips on most pairs. |
| MaxTrades | int | 7 | Maximum number of positions in one basket. Hard stop on grid expansion. |
| EquityStopPercent | double | 20.0 | Close all trades if equity drops this percentage below account balance. |
| TakeProfitPoints | int | 200 | Target profit per basket in points from average entry. |
You'll notice I didn't include a "TradeDirection" input. That's intentional — in a pure grid recovery system, direction is a filter you apply before the EA starts, not something it should decide dynamically. Many martingale EAs try to detect trends and flip direction mid-basket. That's a recipe for confusion and hidden losses. Keep it simple: you pick a direction based on your market analysis, and the EA handles the recovery within that direction.
The OnTick Handler
Here's a minimal but functional OnTick structure. It checks three conditions: whether we're already in a basket, whether we need to add a grid level, and whether any safety stop has been triggered.
void OnTick()
{
// Check equity stop first
if(CheckEquityStop())
{
CloseAllPositions();
return;
}
// Count current basket positions
int basketCount = CountBasketPositions();
// If no positions, open initial trade
if(basketCount == 0)
{
OpenInitialPosition();
return;
}
// If we have positions, check if we need to add to the grid
if(basketCount < MaxTrades)
{
if(ShouldAddGridLevel())
{
double lotSize = BaseLotSize * MathPow(LotMultiplier, basketCount);
AddGridPosition(lotSize);
}
}
// Check basket take profit
if(CheckBasketTakeProfit())
{
CloseAllPositions();
}
}The ShouldAddGridLevel() function is where the strategy logic lives. It compares the current price against the last opened position's entry price. If price has moved against you by at least GridStepPoints, it's time to add another level. The lot size calculation uses MathPow to apply the multiplier cumulatively — that's the martingale engine.
Position Management with CTrade
In MQL5, you'll want to use the CTrade class from the standard library rather than raw OrderSend calls. It handles retries, error checking, and magic number filtering more gracefully. Here's a helper function for adding a grid position:
bool AddGridPosition(double lotSize)
{
CTrade trade;
trade.SetExpertMagicNumber(MagicNumber);
trade.SetDeviationInPoints(50);
double price;
if(PositionDirection() == POSITION_TYPE_BUY)
{
price = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
return trade.Buy(lotSize, _Symbol, price, 0, 0, "Grid buy");
}
else
{
price = SymbolInfoDouble(_Symbol, SYMBOL_BID);
return trade.Sell(lotSize, _Symbol, price, 0, 0, "Grid sell");
}
}Notice I'm not setting stop loss or take profit on individual positions. In a grid system, individual SL/TP levels are counterproductive — they'd close positions before the basket reaches its target. Instead, the basket-level take profit and equity stop handle all exits. If you're uncomfortable with no SL on individual trades, that's a valid concern, but it's inherent to how grid recovery works. The equity stop is your real protection.
The Equity Stop Implementation
This is the most important piece of the puzzle. Without it, you're gambling. The equity stop compares current equity to the account balance. If the drawdown exceeds your threshold, it liquidates everything. I use a percentage of balance rather than a fixed dollar amount because it scales with account size.
bool CheckEquityStop()
{
double equity = AccountInfoDouble(ACCOUNT_EQUITY);
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
if(balance == 0) return false;
double drawdownPercent = (balance - equity) / balance * 100.0;
if(drawdownPercent >= EquityStopPercent)
{
Print("Equity stop triggered. Drawdown: ", drawdownPercent, "%");
return true;
}
return false;
}One thing I've learned the hard way: don't rely solely on the EA's equity stop. Set a hard stop-loss on the broker side too. Some brokers have their own equity protection features, but they're not universal. If your VPS disconnects or the EA crashes, the equity stop won't fire. A broker-level stop is your last line of defense.
Also, be careful with the timing of the equity check. In fast-moving markets, equity can drop through your stop level and keep going before the EA processes the next tick. To mitigate this, I add a "confirmation" mechanism: the equity stop must be triggered on two consecutive ticks before closing. This prevents a single spike from liquidating the basket at the worst possible moment. The tradeoff is a slightly larger potential loss, but it avoids false triggers during spread widening events.
Handling Partial Fills and Re-quotes
In MQL5, market orders can be partially filled, especially on exotic pairs or during news events. Your grid EA needs to handle this gracefully. If trade.Buy()</code






