ZoneMartingale EA is the "zone recovery" robot coded live on the channel, and its full source code is shared for free. It is a good thing to study: the file is under 200 lines long, it has only four inputs, and it shows plainly how a zone martingale works. It also shows why this kind of robot calls for caution.
This article goes through the code of version 1.0 block by block. By the end, you will be able to read every decision the robot makes, redo the calculation behind its gains, and fix the weak points of this first version.
The idea in one minute
The robot opens a first position when the RSI leaves an extreme area. Around the entry price, it draws a zone of fixed width and two targets, one above and one below.
- If price reaches one of the two targets, all positions are closed: the cycle is over.
- If price crosses the zone the other way, the robot opens an opposite position, with a lot multiplied by a factor (2 by default).
- Each new crossing adds a position, always bigger, in the direction of the move.
As long as price eventually reaches a target, the cycle ends in profit. The risk lies elsewhere: if price keeps going back and forth inside the zone, the lot doubles every time.
The inputs
input double lot = 0.01;
input double Range = 2000;
input double Target = 4000;
input double factor = 2.0;lot: the volume of the first position.Range: the width of the zone, in points.Target: the distance between each edge of the zone and its target, in points.factor: the multiplier applied to each new position.
A point is the smallest price step of the symbol (_Point). On a symbol quoted with two decimals, 2,000 points are 20.00 in price; with five decimals, 0.02000. Distances therefore have to be set symbol by symbol.
The entry signal: the RSI at an extreme
int bars = iBars(_Symbol, PERIOD_CURRENT);
if(totalBars == bars) return;
totalBars = bars;
if(PositionsTotal() > 0) return;
if(!CopyBuffer(rsi_handle, 0,1,2, rsiData)) return;
if(rsiData[1] < OverSoldLevel && rsiData[0] > OverSoldLevel) {
MQL5_Lab_trade.Buy(lot, _Symbol, Bid, 0,0, "MQL5 Lab");Three things to notice:
- The signal is checked once per candle, when a new bar opens. The robot does not react to moves inside a candle to enter.
- No signal is checked while a position is open: one cycle at a time.
CopyBuffer(rsi_handle, 0, 1, 2, rsiData)copies the RSI of the last two closed candles. WithoutArraySetAsSeries, the array keeps chronological order:rsiData[0]is the older value,rsiData[1]the most recent. The conditionrsiData[1] < 30 && rsiData[0] > 30therefore means: the RSI was above 30 two candles ago and dropped below 30 on the last closed candle.
This detail matters: the code comment speaks of an "exit from oversold", but the condition, read in the real order of the array, detects an entry into oversold. The robot buys when the RSI has just dropped below 30, not when it climbs back out. That is a defensible behaviour (buying the excess), but it is not the one the comment announces. If you want the buy on the exit from oversold, swap the indexes. The same reasoning applies to the sell around 70.
The zone and the targets
Once the buy is filled, the robot reads the real open price and draws its levels:
double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
zoneHigh = NormalizeDouble(openPrice, _Digits);
zoneLow = NormalizeDouble(zoneHigh - zoneRange, _Digits);
zoneTargetHigh = NormalizeDouble(zoneHigh + zoneTarget, _Digits);
zoneTargetLow = NormalizeDouble(zoneLow - zoneTarget, _Digits);For a buy, the top of the zone is the entry price and the bottom is Range points lower. The targets sit Target points beyond each edge. For a sell, it is the mirror image: the bottom of the zone is the entry price.
Recovery: an opposite position on each crossing
if(Bid > zoneHigh) {
if(isBuyDone == false || lastDirection < 0) {
MQL5_Lab_trade.Buy(lots_Rec, _Symbol, Bid, 0,0, "MQL5 Lab");
lastDirection = 1;
recovery_lot = recovery_lot*factor;On every tick, the robot checks where price is. If it moves above the top of the zone while the last position was a sell, it buys. If it moves below the bottom while the last one was a buy, it sells. The recovery lot starts at lot × factor and is multiplied by factor with each new position.
The exit: close everything at a target
if (Bid > zoneTargetHigh || Bid < zoneTargetLow) {
deleteZoneLevels();
for (int i=PositionsTotal()-1; i>=0; i--) {
ulong ticket = PositionGetTicket(i);
...
MQL5_Lab_trade.PositionClose(ticket);As soon as price goes beyond one of the two targets, all positions are closed and the variables reset. The robot then waits for a new RSI signal.
Why the cycle ends in profit
Take the default settings: a zone of width Z, targets at 2Z, a factor of 2, and a first lot of 1 (to keep the numbers simple). We count in multiples of Z, without the spread.
| Scenario | Open positions | Result at the target |
|---|---|---|
| Price goes straight to the upper target | Buy 1 | +2 |
| It drops, then reaches the lower target | Buy 1, sell 2 | −3 + 4 = +1 |
| Down, up, then upper target | Buy 1, sell 2, buy 4 | +2 − 6 + 8 = +4 |
| Down, up, down, then lower target | Buy 1, sell 2, buy 4, sell 8 | −3 + 4 − 12 + 16 = +5 |
Each new position, twice as big as the previous one and in the direction of the move, covers the losses of the others. The cycle therefore always ends positive if price reaches a target, if orders fill at the planned levels, and if the account has the margin to hold on.
The price to pay: exposure
The total volume open after n positions is lot × (2^n − 1) with a factor of 2:
| Open positions | Last lot | Total volume |
|---|---|---|
| 1 | 0.01 | 0.01 |
| 4 | 0.08 | 0.15 |
| 6 | 0.32 | 0.63 |
| 8 | 1.28 | 2.55 |
| 10 | 5.12 | 10.23 |
Nine crossings of the zone in a row, which happens in a ranging market, turn a first lot of 0.01 into more than 10 open lots. The gain of the cycle, meanwhile, stays at a few Z on the initial lot. The spread is paid on every new position, and a fill beyond the planned level (slippage, Monday open) eats into the calculation above. That is the whole problem with martingales: many small gains, and a rare but very heavy risk.
Five fixes to make before using it
Version 1.0 was written live to show the principle. Before using it, even on demo, here is what needs fixing.
1. Manage only its own positions
The robot counts and closes all positions on the account, whatever the symbol. A manual position on another asset blocks new signals, and a target being hit would close it. Add a magic number and filter:
input long Magic = 2601;
// in OnInit()
MQL5_Lab_trade.SetExpertMagicNumber(Magic);
// a function that counts only the robot's positions
int MyPositions() {
int n = 0;
for(int i = PositionsTotal() - 1; i >= 0; i--) {
ulong t = PositionGetTicket(i);
if(t > 0 && PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == Magic) n++;
}
return n;
}Use this filter everywhere the code loops through positions, and replace PositionsTotal() > 0 with MyPositions() > 0.
2. Cap the recovery
Add a maximum number of positions and a maximum lot. Once the cap is reached, the robot adds nothing more and warns you:
input int MaxPositions = 6;
input double MaxLot = 0.32;
if(MyPositions() >= MaxPositions || lots_Rec > MaxLot) {
Alert(_Symbol, ": recovery cap reached");
return;
}This is the most important decision: it sets in advance the largest loss you accept on one cycle.
3. Buy at the right price
Buys pass Bid as the price. A buy fills at the Ask price; on an instant execution account, a wrong price can get the order rejected. Pass 0: the CTrade class then takes the current price on the right side.
MQL5_Lab_trade.Buy(lots_Rec, _Symbol, 0, 0, 0, "ZoneMartingale");4. Draw the right lower target
A typo draws the lower target under the name ZONE_L instead of ZONE_T_L. Since that object already exists, the orange line never shows up. Fix the name in both calls to drawZoneLevel.
5. Do not lose track on a restart
The zone, the direction and the current lot are kept in variables. If MetaTrader restarts in the middle of a cycle, the robot forgets everything: the positions stay open, with no target and no recovery. Save the state in terminal global variables (GlobalVariableSet) and read it back in OnInit(), or close the cycle by hand before any restart.
One last line can go: EventSetTimer(60) starts a timer that no OnTimer() function uses.
How to test it
- Open the strategy tester (Ctrl+R), pick the robot, the symbol and the timeframe.
- Choose Every tick based on real ticks: recovery plays out tick by tick, and an approximate mode would distort the result.
- Look at the maximum drawdown and the largest open volume, not only the profit. A steady profit with a 60% drawdown is not a good result.
- Test at least one ranging period. That is where the martingale shows its real limits.
The takeaway
ZoneMartingale EA is an excellent learning tool: a simple signal, readable state management, straightforward execution. The cycle calculation explains why martingales seem to win almost every time, and the exposure table explains why they sometimes end badly. Fix the five points above, set a cap you accept to lose, and keep it on demo until you have seen what it does in a market with no direction.