A trailing stop follows price as a trade moves in your favour and never moves back. In MetaTrader 5 you can attach one by hand from the terminal, but that version runs inside your terminal and only for the trades you set it on. If an Expert Advisor manages the exit, the trailing logic belongs in its code, where it can use the strategy's own rules, such as an ATR distance, and where you can test it in the Strategy Tester.
This article builds a trailing-stop routine in MQL5 with break-even, fixed-distance and ATR trailing. It respects the broker's stop and freeze levels, rounds prices properly and touches only the EA's own positions. It's an example to adapt and test, not a recommendation.
Trailing stop styles
| Style | How the stop moves | Typical use |
|---|---|---|
| Break-even | Once, to the entry price (plus a few points) after a set profit | Removing the initial risk |
| Fixed distance | Follows price at a constant number of points | Simple, but ignores changing volatility |
| ATR-based | Follows price at a multiple of the Average True Range | Adapts to quiet and volatile conditions |
| Swing or channel | Trails behind recent lows (for longs) or highs (for shorts) | Structure-based exits for trend trades |
The ATR indicator page explains the volatility measure, and Supertrend is a ready-made ATR trailing line if you want to see the idea on a chart first.
Rules every trailing routine should follow
- Only move the stop in the trade's favour, never back.
- Move in steps. Modifying on every tiny tick floods the server with requests. A minimum improvement keeps it reasonable.
- Respect the stops level. Brokers set a minimum distance between price and a stop (
SYMBOL_TRADE_STOPS_LEVEL). Closer stops are rejected. - Respect the freeze level. When price is within
SYMBOL_TRADE_FREEZE_LEVELof the current stop or target, the broker refuses changes. - Round to the tick size, not just to the number of digits. On many indices and metals the tick is larger than one point.
- Touch only your own positions: filter by symbol and magic number.
- Leave the take-profit as it is unless you mean to change it.
The code
#include <Trade/Trade.mqh>
input int InpBreakEvenPoints = 200; // Profit (points) that moves the stop to break-even (0 = off)
input int InpBreakEvenLock = 10; // Points locked in beyond the entry at break-even
input int InpTrailStartPoints = 300; // Profit (points) before trailing starts
input int InpTrailDistPoints = 200; // Trailing distance (points) when ATR is off
input bool InpUseAtr = false; // Trail at a multiple of ATR instead
input int InpAtrPeriod = 14; // ATR period
input double InpAtrMult = 2.0; // ATR multiple
input int InpStepPoints = 20; // Minimum improvement before the stop is modified (points)
input ulong InpMagic = 261105; // Only positions with this magic number are managed
CTrade trade;
int atrHandle = INVALID_HANDLE;
int OnInit()
{
trade.SetExpertMagicNumber(InpMagic);
if(InpUseAtr)
{
atrHandle = iATR(_Symbol, PERIOD_CURRENT, InpAtrPeriod);
if(atrHandle == INVALID_HANDLE)
return INIT_FAILED;
}
return INIT_SUCCEEDED;
}
void OnDeinit(const int reason)
{
if(atrHandle != INVALID_HANDLE)
IndicatorRelease(atrHandle);
}
// Rounds a price to the symbol's tick size (often larger than one point on indices and metals).
double ToTick(const double price)
{
double tick = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
if(tick <= 0.0)
return NormalizeDouble(price, _Digits);
return NormalizeDouble(MathRound(price / tick) * tick, _Digits);
}
// Trailing distance in price: fixed points, or the last closed bar's ATR times a multiple.
double TrailDistance()
{
if(!InpUseAtr)
return InpTrailDistPoints * _Point;
double atr[1];
if(CopyBuffer(atrHandle, 0, 1, 1, atr) != 1)
return 0.0;
return atr[0] * InpAtrMult;
}
void ManageStops()
{
double dist = TrailDistance();
double minDist = (double)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL) * _Point;
double freeze = (double)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_FREEZE_LEVEL) * _Point;
double step = InpStepPoints * _Point;
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(ticket == 0)
continue;
if(PositionGetString(POSITION_SYMBOL) != _Symbol || PositionGetInteger(POSITION_MAGIC) != (long)InpMagic)
continue;
double open = PositionGetDouble(POSITION_PRICE_OPEN);
double sl = PositionGetDouble(POSITION_SL);
double tp = PositionGetDouble(POSITION_TP);
bool isBuy = PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY;
double price = isBuy ? bid : ask; // a buy closes at the bid, a sell at the ask
double profit = isBuy ? price - open : open - price;
double newSl = sl;
// 1) Break-even: lock a few points beyond the entry once the trade has moved far enough.
if(InpBreakEvenPoints > 0 && profit >= InpBreakEvenPoints * _Point)
{
double be = isBuy ? open + InpBreakEvenLock * _Point : open - InpBreakEvenLock * _Point;
if(newSl == 0.0 || (isBuy ? be > newSl : be < newSl))
newSl = be;
}
// 2) Trailing: follow price at a fixed or ATR distance once profit passes the start level.
if(dist > 0.0 && profit >= InpTrailStartPoints * _Point)
{
double gap = MathMax(dist, minDist + _Point);
double trail = isBuy ? price - gap : price + gap;
if(newSl == 0.0 || (isBuy ? trail > newSl : trail < newSl))
newSl = trail;
}
newSl = ToTick(newSl);
if(newSl == 0.0 || newSl == sl)
continue;
// Only forward, and only by at least one step.
if(sl != 0.0 && (isBuy ? newSl < sl + step : newSl > sl - step))
continue;
// The new stop must respect the minimum distance from price.
if(isBuy ? price - newSl <= minDist : newSl - price <= minDist)
continue;
// Inside the freeze level the broker refuses modifications.
if((sl != 0.0 && MathAbs(price - sl) <= freeze) || (tp != 0.0 && MathAbs(tp - price) <= freeze))
continue;
if(!trade.PositionModify(ticket, newSl, tp))
PrintFormat("Modify #%I64u failed: %u %s", ticket, trade.ResultRetcode(), trade.ResultRetcodeDescription());
}
}
void OnTick()
{
ManageStops();
// Your entry logic goes here.
}
How it works
Break-even first, trailing second. Each position gets a candidate stop from the break-even rule, then from the trailing rule, and keeps whichever is more favourable. With the defaults, the stop moves to entry + 10 points after 200 points of profit and starts trailing 200 points behind price after 300.
Bid and ask. A buy is closed at the bid and a sell at the ask, so profit and distances are measured from those prices. Measuring a sell's trailing stop from the bid would put it too close by the spread.
ATR from the last closed bar. CopyBuffer(atrHandle, 0, 1, 1, atr) reads the ATR of bar 1, the last completed bar, so the distance doesn't wobble with the forming bar. Note that MT5's built-in ATR uses a simple average of the true range, while TradingView's ta.atr() uses Wilder's smoothing, so distances can differ slightly from a Pine Script version of the same idea.
The checks before modifying. The routine skips the change when it wouldn't move the stop forward by at least one step, when the new stop would be inside the broker's minimum distance, or when price is inside the freeze level. Each of those would be rejected anyway; checking first keeps the journal clean.
Own positions only. The symbol and magic-number filter means manual trades and other EAs on the same account are left alone. MQL5 magic numbers explained covers why that matters.
Choosing the distances
The three numbers that shape a trailing stop are when it starts, how far it trails and how much it must improve before moving. A few guidelines:
- Trail outside the noise. A distance smaller than the typical bar range on your timeframe gets hit by ordinary fluctuations. That's the argument for ATR: 2 × ATR on an hourly chart adapts as volatility changes, where a fixed 200 points is wide one week and tight the next.
- Don't start trailing too early. If trailing starts at the same profit as the break-even move, many trades end as small winners that would have run. Starting later gives the trade room first.
- Test neighbouring values. If a trailing distance of 2.0 × ATR looks good and 1.8 or 2.2 look poor, the result is fragile. Choose settings from a stable region, not a single peak.
Every tick or once per bar?
| Run it | Pros | Cons |
|---|---|---|
| On every tick | Follows price closely; reacts to fast moves | More modification requests; noisier in backtests |
| Once per new bar | Fewer requests; matches bar-based strategies | Lags within the bar; a reversal inside the bar isn't caught by the trail |
| On a timer | Predictable load | Same lag as above, plus timing differences in the tester |
The step input is what keeps per-tick trailing reasonable. If your entries are evaluated once per bar, trailing once per bar is often the more consistent choice.
Testing it
- In the Strategy Tester, use Every tick based on real ticks where available, and watch a visual test: the stop line should only move in one direction.
- Read the journal. Retcode 10016 (invalid stops) means a distance check is missing or the stops level is larger than you thought; 10025 (no changes) means the routine sent a stop equal to the current one.
- Test on a symbol with a tick size larger than a point, such as an index CFD, to make sure rounding works.
- Forward-test on a demo account before going live.
A trailing stop is only one part of an EA's safety. Pair it with risk-based sizing (lot size from risk percent) and run through the MQL5 Expert Advisor risk checklist.
Sources and further reading
- MQL5 Reference: CTrade::PositionModify() — changing a position's stop loss and take profit.
- MQL5 Reference: Symbol properties —
SYMBOL_TRADE_STOPS_LEVELandSYMBOL_TRADE_FREEZE_LEVEL. - MQL5 Reference: iATR() and CopyBuffer() — reading ATR values in an Expert Advisor.
Try it with Edgoro
Tell Edgoro how your exits should work — "move the stop to break-even plus 5 points after 1R, then trail at 2 × ATR(14) of the last closed bar, only for this EA's trades" — and it writes the MQL5 for your Expert Advisor, ready for the Strategy Tester. Generate your EA.
Trailing stops don't guarantee a fill at the stop price; gaps and slippage happen. Test on a demo account first. Trading involves substantial risk of loss.