How to Move Stop Loss to Breakeven in MQL5 (EA Code + Real Backtest)
To move a stop loss to breakeven in MQL5, loop over your open positions on every tick, and once price is a set number of points in profit, call CTrade::PositionModify(ticket, open_price + lock, current_tp) for a buy (or open_price - lock for a sell), skipping positions whose stop is already at or past that level. Add a small "lock" of a few points so spread and commission are covered, and respect the broker's SYMBOL_TRADE_STOPS_LEVEL so the request is not rejected.
Why breakeven code goes wrong
A breakeven stop sounds like two lines of code, but most broken versions share the same bugs:
- Spamming the server. Calling
PositionModifyon every tick even after the stop is already at breakeven floods the journal with "no changes" errors (retcode 10025). You must check the currentPOSITION_SLfirst. - Wrong price side. A buy is closed at the Bid, a sell at the Ask. Measure a buy's profit from the Bid and a sell's from the Ask, otherwise breakeven arms early by the size of the spread.
- Ignoring the stops level. If the new stop is closer to price than the broker's minimum distance, the request fails with "invalid stops" (10016).
- Pips vs points. On a 5-digit EURUSD quote, 1 pip = 10 points. All inputs below are in points, multiplied by
_Point. - Moving the stop backwards. For a buy, the new stop must only ever go up. Never replace a trailing stop that is already above breakeven.
The breakeven function
This works on hedging and netting accounts because it walks positions by ticket and filters by symbol and magic number. PositionGetTicket(i) also selects the position, so the PositionGet* calls that follow read that position.
input int InpBETriggerPts = 200; // profit (points) that arms breakeven
input int InpBELockPts = 20; // points locked beyond entry (covers spread/commission)
void ManageBreakeven()
{
double pt = _Point;
double minD = (double)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL) * pt;
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong tk = PositionGetTicket(i); // also selects the position
if(tk == 0) continue;
if(PositionGetString(POSITION_SYMBOL) != _Symbol ||
PositionGetInteger(POSITION_MAGIC) != InpMagic)
continue;
double open = PositionGetDouble(POSITION_PRICE_OPEN);
double sl = PositionGetDouble(POSITION_SL);
double tp = PositionGetDouble(POSITION_TP);
if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
{
double newSL = NormalizeDouble(open + InpBELockPts * pt, _Digits);
if(bid - open < InpBETriggerPts * pt) continue; // not armed yet
if(sl != 0.0 && sl >= newSL) continue; // already at/above breakeven
if(bid - newSL <= minD) continue; // broker stops level
c_Trade.PositionModify(tk, newSL, tp);
}
else
{
double newSL = NormalizeDouble(open - InpBELockPts * pt, _Digits);
if(open - ask < InpBETriggerPts * pt) continue;
if(sl != 0.0 && sl <= newSL) continue;
if(newSL - ask <= minD) continue;
c_Trade.PositionModify(tk, newSL, tp);
}
}
}
Call ManageBreakeven() at the top of OnTick(), before any new-bar check, so the stop moves on the first tick that qualifies instead of waiting up to an hour for the next H1 bar. Entry logic can still run once per bar.
A few details worth knowing:
NormalizeDouble(..., _Digits)keeps the price on a valid tick so the comparison with the existing stop is exact.sl != 0.0handles positions opened with no stop at all; those get a breakeven stop too.PositionModifyreturningtrueonly means the request passed basic checks. If you need certainty, readc_Trade.ResultRetcode()afterwards and log anything other thanTRADE_RETCODE_DONE.- Validate inputs in
OnInit(): if the lock is bigger than the trigger the stop would land above the current price, so returnINIT_PARAMETERS_INCORRECT.
Full example EA
The test EA is a plain EMA(20/50) crossover on closed bars: buy when the fast EMA crosses above the slow one, sell on the opposite cross, one position at a time, 300-point stop, 600-point target, 0.10 lots. Breakeven arms at +200 points and locks +20 points. The entry side uses iMA handles, CopyBuffer on bars 1 and 2, and SetTypeFillingBySymbol so orders are accepted by brokers with different filling modes. OnTick() looks like this:
void OnTick()
{
ManageBreakeven(); // every tick
if(!IsNewBar())
return;
if(CopyBuffer(h_Fast, 0, 1, 2, g_fast) < 2 || CopyBuffer(h_Slow, 0, 1, 2, g_slow) < 2)
return;
bool crossUp = g_fast[1] <= g_slow[1] && g_fast[0] > g_slow[0];
bool crossDown = g_fast[1] >= g_slow[1] && g_fast[0] < g_slow[0];
if(MyPositionType() != -1 || (!crossUp && !crossDown))
return;
// ... c_Trade.Buy / c_Trade.Sell with SL and TP in points
}
Real backtest: breakeven on vs off
We ran the same EA twice in the MetaTrader 5 Strategy Tester on EURUSD H1, 2024.01.01 to 2025.01.01, $10,000 deposit, 1-minute OHLC modelling. The only difference: in the second run the trigger was set out of reach, so breakeven never fired.
- Breakeven at +200 pts: net P/L -$286, win rate 58%, profit factor 0.77, max drawdown 2.9%, 99 trades
- No breakeven: net P/L -$163, win rate 31%, profit factor 0.91, max drawdown 3.4%, 89 trades
This is a useful result precisely because it is not flattering. Breakeven almost doubled the win rate and lowered drawdown, but it made the system lose more overall. Trades that would have run to the 600-point target were stopped out at +20 after a pullback, so many small wins replaced fewer big ones, and the freed-up slot let the EA take ten more trades. Neither version is profitable; the crossover itself has no edge on this data. The lesson: breakeven is a risk-shaping tool, not a profit generator. A trigger that is too close to entry (here 200 of a 600-point target) cuts your winners. Test a few trigger values, or tie it to ATR, before trusting it. Past performance does not guarantee future results.
Building this without writing the plumbing
The breakeven loop, the side-of-book pricing, stops-level checks and the on/off comparison above are the parts that take time to get right. Ranar Algo lets you describe the rule in plain English ("EMA 20/50 cross on EURUSD H1, 300-point stop, 600-point target, move the stop to entry +20 points once price is 200 points in profit"), then generates the MQL5, compiles it, and runs a real MT5 backtest so you see actual numbers like the comparison above. You get the source code and keep your IP. It is still improving, so if a generated EA misses something in your spec, feedback is genuinely welcome.
Summary
- Loop positions by ticket, filter by symbol and magic.
- Measure buys from the Bid and sells from the Ask.
- Only modify when the new stop is better than the current one and outside the stops level.
- Run it every tick, keep entries per bar.
- Backtest with and without it: a higher win rate is not the same as a better system.