ATR Dynamic Stop Loss EA MQL5: Volatility-Based Risk

Build an MQL5 EA that adjusts stop loss and take profit in real time using ATR. Full code, backtest results, and practical settings for volatility-based risk.

atr-dynamic-stop-loss-ea-mql5-volatility-based

Why Most Fixed Stop Losses Fail

You've probably seen it happen: you place a stop loss at 30 pips on EURUSD, the market whipsaws through it in a news spike, and price immediately reverses to your target. Or you set a generous 100-pip stop, only to watch a quiet Friday session grind through it slowly. The problem isn't your entry—it's that you're using a fixed distance in a market where volatility changes constantly.

I've been guilty of this myself. Early in my trading career, I'd set a 50-pip stop on every trade regardless of what the market was doing. Some days that was too tight, other days too loose. The result? A string of losses that had nothing to do with my analysis and everything to do with ignoring market conditions.

Average True Range (ATR) has been around since Welles Wilder introduced it in the 1970s, but most traders still use it as a static indicator on a chart. The real power comes when you let it drive your risk management in real time. In this post, I'll walk you through building an MQL5 Expert Advisor that reads current ATR and adjusts stop loss and take profit dynamically with every tick. No fluff, no magic settings—just a practical tool you can adapt to your own strategy.

Let me be clear upfront: this isn't a holy grail. Dynamic stops won't turn a losing strategy into a winner. What they will do is align your risk with actual market conditions, reducing the randomness of being stopped out at the wrong time. That alone can make the difference between a system that survives drawdowns and one that blows up.

What ATR Actually Tells You

ATR measures market volatility as an average of true range over a given period. True range is the greatest of: current high minus current low, absolute value of current high minus previous close, or absolute value of current low minus previous close. The indicator smooths this with a moving average—typically 14 periods.

Here's the key insight: when ATR is rising, you need wider stops to avoid getting shaken out by normal price noise. When ATR is falling, tighter stops protect profits without getting stopped out by random fluctuations. A fixed 50-pip stop might be reasonable when ATR is 40 pips, but suicidal when ATR spikes to 120 pips during a news event.

I prefer using a multiplier rather than a fixed ATR value. For example, stop loss = ATR × 1.5 and take profit = ATR × 3.0. This scales your risk-reward ratio to current conditions automatically. A 1:2 ratio stays consistent whether volatility is low or high.

One nuance most traders miss: ATR doesn't tell you direction. It's purely a volatility gauge. A rising ATR could mean a breakout in your favor or against you. The stop loss adjustment is defensive—it protects you from the volatility, not from being wrong on direction. Always combine ATR-based stops with a solid entry logic.

Building the ATR Dynamic Stop Loss EA in MQL5

Let's get into the code. This EA will:

  • Open a position when a simple moving average crossover occurs (you can swap this for your own entry logic)
  • Calculate ATR on every tick after the position is open
  • Adjust stop loss and take profit as ATR changes
  • Optionally trail the stop loss as price moves favorably

First, the input parameters. These are the knobs you'll tune in the Strategy Tester. I've chosen sensible defaults based on my own testing, but you'll want to optimize for your specific pair and timeframe.

ParameterTypeDefaultDescription
ATR_Periodint14Number of periods for ATR calculation
ATR_SL_Multiplierdouble1.5Multiplier for stop loss distance
ATR_TP_Multiplierdouble3.0Multiplier for take profit distance
Trailing_ATR_Offsetdouble1.0ATR multiplier for trailing stop activation
MA_Fastint10Fast moving average period for entry signal
MA_Slowint30Slow moving average period for entry signal
LotSizedouble0.1Fixed lot size for each trade
MagicNumberint123456Unique identifier to manage only this EA's positions

Setting Up the Global Variables and OnInit

Before we dive into the core logic, we need to set up the EA's initialization. This is where you define global variables, load indicator handles, and set the magic number. In MQL5, you must create indicator handles using iATR() and store them for later use in OnTick(). Here's how I structure it:

//+------------------------------------------------------------------+
//| Global variables                                                 |
//+------------------------------------------------------------------+
int atr_handle;
int ma_fast_handle;
int ma_slow_handle;
int MagicNumber = 123456;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   // Create ATR indicator handle
   atr_handle = iATR(_Symbol, _Period, ATR_Period);
   if(atr_handle == INVALID_HANDLE)
   {
      Print("Failed to create ATR handle. Error: ", GetLastError());
      return(INIT_FAILED);
   }
   
   // Create MA handles for entry signals
   ma_fast_handle = iMA(_Symbol, _Period, MA_Fast, 0, MODE_SMA, PRICE_CLOSE);
   ma_slow_handle = iMA(_Symbol, _Period, MA_Slow, 0, MODE_SMA, PRICE_CLOSE);
   if(ma_fast_handle == INVALID_HANDLE || ma_slow_handle == INVALID_HANDLE)
   {
      Print("Failed to create MA handles. Error: ", GetLastError());
      return(INIT_FAILED);
   }
   
   Print("EA initialized successfully on ", _Symbol, " ", EnumToString(_Period));
   return(INIT_SUCCEEDED);
}

Notice I'm using iATR() with the symbol and period from the current chart. You could hardcode these, but I prefer letting the EA work on whatever chart it's attached to. That way you can test on multiple pairs without recompiling.

ATR Calculation and Dynamic Stop/Target

The CalculateDynamicLevels() function runs on every tick after a position is open. It fetches the current ATR value and computes the stop and target distances in points. This is the heart of the EA—get this wrong and your stops will be all over the place.

//+------------------------------------------------------------------+
//| Calculate dynamic SL and TP levels based on current ATR          |
//+------------------------------------------------------------------+
void CalculateDynamicLevels(double &sl_price, double &tp_price, int type)
{
   double atr_array[];
   ArraySetAsSeries(atr_array, true);
   int copied = CopyBuffer(atr_handle, 0, 0, 1, atr_array);
   if(copied <= 0)
   {
      Print("Failed to copy ATR buffer. Error: ", GetLastError());
      return;
   }
   
   double atr_value = atr_array[0];
   if(atr_value <= 0)
   {
      Print("ATR value is zero or negative, using last known value");
      return;
   }
   
   double sl_points = atr_value * ATR_SL_Multiplier / _Point;
   double tp_points = atr_value * ATR_TP_Multiplier / _Point;
   
   if(type == POSITION_TYPE_BUY)
   {
      sl_price = SymbolInfoDouble(_Symbol, SYMBOL_BID) - sl_points * _Point;
      tp_price = SymbolInfoDouble(_Symbol, SYMBOL_BID) + tp_points * _Point;
   }
   else if(type == POSITION_TYPE_SELL)
   {
      sl_price = SymbolInfoDouble(_Symbol, SYMBOL_ASK) + sl_points * _Point;
      tp_price = SymbolInfoDouble(_Symbol, SYMBOL_ASK) - tp_points * _Point;
   }
   
   // Round to allowed tick size
   double tick_size = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
   sl_price = NormalizeDouble(MathRound(sl_price / tick_size) * tick_size, _Digits);
   tp_price = NormalizeDouble(MathRound(tp_price / tick_size) * tick_size, _Digits);
}

Notice the rounding to tick size. This is a common gotcha—if you pass prices that aren't aligned with the symbol's tick size, the trade server will reject the modification. I've seen EAs fail silently because of this. For example, on EURUSD with a tick size of 0.00001, a price of 1.123456 would round to 1.12346. On USDJPY with a tick size of 0.001, it would round to 123.456. Always check your symbol's tick size in the Market Watch window before deploying.

Modifying Orders on Every Tick

The OnTick() handler checks for new signals and then updates existing positions. Here's the modification part. I've added a threshold to avoid spamming the trade server—something that's especially important on volatile pairs like GBPJPY where ATR can jump 20-30 points in minutes.

//+------------------------------------------------------------------+
//| Expert tick function

Want to build your own version?

Recreate similar entry logic, risk rules, and filters in TradingBotMaker—no MQL coding. Start free.

Community

Clap for the article and open comments only when you want to read them.

0 claps0 comments

Related articles