Edgoro
Platforms

MQL5 trailing stop: points, ATR and break-even code for MT5 Expert Advisors

Add a trailing stop to an MT5 Expert Advisor in MQL5: fixed-point and ATR trailing, break-even, broker stop and freeze levels, only the EA's own positions.

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

StyleHow the stop movesTypical use
Break-evenOnce, to the entry price (plus a few points) after a set profitRemoving the initial risk
Fixed distanceFollows price at a constant number of pointsSimple, but ignores changing volatility
ATR-basedFollows price at a multiple of the Average True RangeAdapts to quiet and volatile conditions
Swing or channelTrails 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

  1. Only move the stop in the trade's favour, never back.
  2. Move in steps. Modifying on every tiny tick floods the server with requests. A minimum improvement keeps it reasonable.
  3. Respect the stops level. Brokers set a minimum distance between price and a stop (SYMBOL_TRADE_STOPS_LEVEL). Closer stops are rejected.
  4. Respect the freeze level. When price is within SYMBOL_TRADE_FREEZE_LEVEL of the current stop or target, the broker refuses changes.
  5. Round to the tick size, not just to the number of digits. On many indices and metals the tick is larger than one point.
  6. Touch only your own positions: filter by symbol and magic number.
  7. 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 itProsCons
On every tickFollows price closely; reacts to fast movesMore modification requests; noisier in backtests
Once per new barFewer requests; matches bar-based strategiesLags within the bar; a reversal inside the bar isn't caught by the trail
On a timerPredictable loadSame 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

  1. 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.
  2. 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.
  3. Test on a symbol with a tick size larger than a point, such as an index CFD, to make sure rounding works.
  4. 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

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.

Build it with Edgoro

Describe your strategy in plain English and get a TradingView indicator in Pine Script v6 or a MetaTrader 5 Expert Advisor in minutes. Every first purchase is covered by a 7-day money-back guarantee.

Build your indicator with Edgoro