How to Code a Break-Even Stop Expert Advisor in MQL5
By HorizonAI Team · 12 min read · Intermediate
How to Code a Break-Even Stop Expert Advisor in MQL5
A trade reaches your planned profit threshold, then reverses before you can protect it. A break-even stop solves that mechanical problem: once price has moved a defined number of points in your favor, the EA moves the stop loss to the entry price, or slightly beyond it to lock in a small gain.
Short answer: An MQL5 break-even EA reads each open position's entry price, compares the current bid or ask with a trigger measured in points, and uses CTrade::PositionModify() to move the stop only when the new level is valid and more protective than the existing one. For buys, profit is measured from entry to bid; for sells, from entry to ask.
The practical version must also filter by symbol and magic number, use the instrument's actual _Point, preserve take profit, and reject a move that is inside the broker's stop or freeze distance. The EA below does exactly that. It manages positions only. It does not create entries or send trades by itself.
What this break-even EA does, and what it doesn't
A break-even stop is not a trailing stop. It makes one planned adjustment after a position reaches a threshold. A trailing stop keeps moving as the market moves further in your favor.
This EA has four jobs:
- Watch open positions on the chart symbol.
- Ignore positions that don't match its magic number, unless you explicitly enable manual-position management.
- Move the stop to entry plus or minus a configurable offset once the position reaches the trigger.
- Leave the position alone after the stop is already at a better level.
The offset matters. A zero offset puts the stop at the entry price. A positive lock-in offset moves a buy stop above entry and a sell stop below entry, which can account for a small amount of price movement but isn't a guarantee that every exit will be exactly profitable after costs.
CTrade::PositionModify() is the standard-library method used to modify a position's stop loss and take profit. Its documentation also notes that a successful method call should be followed by a check of the trade-server result code.
Set the inputs before you attach it
Use points, not pips, for the trigger and offset. On a five-digit EURUSD quote, 10 points equals one pip. On a two-decimal index CFD, the broker's point size can be different, so don't hard-code a pip conversion.
| Input | Practical default | Meaning |
|---|---|---|
InpBreakEvenTriggerPoints | 200 | Move the stop after 200 points of favorable movement. On five-digit EURUSD, that is 20 pips. |
InpLockInOffsetPoints | 10 | Put the new stop 10 points past entry. Set it to 0 for exact entry. |
InpMagicNumber | 20260921 | Only manage positions opened by this strategy ID. |
InpManageManualPositions | false | Set true only when you deliberately want the EA to manage manual trades on this symbol. |
Choose a trigger that gives the trade room to breathe. For example, on an intraday FX setup with an initial 300-point stop, a 200-point break-even trigger often fires much earlier than the original risk has been recovered. That may fit a quick mean-reversion setup, but it can cut a trend entry prematurely.
If you haven't assigned magic numbers consistently, fix that before adding management rules. The companion guide on setting a magic number in an MQL5 Expert Advisor explains how to keep multiple EAs from touching each other's positions.
Build the MQL5 break-even manager
Create a new Expert Advisor in MetaEditor, replace its generated contents with the code below, compile it, then attach it to the symbol you want to manage. It runs on every incoming tick for that chart symbol.
#property strict
#property version "1.00"
#property description "Moves qualifying positions to break-even without opening new trades."
#include <Trade/Trade.mqh>
input long InpMagicNumber = 20260921; // Magic number to manage
input bool InpManageManualPositions = false; // Also manage magic-number 0 positions
input int InpBreakEvenTriggerPoints = 200; // Favorable movement before break-even
input int InpLockInOffsetPoints = 10; // Points beyond entry; 0 = exact entry
CTrade trade;
// Return true only when this position belongs to the management scope.
bool IsManagedPosition(const ulong positionTicket)
{
if(!PositionSelectByTicket(positionTicket))
return false;
if(PositionGetString(POSITION_SYMBOL) != _Symbol)
return false;
const long positionMagic = PositionGetInteger(POSITION_MAGIC);
if(positionMagic == InpMagicNumber)
return true;
return (InpManageManualPositions && positionMagic == 0);
}
// Check whether the broker currently permits an SL at proposedStop.
bool IsStopDistanceValid(const ENUM_POSITION_TYPE positionType,
const double proposedStop,
const MqlTick &tick)
{
const double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
const long stopsLevelPoints = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
const long freezeLevelPoints = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_FREEZE_LEVEL);
const double minimumDistance = (double)MathMax(stopsLevelPoints, freezeLevelPoints) * point;
if(positionType == POSITION_TYPE_BUY)
return (proposedStop <= tick.bid - minimumDistance);
if(positionType == POSITION_TYPE_SELL)
return (proposedStop >= tick.ask + minimumDistance);
return false;
}
void MovePositionToBreakEven(const ulong positionTicket)
{
if(!PositionSelectByTicket(positionTicket))
return;
const ENUM_POSITION_TYPE positionType =
(ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
const double entryPrice = PositionGetDouble(POSITION_PRICE_OPEN);
const double currentStop = PositionGetDouble(POSITION_SL);
const double takeProfit = PositionGetDouble(POSITION_TP);
const int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
const double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
MqlTick tick;
if(!SymbolInfoTick(_Symbol, tick))
{
PrintFormat("Break-even: no current tick for %s", _Symbol);
return;
}
const double triggerDistance = InpBreakEvenTriggerPoints * point;
const double offsetDistance = InpLockInOffsetPoints * point;
double proposedStop = 0.0;
bool triggerReached = false;
if(positionType == POSITION_TYPE_BUY)
{
triggerReached = (tick.bid - entryPrice >= triggerDistance);
proposedStop = NormalizeDouble(entryPrice + offsetDistance, digits);
// Never lower an existing buy stop.
if(currentStop > 0.0 && proposedStop <= currentStop + point * 0.5)
return;
}
else if(positionType == POSITION_TYPE_SELL)
{
triggerReached = (entryPrice - tick.ask >= triggerDistance);
proposedStop = NormalizeDouble(entryPrice - offsetDistance, digits);
// Never raise an existing sell stop.
if(currentStop > 0.0 && proposedStop >= currentStop - point * 0.5)
return;
}
else
{
return;
}
if(!triggerReached)
return;
if(!IsStopDistanceValid(positionType, proposedStop, tick))
{
PrintFormat("Break-even: ticket %I64u skipped. Proposed SL %.5f is inside stop/freeze distance.",
positionTicket, proposedStop);
return;
}
ResetLastError();
if(!trade.PositionModify(positionTicket, proposedStop, takeProfit))
{
PrintFormat("Break-even: PositionModify failed for ticket %I64u. Error=%d, retcode=%u (%s)",
positionTicket,
GetLastError(),
trade.ResultRetcode(),
trade.ResultRetcodeDescription());
return;
}
PrintFormat("Break-even: ticket %I64u moved to SL %.5f. Retcode=%u (%s)",
positionTicket,
proposedStop,
trade.ResultRetcode(),
trade.ResultRetcodeDescription());
}
int OnInit()
{
if(InpBreakEvenTriggerPoints <= 0 || InpLockInOffsetPoints < 0)
{
Print("Break-even: trigger must be positive and offset cannot be negative.");
return INIT_PARAMETERS_INCORRECT;
}
trade.SetAsyncMode(false);
return INIT_SUCCEEDED;
}
void OnTick()
{
for(int index = PositionsTotal() - 1; index >= 0; index--)
{
const ulong positionTicket = PositionGetTicket(index);
if(positionTicket == 0 || !IsManagedPosition(positionTicket))
continue;
MovePositionToBreakEven(positionTicket);
}
}
The code reads position fields only after selecting the position by ticket. That sequence matters because the PositionGetDouble() documentation states that position data should be selected first for fresh values.
Why buy and sell break-even math differs
A long position closes at the bid, so its favorable movement is bid - entryPrice. Its proposed break-even stop is entryPrice + offset.
A short position closes at the ask, so its favorable movement is entryPrice - ask. Its proposed stop is entryPrice - offset.
That bid/ask distinction is why a long and short trade at the same chart price can have different break-even status. It also prevents a common error: using bid for both directions, then wondering why sell stops move too early or never qualify.
The EA preserves the existing take-profit value by passing takeProfit back into PositionModify. It doesn't alter your target while it adjusts the stop.
Tip: Start with
InpLockInOffsetPoints = 0while validating the logic. Once the log confirms the stop moves at the right moment, decide whether a small positive offset makes sense for that instrument's spread and tick size.
Respect stop levels and freeze levels
A valid break-even price can still be rejected by the trade server. Brokers publish a minimum stop distance, SYMBOL_TRADE_STOPS_LEVEL, and can also impose a freeze level around the current market price. The EA uses the larger of the two as a conservative distance check before it submits the modification.
For a buy, the proposed stop must be at or below bid - minimumDistance. For a sell, it must be at or above ask + minimumDistance. If the level isn't valid yet, the EA prints a skip message and tries again on a later tick rather than sending a request it already knows is too close.
MQL5 exposes both SYMBOL_TRADE_STOPS_LEVEL and SYMBOL_TRADE_FREEZE_LEVEL through symbol properties.
This check is deliberately conservative. Some symbols have a zero reported stop level in normal conditions but a larger practical distance around volatile news or rollover. If server logs still show a rejection, read the retcode printed by the EA before changing the trading logic.
Test the exact cases that usually break this feature
Don't judge a stop-management EA from one visual chart run. Use the MT5 Strategy Tester and force a few specific conditions.
| Test case | Setup | Expected result |
|---|---|---|
| Buy reaches trigger | Entry at 1.10000, trigger 200 points, bid reaches 1.10200 | SL moves to 1.10010 with a 10-point offset, subject to distance rules. |
| Sell reaches trigger | Entry at 1.10000, trigger 200 points, ask falls to 1.09800 | SL moves to 1.09990 with a 10-point offset. |
| Existing better stop | A buy already has SL above the calculated break-even level | No modification is sent. |
| Wrong magic number | Open a position with another EA's magic number | Position is ignored. |
| Manual trade | Magic number is 0 and manual management is false | Position is ignored. |
| Broker-distance conflict | Trigger occurs while proposed SL is too near current price | A log message appears and no modification is sent. |
In Strategy Tester, use a visual pass first so you can inspect the entry, current price, and resulting stop. Then run the same scenario without visualization and compare the Journal log. MetaTrader's testing documentation covers the test settings and reporting workflow.
For a more complete validation process, use the checks in how to backtest a trading strategy, especially realistic spreads and a sample that includes different volatility regimes. A stop-management rule can look clean in a narrow date range and behave very differently when spread widens.
Add this to an entry EA without mixing responsibilities
This standalone EA is best for testing the stop-management feature in isolation. It doesn't know why the position was opened, and it doesn't open a replacement after a break-even exit.
When you add it to an entry EA, keep MovePositionToBreakEven() and its helpers unchanged, then call the management loop near the top of that EA's OnTick(). Keep entry logic separate below it. That makes it easier to diagnose whether a bad result came from the entry condition or the stop adjustment.
If an existing EA isn't opening trades or is failing during modification, don't guess. Work through the Journal, AutoTrading settings, symbol permissions, and server return codes with this MT5 EA debugging checklist. A modification error and an entry error can share broker restrictions, but they aren't the same defect.
On hedging accounts, multiple positions can exist for the same symbol. This version loops through tickets and uses the ticket overload of PositionModify, so it can make a decision for each individual position. On netting accounts, there is normally one aggregate position per symbol, but the same ticket-based workflow still works.
Common break-even EA mistakes
❌ Mistake: Treating points as pips. Setting a 20-point trigger on a five-digit EURUSD symbol means 2 pips, not 20. The stop may move almost immediately.
✅ Do this: Read _Point through SYMBOL_POINT, enter the configuration in points, and annotate your presets. A 20-pip FX trigger is usually 200 points on a five-digit quote.
❌ Mistake: Moving a stop backward. A repeated PositionModify() call can worsen a stop if the code doesn't compare the proposed level with the existing SL.
✅ Do this: For buys, modify only when the candidate SL is higher than the current SL. For sells, modify only when it is lower. The code uses a half-point tolerance to avoid pointless repeat requests after rounding.
❌ Mistake: Managing every chart position by accident. A magic-number filter that accepts zero by default can interfere with manual trades or another system.
✅ Do this: Keep InpManageManualPositions = false until you have deliberately tested it on a demo account. Give each entry system its own nonzero magic number.
❌ Mistake: Ignoring the trade-server result. A true return from the method isn't the whole story if the server rejects or adjusts the request.
✅ Do this: Log trade.ResultRetcode() and trade.ResultRetcodeDescription() exactly as the example does. Those messages give you a concrete starting point instead of a vague "break-even doesn't work" report.
Pro tips for a less fragile break-even rule
Make the trigger proportional to the initial stop. A fixed 200-point trigger behaves differently across EURUSD, gold, and an index. If your entry system has a 1R initial stop, a break-even trigger around 0.75R, 1R, or 1.25R is easier to reason about than a copied number.
Use the offset as a rule, not a hope. A 10-point offset is simply a requested stop location. It doesn't guarantee a 10-point realized gain if price gaps through the level or execution occurs at a different available price.
Record the first modification only. The existing-stop guard makes that happen operationally, but logs make it auditable. When reviewing a test, you should be able to see one "moved to SL" line per qualifying position, not dozens.
Test symbols separately. A configuration that works on a liquid major may collide with stop-distance rules on metals, CFDs, or symbols with wider spreads. The risk management guide can help you relate the break-even trigger to position risk rather than treating it as a universal setting.
Generating this without writing the code yourself
You can build the same management module in HorizonAI by describing the behavior and constraints precisely. Ask for MQL5, specify that it must manage stops only, and include the symbol, magic-number, bid/ask, and broker-distance requirements.
"Write a compile-checked MQL5 Expert Advisor that never opens trades. On every tick, loop through open positions for the chart symbol. Manage only magic number 20260921, plus manual magic-number-0 positions only if a boolean input is true. When a buy moves 200 points from entry using bid, move SL to entry plus 10 points. When a sell moves 200 points from entry using ask, move SL to entry minus 10 points. Preserve TP, never worsen an existing SL, check SYMBOL_TRADE_STOPS_LEVEL and SYMBOL_TRADE_FREEZE_LEVEL, and log CTrade result retcodes. Use ticket-based PositionModify for hedging accounts."
HorizonAI can generate MQL5 code from that prompt, compile-check it, and let you revise the logic in chat or its code editor. It writes the EA code; you choose where to run it in MetaTrader 5. Try it free →
FAQs
Is break-even the same as a trailing stop?
No. A break-even rule makes a one-time stop move after a fixed profit threshold. A trailing stop continues moving as price makes further favorable progress.
Why does my break-even stop fail to move on a sell trade?
Sell profit must be measured with the ask price, not the bid. The requested stop can also be rejected or skipped when it is inside the symbol's current stop-distance or freeze-distance requirement.
Can this EA manage manual MT5 trades?
Yes, if you set InpManageManualPositions to true. Test that setting carefully because manual positions normally have magic number 0 and the EA will manage qualifying positions on its chart symbol.
Does moving the stop to entry guarantee no loss?
No. Spread, commissions, swaps, and execution differences can mean an exit around entry isn't exactly flat. A positive offset requests a more protective level, but it still doesn't guarantee the fill price.
Should I use break-even at 1R?
It is a reasonable test case, not a universal rule. Compare 0.75R, 1R, and 1.25R across the same entry logic and market conditions, then inspect expectancy, drawdown, and how often the move cuts trades that later continue.
Final thoughts
A reliable break-even EA is a small piece of code with strict requirements: correct bid/ask math, point-based parameters, a position filter, one-way stop movement, broker-distance checks, and server-result logging. Build and test those pieces before connecting the function to an entry model.
One useful final test: set the trigger just beyond the broker's stop distance, then verify in the Journal that a buy and a sell each modify once. That simple test catches most direction, point-size, and repeated-modification errors.
Related articles
- How to Code a Supertrend Indicator and an EA in MQL5 — Build an MQL5 strategy structure and compare its entry logic with this exit-management module.
- MT5 EA Not Opening Trades: MQL5 Debugging Checklist — Diagnose permissions, Journal errors, and common EA failures.
- How to Set a Magic Number in an MQL5 Expert Advisor — Keep multiple EAs from managing the same positions.
- How to Calculate Lot Size by Risk Percentage in MQL5 — Calculate position size before break-even management starts.
- How to Build an MQL5 News Filter EA With the Economic Calendar — Add a calendar-aware condition to an MT5 system.
- How to Convert Pine Script to MQL5: A Manual Translation Guide — Translate strategy logic from TradingView into MQL5 structure.
- How to Automate Your Trading Strategy on MetaTrader 5 — Understand the workflow for building and running MT5 code.
- How to Backtest Trading Strategies Like a Pro — Avoid data, execution, and sample-selection mistakes in validation.
Questions about MQL5 break-even stops? Join our Discord to discuss with other traders!
