How Does ATR Measure Volatility? A Pine Script v6 Deep Dive into Average True Range

When building systematic trading tools, quantifying market volatility is a foundational requirement — and the Average True Range (ATR) is one of the most mathematically rigorous ways to do it. Developed by J. Welles Wilder Jr., ATR measures the average magnitude of price movement over a rolling window, making it indispensable for dynamic stop-loss placement, position sizing, and volatility-adaptive indicators. This article dissects the ATR formula, explains its Pine Script v6 implementation, and demonstrates how to use it for stop-loss engineering.

1. The Mathematics of True Range

Before ATR can be computed, we must define the True Range (TR) for each bar. TR captures the full extent of price movement, including gaps between sessions. It is defined as the maximum of three values:

$$TR = \max\left(H - L,\; |H - C_{prev}|,\; |L - C_{prev}|\right)$$

Where:

  • $H$ = Current bar's High
  • $L$ = Current bar's Low
  • $C_{prev}$ = Previous bar's Close
Component Formula Captures
Intrabar Range $H - L$ Normal intraday volatility
Upward Gap $|H - C_{prev}|$ Gap-up openings
Downward Gap $|L - C_{prev}|$ Gap-down openings

2. From True Range to ATR: Wilder's Smoothing

ATR is computed by applying Wilder's Smoothing (also known as the Recursive Moving Average, or RMA) to the True Range series. This is mathematically equivalent to an Exponential Moving Average with a smoothing factor of $\alpha = \frac{1}{n}$:

$$ATR_t = \frac{(n-1) \cdot ATR_{t-1} + TR_t}{n}$$

Where $n$ is the ATR period (Wilder's original default was 14). This recursive formula means ATR is history-dependent — every prior bar's TR contributes to the current value, with exponentially decaying weight. In Pine Script v6, this is implemented natively via ta.atr(), which internally calls ta.rma() on the True Range.

graph TD A[Bar Data: High, Low, Close] --> B[Compute True Range] B --> C1[TR1 = High - Low] B --> C2[TR2 = abs High - prevClose] B --> C3[TR3 = abs Low - prevClose] C1 --> D[TR = max TR1 TR2 TR3] C2 --> D C3 --> D D --> E[Apply Wilders RMA Smoothing] E --> F[ATR = RMA of TR over N bars] F --> G1[Plot ATR in Separate Pane] F --> G2[Normalize: ATR Pct = ATR divided by Close times 100] F --> G3[Stop Distance = Multiplier times ATR] G3 --> H1[Long Stop = Close minus StopDistance] G3 --> H2[Short Stop = Close plus StopDistance]

3. Key Properties of ATR

  • ATR is always non-negative: Since TR is a maximum of absolute values, $TR \geq 0$ always, and therefore $ATR \geq 0$.
  • ATR is not directional: It measures the magnitude of movement, not its direction. A rising ATR means volatility is expanding; a falling ATR means it is contracting.
  • ATR is price-denominated: It is expressed in the same units as the price (e.g., dollars, pips). To compare across instruments, normalize it: $ATR\% = \frac{ATR}{Close} \times 100$.
  • Warm-up behavior: Because ta.rma() (and therefore ta.atr()) starts calculating from bar 0 using the first available value, there is no hard na warm-up window equal to the period length. Values are available from the very first bar, though they stabilize after approximately $3n$ bars.

4. Pine Script v6 Implementation

The following script implements ATR from scratch using the manual True Range formula, plots it alongside the built-in ta.atr() for verification, and overlays ATR-based stop-loss bands directly on the price chart.

🔽 [Click to expand] View Full Pine Script Code
//@version=6
indicator("ATR Volatility & Stop-Loss Bands", overlay = false)

// ─── INPUTS ───────────────────────────────────────────────────────────────────
int    atrLength    = input.int(14,  "ATR Length",    minval = 1)       // Wilder's default: 14
float  atrMult     = input.float(2.0, "Stop-Loss ATR Multiplier", minval = 0.1, step = 0.1)
bool   showBuiltin = input.bool(true, "Show Built-in ta.atr() for Comparison")

// ─── STEP 1: MANUAL TRUE RANGE CALCULATION ────────────────────────────────────
// TR = max(High - Low, |High - prev_Close|, |Low - prev_Close|)
// On bar 0, close[1] is na, so math.abs(high - na) = na.
// math.max(na, na, high - low) = high - low (math.max ignores na).
float prevClose = close[1]                                              // Previous bar's close
float tr1       = high - low                                           // Intrabar range
float tr2       = math.abs(high - prevClose)                           // Gap-up component
float tr3       = math.abs(low  - prevClose)                           // Gap-down component
float trManual  = math.max(tr1, math.max(tr2, tr3))                    // True Range

// ─── STEP 2: MANUAL ATR VIA WILDER'S RMA ─────────────────────────────────────
// ta.rma() implements: ATR_t = ((n-1) * ATR_{t-1} + TR_t) / n
// ta.rma() requires simple int for its length parameter.
// atrLength is input int (≤ simple), so this is legal.
float atrManual = ta.rma(trManual, atrLength)                          // Manual ATR

// ─── STEP 3: BUILT-IN ATR FOR VERIFICATION ────────────────────────────────────
// ta.atr() also requires simple int for length. atrLength satisfies this.
float atrBuiltin = ta.atr(atrLength)                                   // Built-in ATR

// ─── STEP 4: NORMALIZED ATR (%) ───────────────────────────────────────────────
// Expresses ATR as a percentage of the current close price.
// Guard against close == 0 to avoid division by zero.
float atrPct = close != 0 ? (atrManual / close) * 100.0 : na          // ATR as % of price

// ─── STEP 5: STOP-LOSS DISTANCE ───────────────────────────────────────────────
// A common technique: place stop-loss at N * ATR from the close.
float stopLossDistance = atrMult * atrManual                           // Raw stop distance

// ─── PLOTS (SEPARATE PANE) ────────────────────────────────────────────────────
// Plot manual ATR
plot(atrManual,
     title     = "ATR (Manual RMA)",
     color     = color.new(color.blue, 0),
     linewidth = 2)

// Conditionally plot built-in ATR for comparison
plot(showBuiltin ? atrBuiltin : na,
     title     = "ATR (Built-in)",
     color     = color.new(color.orange, 40),
     linewidth = 1)

// Plot ATR % on the same pane (secondary reference)
plot(atrPct,
     title     = "ATR % of Close",
     color     = color.new(color.teal, 0),
     linewidth = 1)

// Plot stop-loss distance
plot(stopLossDistance,
     title     = "Stop-Loss Distance (ATR × Mult)",
     color     = color.new(color.red, 20),
     linewidth = 1,
     style     = plot.style_histogram)

// Reference line at zero (using plot, not hline, since this is a separate pane
// and hline would interfere with the pane's scale in unexpected ways)
plot(0,
     title     = "Zero",
     color     = color.new(color.gray, 60),
     linewidth = 1,
     style     = plot.style_line)

5. ATR-Based Stop-Loss Bands on the Price Chart

The most practical application of ATR is placing dynamic stop-loss levels directly on the price chart. The logic is straightforward: the stop-loss distance is $n \times ATR$, subtracted from (long stop) or added to (short stop) the current close. Because ATR expands during volatile periods, the stop automatically widens to avoid being triggered by noise.

🔽 [Click to expand] View ATR Stop-Loss Overlay Script
//@version=6
indicator("ATR Stop-Loss Overlay", overlay = true)

// ─── INPUTS ───────────────────────────────────────────────────────────────────
int   atrLen  = input.int(14,  "ATR Length",     minval = 1)
float mult    = input.float(2.0, "ATR Multiplier", minval = 0.1, step = 0.1)

// ─── ATR CALCULATION ──────────────────────────────────────────────────────────
// ta.atr() requires simple int. atrLen is input int, which satisfies simple int.
float atr = ta.atr(atrLen)

// ─── STOP-LOSS BAND LEVELS ────────────────────────────────────────────────────
// Long stop: below current close by N * ATR
// Short stop: above current close by N * ATR
float longStop  = close - mult * atr                                   // Long position stop
float shortStop = close + mult * atr                                   // Short position stop

// ─── PLOTS ON PRICE CHART ─────────────────────────────────────────────────────
plot(longStop,
     title     = "Long Stop-Loss (Close - N×ATR)",
     color     = color.new(color.red, 0),
     linewidth = 1,
     style     = plot.style_linebr)                                    // Break at na gaps

plot(shortStop,
     title     = "Short Stop-Loss (Close + N×ATR)",
     color     = color.new(color.green, 0),
     linewidth = 1,
     style     = plot.style_linebr)

// ─── FILL BETWEEN BANDS ───────────────────────────────────────────────────────
// Shade the volatility corridor between the two stop levels.
p1 = plot(longStop,  display = display.none)                           // Hidden plot for fill
p2 = plot(shortStop, display = display.none)                           // Hidden plot for fill
fill(p1, p2,
     color = color.new(color.gray, 88),
     title = "ATR Volatility Corridor")

6. ATR Qualifier Constraints in Pine Script v6

A critical engineering detail: ta.atr() and ta.rma() both require a simple int for their length parameter. This means you cannot pass a dynamically computed (series) integer as the length. The table below summarizes legal and illegal patterns:

Pattern Qualifier Legal for ta.atr()?
ta.atr(14) const int ✅ Yes
ta.atr(input.int(14)) input int ✅ Yes (input ≤ simple)
int dynLen = int(ta.atr(14) * 2); ta.atr(dynLen) series int ❌ Compile Error
var int v = 14; if cond: v := 20; ta.atr(v) series int (var + :=) ❌ Compile Error

If you need a dynamic ATR period, you must manually implement the RMA using ta.rma() with a fixed length and vary the source series instead, or use ta.sma() (which accepts series int) as an approximation of the smoothing step.

7. Normalized ATR: Cross-Instrument Comparison

Because ATR is price-denominated, a raw ATR of 5.0 means very different things for a $10 stock versus a $500 stock. The Normalized ATR (also called ATR%) solves this:

$$ATR\% = \frac{ATR}{Close} \times 100$$

This allows you to compare volatility regimes across different instruments and timeframes on a common scale. For example, an ATR% of 1.5% on EURUSD versus 3.2% on BTCUSD immediately communicates that Bitcoin is roughly twice as volatile on that day, regardless of their absolute price levels.

8. Practical Stop-Loss Engineering with ATR

The ATR multiplier approach to stop-loss placement is grounded in the following logic: if the market's average daily movement is $ATR$, then a stop placed at $2 \times ATR$ below entry has a low probability of being triggered by random noise alone, while still limiting downside risk. Common multiplier conventions are:

Multiplier Stop Distance Typical Use Case
1.0× Tight Scalping, high-frequency systems
1.5× Moderate Intraday swing trades
2.0× Standard Wilder's original recommendation
3.0× Wide Trend-following, position trading

9. Conclusion

  • True Range captures gaps: Unlike simple High-Low range, TR incorporates the previous close, ensuring that overnight gaps and limit moves are fully reflected in the volatility measurement.
  • ATR uses Wilder's RMA (not SMA): The recursive smoothing formula $ATR_t = \frac{(n-1) \cdot ATR_{t-1} + TR_t}{n}$ gives more weight to recent volatility while retaining historical context. In Pine Script v6, ta.atr() and ta.rma() both require simple int for their length parameter — dynamic (series) lengths are not supported.
  • ATR-based stops are self-adjusting: By anchoring stop-loss distances to a multiple of ATR, the stop automatically widens during high-volatility regimes and tightens during low-volatility periods, providing a mathematically consistent risk framework.

Ideas for Further Development

  • ATR Trailing Stop: Instead of anchoring the stop to the current close, track the highest close (for longs) and subtract $N \times ATR$, ratcheting the stop upward as price advances. This creates a trend-following exit mechanism.
  • Volatility Regime Filter: Compute a long-period ATR (e.g., 50-bar) and a short-period ATR (e.g., 10-bar). When the ratio $\frac{ATR_{10}}{ATR_{50}} > 1.5$, flag the market as entering a high-volatility regime and automatically widen position sizing or stop distances accordingly.

Related Posts

Comments

Popular posts from this blog

How to Build a Volume Indicator in Pine Script v6: Buying & Selling Pressure Analysis

Pine Script v6 plot() Function: Complete Guide to Lines, Histograms, and Circles

SuperTrend Indicator in Pine Script v6: Canonical Formula, Flip Logic, and Non-Repainting Signals