Average True Range (ATR)
length = 14 Overview
ATR measures market volatility by calculating the greatest distance among three ranges: the current high minus low, the absolute difference between current high and previous close, and the absolute difference between current low and previous close. J. Welles Wilder developed this true range concept to capture gaps and limit moves that traditional range calculations miss. The indicator then smooths these true range values using a 14 period modified moving average that weights recent volatility more heavily. Unlike percentage based indicators, ATR expresses volatility in absolute price terms, making it ideal for position sizing where a 2 ATR stop means the same risk regardless of share price. Traders multiply ATR by factors like 1.5 or 2 to set dynamic stop losses that expand during volatile periods and tighten during quiet markets. The indicator also identifies compression patterns when ATR drops to multi period lows, signaling potential breakouts as volatility cycles from contraction to expansion.
Implementation Examples
Compute ATR from OHLC arrays or Candles:
use vector_ta::indicators::atr::{atr, AtrInput, AtrParams};
use vector_ta::utilities::data_loader::{Candles, read_candles_from_csv};
// Sample OHLC data
let high = vec![101.2, 102.6, 103.1, 101.8, 100.9];
let low = vec![ 99.8, 100.7, 101.4, 99.9, 99.6];
let close = vec![100.5, 102.1, 101.9, 100.8, 100.2];
let params = AtrParams { length: Some(14) };
let input = AtrInput::from_slices(&high, &low, &close, params);
let output = atr(&input)?;
for (idx, value) in output.values.iter().enumerate() {
println!("ATR[{idx}] = {value:.4}");
}
// Using Candles with default configuration (length = 14)
let candles: Candles = read_candles_from_csv("data/sample.csv")?;
let input = AtrInput::with_default_candles(&candles);
let output = atr(&input)?; API Reference
Input Methods ▼
// From candles
AtrInput::from_candles(&Candles, AtrParams) -> AtrInput
// Using explicit OHLC slices (all must be equal length)
AtrInput::from_slices(&[f64], &[f64], &[f64], AtrParams) -> AtrInput
// Shortcut with defaults (length = 14)
AtrInput::with_default_candles(&Candles) -> AtrInput Parameters Structure ▼
pub struct AtrParams {
pub length: Option<usize>, // Default: 14
} Output Structure ▼
pub struct AtrOutput {
pub values: Vec<f64>, // ATR per bar aligned with the input length
} Validation, Warmup & NaNs ▼
length > 0elseAtrError::InvalidLength.- Slice inputs must have equal lengths; otherwise
AtrError::InconsistentSliceLengths. - Data must contain at least
lengthvalid points after the first valid H/L/C triple; elseAtrError::NotEnoughData. - Leading outputs up to
first_valid + length − 1areNaN(warmup). Thereafter, values are finite if inputs are finite. - Streaming:
AtrStream::updatereturnsNoneuntil the buffer fills; then returnsSome(atr)each update.
Error Handling ▼
use vector_ta::indicators::atr::AtrError;
match atr(&input) {
Ok(output) => consume(output),
Err(AtrError::InvalidLength { length }) => log::error!("ATR length {length} is invalid"),
Err(AtrError::InconsistentSliceLengths { high_len, low_len, close_len }) =>
panic!("Slice length mismatch: high={high_len}, low={low_len}, close={close_len}"),
Err(AtrError::NoCandlesAvailable) => log::warn!("Candles source returned no rows"),
Err(AtrError::NotEnoughData { length, data_len }) =>
log::warn!("Need {length} bars but only have {data_len}"),
} Python Bindings
Basic Usage ▼
Work with NumPy arrays for high, low, and close series:
import numpy as np
from vector_ta import atr
high = np.asarray([101.2, 102.6, 103.1, 101.8, 100.9], dtype=np.float64)
low = np.asarray([ 99.8, 100.7, 101.4, 99.9, 99.6], dtype=np.float64)
close = np.asarray([100.5, 102.1, 101.9, 100.8, 100.2], dtype=np.float64)
values = atr(high, low, close, length=14)
print("ATR values", values)
# Prefer explicit kernel selection when benchmarking
values = atr(high, low, close, length=21, kernel="scalar") Streaming Updates ▼
Maintain rolling ATR values with the PyO3-backed stream:
from vector_ta import AtrStream
stream = AtrStream(length=14)
for candle in ohlc_generator():
atr_value = stream.update(candle.high, candle.low, candle.close)
if atr_value is not None:
use_stream_value(candle.timestamp, atr_value) Batch Sweeps ▼
Evaluate multiple window sizes without leaving Python:
from vector_ta import atr_batch
result = atr_batch(
high,
low,
close,
length_range=(5, 30, 5),
kernel="auto",
)
values = result["values"] # shape: [rows, len(high)]
lengths = result["lengths"] # vector of window lengths CUDA Acceleration ▼
CUDA helpers are available when the Python package is built with CUDA support. Inputs must be float32; outputs are device arrays (DLPack / __cuda_array_interface__ compatible).
import numpy as np
from vector_ta import atr_cuda_batch_dev, atr_cuda_many_series_one_param_dev
# One series (float32)
high = np.asarray(load_high(), dtype=np.float32)
low = np.asarray(load_low(), dtype=np.float32)
close = np.asarray(load_close(), dtype=np.float32)
dev = atr_cuda_batch_dev(
high=high,
low=low,
close=close,
length_range=(5, 30, 5),
device_id=0,
)
# Many series (time-major)
high_tm = np.asarray(load_high_time_major_matrix(), dtype=np.float32)
rows, cols = high_tm.shape
high_tm = high_tm.ravel()
low_tm = np.asarray(load_low_time_major_matrix(), dtype=np.float32)
low_tm = low_tm.ravel()
close_tm = np.asarray(load_close_time_major_matrix(), dtype=np.float32)
close_tm = close_tm.ravel()
dev_tm = atr_cuda_many_series_one_param_dev(
high_tm=high_tm,
low_tm=low_tm,
close_tm=close_tm,
cols=cols,
rows=rows,
length=14,
device_id=0,
) JavaScript / WASM Bindings
Quick Usage ▼
Call ATR directly from TypeScript or JavaScript with Float64 arrays:
import { atr_js } from 'vectorta-wasm';
const high = new Float64Array([101.2, 102.6, 103.1, 101.8, 100.9]);
const low = new Float64Array([ 99.8, 100.7, 101.4, 99.9, 99.6]);
const close = new Float64Array([100.5, 102.1, 101.9, 100.8, 100.2]);
const values = atr_js(high, low, close, 14);
console.log('ATR values', Array.from(values)); Memory-Efficient Operations ▼
Reuse WASM memory by allocating buffers once and streaming data into them:
import { atr_alloc, atr_free, atr_into, memory } from 'vectorta-wasm';
const len = highs.length;
const highPtr = atr_alloc(len);
const lowPtr = atr_alloc(len);
const closePtr = atr_alloc(len);
const outPtr = atr_alloc(len);
new Float64Array(memory.buffer, highPtr, len).set(highs);
new Float64Array(memory.buffer, lowPtr, len).set(lows);
new Float64Array(memory.buffer, closePtr, len).set(closes);
atr_into(highPtr, lowPtr, closePtr, outPtr, len, 14);
const atrValues = new Float64Array(memory.buffer, outPtr, len).slice();
atr_free(highPtr, len);
atr_free(lowPtr, len);
atr_free(closePtr, len);
atr_free(outPtr, len); Batch & Metadata ▼
Sweep window sizes and align results with metadata describing every configuration:
import { atr_batch_js, atr_batch_metadata_js, atr_batch_unified_js } from 'vectorta-wasm';
const high = new Float64Array(loadHighs());
const low = new Float64Array(loadLows());
const close = new Float64Array(loadCloses());
const metadata = atr_batch_metadata_js(5, 30, 5); // returns [len1, len2, ...]
const combos = metadata.length;
const flattened = atr_batch_js(high, low, close, 5, 30, 5);
const block = high.length;
const firstResult = flattened.slice(0, block);
const unified = atr_batch_unified_js(high, low, close, {
length_range: [5, 30, 5],
});
console.log(unified.values.length, unified.lengths.length); CUDA Bindings (Rust)
use vector_ta::cuda::CudaAtr;
use vector_ta::indicators::atr::AtrBatchRange;
let cuda = CudaAtr::new(0)?;
let high: [f32] = /* ... */;
let low: [f32] = /* ... */;
let close: [f32] = /* ... */;
let sweep = AtrBatchRange::default();
let out = cuda.atr_batch_dev(&high, &low, &close, &sweep)?;
let _ = out; Performance Analysis
Across sizes, Rust CPU runs about 1.14× faster than Tulip C in this benchmark.
AMD Ryzen 9 9950X (CPU) | NVIDIA RTX 4090 (GPU) | Benchmarks: 2026-02-28