Advanced Forex EA Analytics

Average True Range: Measuring Market Volatility

Learn how to use ATR to measure market volatility, set optimal stop-losses, and make more informed trading decisions across all financial markets.

Volatility Measurement

Quantify true market movement range

Stop-Loss Sizing

Set dynamic stops based on market conditions

EA Optimisation

Adapt EA parameters to live volatility

Core Formula

ATR = SMA(True Range, N)

Where N is the lookback period (default: 14 periods)

Start Learning
14 min read
Beginner to Intermediate
11,200+ learners

What Is Average True Range (ATR)?

Average True Range (ATR) is a technical indicator developed by J. Welles Wilder in 1978, introduced in his book New Concepts in Technical Trading Systems. Unlike most volatility indicators, ATR does not measure price direction — it measures the degree of price movement over a given period. For Forex EA traders, ATR is an essential tool for calibrating stop-losses, position sizing, and detecting changes in market conditions that may affect EA performance.

How ATR Is Calculated

ATR is derived from the concept of True Range (TR) — the largest of three values for each period:

True Range Calculation:

1 TR₁ = Current High − Current Low
2 TR₂ = |Current High − Previous Close|
3 TR₃ = |Current Low − Previous Close|
True Range = MAX(TR₁, TR₂, TR₃)

TR₂ and TR₃ account for overnight gaps and sessions where price opens significantly away from the previous close — crucial for forex markets that trade across multiple sessions.

ATR Formula (14-period default):

ATR(14) = EMA(True Range, 14)

Wilder used a smoothed moving average (equivalent to an EMA with α = 1/N). The 14-period setting remains the industry standard, though EA developers often tune this to their strategy's timeframe.

5 Key Uses of ATR in Forex EA Development

1. Dynamic Stop-Loss Placement

ATR-based stop-losses adapt to current market volatility rather than using a fixed pip value. A common approach is to place a stop at 1.5× to 2× ATR from the entry price, ensuring the stop is wide enough to avoid noise but tight enough to limit risk.

Example: If EURUSD has a 14-period ATR of 0.0080 (80 pips) on the H4 chart, an EA using 1.5× ATR would place a stop-loss 120 pips from entry — automatically widening during high-volatility news events and tightening in calm sessions.

2. Volatility-Adjusted Position Sizing

Rather than trading a fixed lot size, ATR allows EAs to scale position size inversely with volatility. When ATR is high (volatile market), position size decreases; when ATR is low (calm market), position size increases. This keeps risk per trade consistent in monetary terms.

Formula: Lot Size = (Account Risk %) ÷ (ATR × Pip Value)

3. Detecting Breakout Conditions

A sharp increase in ATR signals expanding volatility, often associated with a genuine breakout rather than a false move. EAs can use an ATR threshold — such as ATR exceeding its 20-period average — as a filter to only enter trades during confirmed breakouts.

4. Filtering Low-Volatility Environments

Many trend-following EAs struggle during consolidating, low-volatility markets. By adding a minimum ATR filter (e.g., only trade when ATR > 30 pips on H1), EAs can avoid choppy conditions that produce excessive false signals and unnecessary drawdown.

5. Take-Profit Targeting

ATR can define realistic profit targets based on what the market is actually capable of moving in a given period. Setting a take-profit at 2× to 3× ATR aligns targets with realistic daily or session range expectations, improving the probability of target completion.

ATR Across Different Timeframes

ATR values are relative to the timeframe being analysed. A 14-period ATR on a daily chart measures daily volatility, while ATR on an M15 chart measures intraday volatility. When building EAs, always ensure stop-loss and take-profit calculations reference the ATR from the same timeframe as the trading signal.

Timeframe Typical EURUSD ATR Common EA Use Case
M15 5–15 pips Scalping stop-loss calibration
H1 15–35 pips Intraday breakout filtering
H4 40–90 pips Swing trade stop & target sizing
D1 70–120 pips Daily range targeting & position sizing

Interpreting ATR Values

ATR vs. Historical Average Market Condition EA Implication
ATR significantly below average Low volatility / consolidation Reduce position size or pause EA
ATR near its average Normal conditions Standard EA operation
ATR 1.5× above average Elevated volatility Widen stops; reduce lot size
ATR 2× or more above average Extreme volatility (news event) Avoid new entries; tighten risk

Implementing ATR in MQL5

Here is an MQL5 example demonstrating how to retrieve ATR values and use them to set a dynamic stop-loss and take-profit on each trade:

MQL5 Example: ATR-Based Stop-Loss & Take-Profit
// ATR indicator handle
int atrHandle;
double atrBuffer[];
 
int OnInit()
{
   // Create ATR indicator: 14-period on the current symbol/timeframe
   atrHandle = iATR(_Symbol, PERIOD_CURRENT, 14);
   
   if(atrHandle == INVALID_HANDLE)
   {
      Print("Error creating ATR handle: ", GetLastError());
      return INIT_FAILED;
   }
   
   ArraySetAsSeries(atrBuffer, true);
   return INIT_SUCCEEDED;
}
 
double GetATR()
{
   // Copy the last 3 ATR values into the buffer
   if(CopyBuffer(atrHandle, 0, 0, 3, atrBuffer) < 0)
   {
      Print("Error copying ATR buffer: ", GetLastError());
      return 0.0;
   }
   // Return the most recently completed candle's ATR (index 1)
   return atrBuffer[1];
}
 
void PlaceATROrder(ENUM_ORDER_TYPE orderType)
{
   double atrValue    = GetATR();
   double entryPrice  = (orderType == ORDER_TYPE_BUY) 
                           ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) 
                           : SymbolInfoDouble(_Symbol, SYMBOL_BID);
   
   // Stop-loss: 1.5× ATR from entry
   double stopLoss = (orderType == ORDER_TYPE_BUY)
                        ? entryPrice - (1.5 * atrValue)
                        : entryPrice + (1.5 * atrValue);
   
   // Take-profit: 2.5× ATR from entry
   double takeProfit = (orderType == ORDER_TYPE_BUY)
                          ? entryPrice + (2.5 * atrValue)
                          : entryPrice - (2.5 * atrValue);
   
   MqlTradeRequest request = {};
   MqlTradeResult  result  = {};
   
   request.action    = TRADE_ACTION_DEAL;
   request.symbol    = _Symbol;
   request.volume    = 0.1;
   request.type      = orderType;
   request.price     = entryPrice;
   request.sl        = NormalizeDouble(stopLoss,  _Digits);
   request.tp        = NormalizeDouble(takeProfit, _Digits);
   request.comment   = "ATR SL/TP";
   
   if(!OrderSend(request, result))
      Print("OrderSend failed: ", result.retcode);
   else
      Print("Order placed | ATR: ", atrValue, 
            " | SL: ", stopLoss, " | TP: ", takeProfit);
}
 
void OnDeinit(const int reason)
{
   IndicatorRelease(atrHandle);
}

ATR vs. Fixed Pip Stop-Losses: A Comparison

Many beginner EA developers default to fixed pip stop-losses (e.g., always 50 pips). While simple, this approach ignores the reality that markets breathe differently depending on volatility. Here's how ATR-based stops compare:

❌ Fixed Pip Stop-Loss

  • • Too tight during high volatility → stopped out prematurely
  • • Too wide during low volatility → excessive risk per trade
  • • Does not adapt to different pairs or sessions
  • • Requires constant manual re-tuning after market regime changes

✅ ATR-Based Stop-Loss

  • • Automatically widens during news/volatility spikes
  • • Tightens during quiet sessions, reducing unnecessary risk
  • • Works across different pairs without re-optimisation
  • • Produces more consistent risk-adjusted returns over time

ATR & Position Sizing: The Complete Formula

One of ATR's most powerful applications is volatility-normalised position sizing. The goal is simple: risk the same monetary amount on every trade regardless of how volatile the market is. Instead of manually adjusting lot sizes before each trade, an EA can calculate the correct position size automatically using ATR.

Position Sizing Formula:

Lot Size = (Account Balance × Risk %) ÷ (ATR × ATR Multiplier × Pip Value)

Variables:

  • Account Balance — your current equity in account currency
  • Risk % — percentage of equity risked per trade (e.g., 1%)
  • ATR — current ATR value in price units

 

  • ATR Multiplier — stop distance expressed as ATR multiples (e.g., 1.5)
  • Pip Value — monetary value of 1 pip per lot (pair-dependent)

The following table shows how position size changes automatically as ATR fluctuates, keeping monetary risk fixed at 1% of a $10,000 account on EURUSD (pip value ≈ $10/lot), with a 1.5× ATR stop:

ATR (pips) Stop Distance (1.5× ATR) $ Risk (1%) Calculated Lot Size Market Condition
30 pips 45 pips $100 0.22 lots Low volatility
60 pips 90 pips $100 0.11 lots Normal volatility
100 pips 150 pips $100 0.07 lots Elevated volatility
160 pips 240 pips $100 0.04 lots High volatility / news

Key insight: When ATR doubles, your lot size halves. The monetary risk stays fixed at $100 regardless of market conditions — the EA adapts automatically without any manual intervention.

Here is a complete MQL5 function that implements ATR-based position sizing. It accepts a risk percentage and ATR multiplier and returns a normalised lot size ready for use in an order request:

MQL5 Example: ATR Position Sizing Function
// ─── Inputs ───────────────────────────────────────────────────────────────────
input double InpRiskPercent   = 1.0;   // Risk per trade as % of account balance
input double InpATRMultiplier = 1.5;   // Stop distance in ATR multiples
input int    InpATRPeriod     = 14;    // ATR lookback period
 
// ─── ATR handle (initialised in OnInit) ───────────────────────────────────────
int    g_atrHandle;
double g_atrBuffer[];
 
int OnInit()
{
   g_atrHandle = iATR(_Symbol, PERIOD_CURRENT, InpATRPeriod);
   if(g_atrHandle == INVALID_HANDLE) return INIT_FAILED;
   ArraySetAsSeries(g_atrBuffer, true);
   return INIT_SUCCEEDED;
}
 
// ─── Get current ATR value ─────────────────────────────────────────────────────
double GetCurrentATR()
{
   if(CopyBuffer(g_atrHandle, 0, 1, 1, g_atrBuffer) < 1) return 0.0;
   return g_atrBuffer[0];
}
 
// ─── Calculate lot size from ATR + risk parameters ────────────────────────────
double CalculateLotSize(double atrValue)
{
   if(atrValue <= 0.0) return 0.0;
 
   double accountBalance = AccountInfoDouble(ACCOUNT_BALANCE);
   double riskAmount     = accountBalance * (InpRiskPercent / 100.0);
   double stopPips       = (atrValue / _Point) * InpATRMultiplier;
 
   // Pip value per lot in account currency
   double pipValuePerLot = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE)
                           / SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE)
                           * _Point;
 
   if(pipValuePerLot <= 0.0) return 0.0;
 
   double rawLots = riskAmount / (stopPips * pipValuePerLot);
 
   // Clamp to broker lot constraints
   double minLot  = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
   double maxLot  = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
   double lotStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
   double lots    = MathFloor(rawLots / lotStep) * lotStep;
 
   lots = MathMax(minLot, MathMin(maxLot, lots));
 
   Print("ATR: ", atrValue,
         " | Stop: ", stopPips, " pips",
         " | Risk $: ", riskAmount,
         " | Lots: ", lots);
 
   return lots;
}
 
// ─── Example usage in OnTick / signal logic ───────────────────────────────────
void OnTick()
{
   double atr  = GetCurrentATR();
   double lots = CalculateLotSize(atr);
   
   if(lots <= 0.0) return;
   
   // Use lots in your MqlTradeRequest as normal
   // request.volume = lots;
}

ATR in Different Market Regimes: Trending vs. Ranging

ATR does not distinguish between trending and ranging markets — it only measures the size of moves, not their direction. However, understanding how ATR behaves across regimes allows EA developers to tailor their logic appropriately for each environment.

Characteristic Trending Market Ranging Market
Typical ATR behaviour Rising or elevated ATR as momentum builds Low and flat ATR; contracting ranges
Optimal stop strategy Wider stops (2× ATR) to ride the move Tighter stops (1× ATR) near range extremes
Take-profit approach Trail using ATR; let winners run Fixed TP at opposite range boundary
ATR filter to use ATR > 20-period ATR average (expanding) ATR < 20-period ATR average (compressing)
EA types that perform well Trend-following, breakout EAs Mean-reversion, grid, range EAs
Risk Sudden reversal wipes open profit False breakout triggers erroneous entry

A practical approach is to compare the current ATR to its own moving average. When ATR is rising relative to its average, the market is transitioning from ranging to trending — a signal to loosen stops and increase targets. When ATR is falling below its average, conditions are compressing — tighten targets and reduce exposure.

MQL5 Example: Regime Detection Using ATR vs. Its Moving Average
// ATR handles
int g_atrHandle;
int g_atrMaHandle;   // MA applied to ATR values
double g_atr[];
double g_atrMa[];
 
int OnInit()
{
   // 14-period ATR
   g_atrHandle = iATR(_Symbol, PERIOD_CURRENT, 14);
   
   // 20-period SMA of ATR — requires a custom indicator approach.
   // A common alternative: calculate ATR manually over two windows.
   // Here we use iMA on the CLOSE with the same logic as a proxy.
   // For production, apply iCustom or compute ATR SMA inline (see below).
   
   if(g_atrHandle == INVALID_HANDLE) return INIT_FAILED;
   ArraySetAsSeries(g_atr, true);
   return INIT_SUCCEEDED;
}
 
enum ENUM_MARKET_REGIME { REGIME_TRENDING, REGIME_RANGING, REGIME_UNKNOWN };
 
ENUM_MARKET_REGIME DetectRegime()
{
   // Copy 21 ATR values to compute a 20-bar simple average inline
   double atrValues[];
   ArraySetAsSeries(atrValues, true);
   
   if(CopyBuffer(g_atrHandle, 0, 1, 21, atrValues) < 21)
      return REGIME_UNKNOWN;
   
   double currentATR = atrValues[0];
   
   // Simple 20-period average of ATR (bars 1–20)
   double atrSum = 0.0;
   for(int i = 1; i <= 20; i++) atrSum += atrValues[i];
   double atrAvg = atrSum / 20.0;
   
   // Regime thresholds
   if(currentATR > atrAvg * 1.20)
      return REGIME_TRENDING;   // ATR 20% above average → expanding volatility
   if(currentATR < atrAvg * 0.80)
      return REGIME_RANGING;    // ATR 20% below average → compressing volatility
      
   return REGIME_UNKNOWN;       // Neutral / transitioning
}
 
void OnTick()
{
   ENUM_MARKET_REGIME regime = DetectRegime();
   
   double atrMultiplier;
   double tpMultiplier;
   
   switch(regime)
   {
      case REGIME_TRENDING:
         atrMultiplier = 2.0;   // Wider stop to ride the trend
         tpMultiplier  = 4.0;   // Extended target
         Print("Regime: TRENDING — wider stops, extended targets");
         break;
      case REGIME_RANGING:
         atrMultiplier = 1.0;   // Tight stop near range boundary
         tpMultiplier  = 1.5;   // Conservative target
         Print("Regime: RANGING — tight stops, conservative targets");
         break;
      default:
         atrMultiplier = 1.5;   // Default
         tpMultiplier  = 2.5;
         Print("Regime: NEUTRAL — standard parameters");
         break;
   }
   
   // Pass atrMultiplier into your stop/TP and lot size logic
}

ATR Limitations & Common Mistakes

ATR is a highly versatile indicator, but it has well-documented limitations. Understanding these prevents over-reliance and helps you build more robust EA logic.

Limitation / Mistake Why It's a Problem How to Address It
ATR is a lagging indicator It reflects past volatility; sudden news spikes are not captured until the next bar closes Combine with an economic calendar filter to pause the EA during scheduled high-impact news
Using ATR from the wrong timeframe A daily ATR stop applied to an M15 entry will be massively oversized, destroying R:R Always match ATR timeframe to signal timeframe; use MTF ATR only intentionally
Treating ATR as a directional signal A rising ATR does not confirm trend direction — it only confirms expanding range Pair ATR with trend-confirming indicators (e.g., ADX, moving average alignment)
Over-optimising the ATR multiplier Backtesting to find the "perfect" multiplier (e.g., 1.73×) leads to curve-fitting Use standard multiples (1×, 1.5×, 2×); validate across multiple instruments and time periods
Ignoring ATR during low-liquidity sessions ATR drops artificially during weekends, holidays, and the Asian session for USD pairs, distorting stop calculations Add a session filter; use an ATR floor (minimum ATR threshold) to prevent unrealistically tight stops
Not accounting for spread in stop calculations A stop set at exactly 1× ATR from entry may be within the spread, causing instant stop-outs on volatile pairs Add the current spread to the stop distance: stopDistance = (ATR × multiplier) + spread

The following snippet shows how to implement an ATR floor and spread buffer — two simple safeguards against the most common ATR implementation errors:

MQL5 Example: ATR Floor & Spread Buffer
input double InpATRMultiplier  = 1.5;    // Stop distance in ATR multiples
input double InpMinATRPips     = 20.0;   // Minimum ATR floor in pips
input double InpSpreadBuffer   = 1.5;    // Extra spread buffer multiplier
 
double GetSafeStopDistance(double atrValue)
{
   // Convert minimum ATR pips to price units
   double atrFloor = InpMinATRPips * _Point * 10; // adjust for 5-digit brokers
   
   // Use the larger of current ATR or the floor
   double effectiveATR = MathMax(atrValue, atrFloor);
   
   // Raw stop distance
   double stopDistance = effectiveATR * InpATRMultiplier;
   
   // Add spread buffer to prevent stop being within bid/ask spread
   double currentSpread = SymbolInfoInteger(_Symbol, SYMBOL_SPREAD) * _Point;
   stopDistance += currentSpread * InpSpreadBuffer;
   
   return NormalizeDouble(stopDistance, _Digits);
}
 
void OnTick()
{
   double atr  = GetCurrentATR(); // from earlier example
   double stop = GetSafeStopDistance(atr);
   
   double entryBuy = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   double slBuy    = entryBuy - stop;
   double tpBuy    = entryBuy + (stop * (2.5 / InpATRMultiplier)); // maintain R:R
   
   Print("Effective stop: ", stop / _Point, " pips",
         " (ATR: ", atr / _Point, " pips, spread buffered)");
}

Practical Application: Using ATR to Evaluate EA Stop-Loss Quality

When reviewing an EA's backtest or live results, you can use ATR as a benchmark to assess whether the EA's stop-loss logic is well-calibrated:

  1. Check average stop distance vs. ATR — If the EA's average stop is less than 0.5× ATR, it is likely getting stopped out by normal noise. Aim for 1× to 2× ATR.
  2. Examine stop-outs during news events — A spike in ATR that coincides with a run of stop-outs signals the EA needs a volatility filter.
  3. Compare ATR levels at entry vs. at stop-out — If ATR was already elevated at entry, the EA entered in unfavourable conditions.
  4. Use ATR to normalise drawdown analysis — Express maximum drawdown in ATR multiples rather than pips to make it comparable across different market regimes.
  5. Assess take-profit realism — A take-profit beyond 3× daily ATR is rarely hit on intraday strategies; recalibrate using ATR to set achievable targets.

Key Takeaways

  • ATR measures the magnitude of price movement — not direction — making it a pure volatility indicator
  • True Range accounts for gaps between sessions, making it more accurate than simple High−Low range
  • ATR-based stop-losses (1.5× to 2× ATR) outperform fixed pip stops by adapting to live market conditions
  • A minimum ATR filter prevents EAs from trading in low-volatility, choppy conditions where false signals are most common
  • Always reference ATR from the same timeframe as your trading signal for consistent stop and target calculations
  • Use ATR to normalise drawdown and performance metrics, enabling fair comparison of an EA across different volatility regimes
  • ATR position sizing keeps monetary risk constant by scaling lot size inversely with volatility — when ATR doubles, lot size halves
  • Compare ATR to its own moving average to detect market regime transitions — rising ATR signals trend expansion; falling ATR signals consolidation
  • ATR is a lagging indicator and does not indicate direction — always pair it with a trend-confirming tool such as ADX or moving average alignment
  • Protect against low-liquidity distortions by setting an ATR floor (minimum pip threshold) and adding a spread buffer to all stop-loss calculations
  • Avoid over-optimising the ATR multiplier in backtests — stick to round multiples (1×, 1.5×, 2×) and validate across multiple instruments to prevent curve-fitting