How to add an ATR-based trailing stop to any MQL5 EA

Published 2026-08-09 ยท Ranar Algo

An ATR-based trailing stop adapts to volatility instead of guessing a fixed number of points. The version below is a small include file you can drop into any MQL5 EA, and it respects the details that usually break trailing stops in live trading. It compiles as-is and needs no changes to your entry logic.

Why ATR beats a fixed point trail

A 200-point trail on EURUSD is generous in a quiet Asian session and suicidal during a news spike. ATR measures the average range over N bars, so the stop widens when the market breathes and tightens when it goes quiet. You keep the same relative distance from price regardless of regime.

The unit matters too. Multiplying ATR by a factor (2.0 to 3.0 is a common starting range) gives you one number to tune per symbol and timeframe, rather than a raw point value you have to re-fit every time the pair's volatility changes.

The design decisions, in order

1. Create the ATR handle once. Never call iATR() inside OnTick(). It allocates a new indicator handle on every tick, and you will leak handles until the terminal refuses to run your EA.

2. Read ATR with CopyBuffer(), not iATR(). The handle gives you a value only through the copy function.

3. Filter to your own positions. Check the symbol and the magic number. Without this, one EA trails another EA's trades, and you will spend an evening wondering why your stop keeps jumping.

4. Compute the candidate stop. For a buy: bid - multiplier * atr. For a sell: ask + multiplier * atr. Use bid for longs and ask for shorts so the spread works in your favour.

5. Respect the broker's minimum distance. SYMBOL_TRADE_STOPS_LEVEL gives the minimum gap in points between price and stop. Exchanges reject modifications that violate it, and some brokers *silently* reject them, which is worse.

6. Only ever move the stop one way. A trailing stop that can loosen is just a wider stop. Compare the candidate against the existing SL and bail if it is not an improvement.

7. Normalise before sending. Round to SYMBOL_DIGITS or you will get Invalid stops errors on five-digit quotes.

The complete class

Save this as AtrTrailing.mqh in MQL5/Include.

//+------------------------------------------------------------------+
//|                                                  AtrTrailing.mqh |
//|  Drop-in ATR trailing stop for any MQL5 expert advisor.          |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>

class CAtrTrailing
  {
private:
   int               m_handle;
   double            m_multiplier;
   ulong             m_magic;
   CTrade            m_trade;
   double            m_atr[];

   // Smallest gap this broker will accept between price and a stop.
   double            MinDistance(void) const
     {
      double point  = SymbolInfoDouble(_Symbol,SYMBOL_POINT);
      long   level  = SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL);
      double spread = (double)SymbolInfoInteger(_Symbol,SYMBOL_SPREAD)*point;
      return(MathMax((double)level*point,spread)+point);
     }

public:
                     CAtrTrailing(void) : m_handle(INVALID_HANDLE),
                                          m_multiplier(2.0), m_magic(0)
     {
      ArraySetAsSeries(m_atr,true);
     }

                    ~CAtrTrailing(void)
     {
      if(m_handle!=INVALID_HANDLE)
         IndicatorRelease(m_handle);
     }

   bool              Init(const string symbol,const ENUM_TIMEFRAMES tf,
                          const int atr_period,const double multiplier,
                          const ulong magic)
     {
      m_multiplier = multiplier;
      m_magic      = magic;
      m_trade.SetExpertMagicNumber(magic);
      m_handle     = iATR(symbol,tf,atr_period);
      return(m_handle!=INVALID_HANDLE);
     }

   void              Trail(void)
     {
      if(m_handle==INVALID_HANDLE)
         return;
      if(CopyBuffer(m_handle,0,0,2,m_atr)<2)
         return;

      double atr = m_atr[0];
      if(atr<=0.0)
         return;

      int    digits  = (int)SymbolInfoInteger(_Symbol,SYMBOL_DIGITS);
      double minDist = MinDistance();

      for(int i=PositionsTotal()-1; i>=0; i--)
        {
         ulong ticket = PositionGetTicket(i);
         if(ticket==0)
            continue;
         if(PositionGetString(POSITION_SYMBOL)!=_Symbol)
            continue;
         if((ulong)PositionGetInteger(POSITION_MAGIC)!=m_magic)
            continue;

         ENUM_POSITION_TYPE type =
            (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
         double open = PositionGetDouble(POSITION_PRICE_OPEN);
         double sl   = PositionGetDouble(POSITION_SL);
         double tp   = PositionGetDouble(POSITION_TP);
         double bid  = SymbolInfoDouble(_Symbol,SYMBOL_BID);
         double ask  = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
         double newSL= 0.0;

         if(type==POSITION_TYPE_BUY)
           {
            double candidate = NormalizeDouble(bid-m_multiplier*atr,digits);
            // Only trail once the stop can lock in at least breakeven.
            if(candidate>open && bid-candidate>=minDist)
               newSL = candidate;
            if(newSL==0.0 || (sl>0.0 && newSL<=sl))
               continue;
           }
         else if(type==POSITION_TYPE_SELL)
           {
            double candidate = NormalizeDouble(ask+m_multiplier*atr,digits);
            if(candidate<open && candidate-ask>=minDist)
               newSL = candidate;
            if(newSL==0.0 || (sl>0.0 && newSL>=sl))
               continue;
           }
         else
            continue;

         m_trade.PositionModify(ticket,newSL,tp);
        }
     }
  };

Wiring it into your EA

Declare one instance at global scope: CAtrTrailing g_trail;. In OnInit(), call g_trail.Init(_Symbol,PERIOD_H1,14,2.5,InpMagic) and return INIT_FAILED if it returns false. In OnTick(), call g_trail.Trail() after your entry logic.

The ATR timeframe is independent of the chart timeframe, which is useful. A breakout EA on M15 often trails better against H1 ATR, because the hourly range is a more stable measure than the fifteen-minute one.

What will bite you

The ATR value for the current bar changes on every tick, so the stop can move in small increments as the bar develops. That is usually harmless, but if your broker charges per modification or throttles requests, copy the ATR from a closed bar instead by shifting the CopyBuffer start index.

Freeze levels (SYMBOL_TRADE_FREEZE_LEVEL) can also block modifications when price is very close to the stop. The class handles the stops level but not the freeze level; if you see repeated modify failures on a specific broker, add that check the same way.

Finally, backtest with real ticks rather than OHLC bars. Trailing logic is entirely intrabar, and the "Open prices only" model will flatter it mercilessly. Trailing stops are not a magic fix either โ€” [see a real backtest](https://mql.ranartech.com/r/fNGF0hEO0pNW) of a EURUSD H4 EA that lost $1,800 across 85 trades with a 49% win rate. Good trailing logic protects a profitable edge. It does not create one.

Build & backtest your own EA free โ†’

Ranar Algo turns a plain-English strategy into a compiled MQL5 Expert Advisor with a real MetaTrader 5 backtest. You keep your IP. Past performance does not guarantee future results.