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
Where N is the lookback period (default: 14 periods)
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:
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):
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:
// 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:
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:
// ─── 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.
// 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:
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:
- 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.
- 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.
- Compare ATR levels at entry vs. at stop-out — If ATR was already elevated at entry, the EA entered in unfavourable conditions.
- Use ATR to normalise drawdown analysis — Express maximum drawdown in ATR multiples rather than pips to make it comparable across different market regimes.
- 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