Normalized Average True Range (NATR)
period = 14 Overview
The Normalized Average True Range expresses volatility as a percentage of price rather than absolute dollar amounts, enabling direct comparison of volatility across assets trading at vastly different price levels. NATR calculates the standard ATR value then divides it by the closing price and multiplies by 100, transforming raw volatility into a percentage that remains consistent whether examining a $10 stock or a $1000 stock. This normalization proves invaluable for position sizing, where traders need to assess relative risk across diverse portfolios, and for screening markets to find assets with similar volatility characteristics regardless of price. The indicator retains all of ATR's advantages in handling gaps and limit moves through its use of true range, while adding the ability to meaningfully compare a penny stock's 5% daily movement against a blue chip's 1% swing. Portfolio managers particularly value NATR for risk parity strategies where equal volatility weighting across positions requires normalized rather than absolute volatility measurements.
Implementation Examples
Compute NATR from OHLC arrays or candles in a few lines:
use vector_ta::indicators::natr::{natr, NatrInput, NatrParams};
use vector_ta::utilities::data_loader::{Candles, read_candles_from_csv};
// OHLC slices (all equal length)
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 = NatrParams { period: Some(14) }; // default
let input = NatrInput::from_slices(&high, &low, &close, params);
let out = natr(&input)?;
for (i, v) in out.values.iter().enumerate() {
println!("NATR[%d] = %.4f", i, v);
}
// With Candles and defaults (period = 14)
let candles: Candles = read_candles_from_csv("data/sample.csv")?;
let input = NatrInput::with_default_candles(&candles);
let out = natr(&input)?; API Reference
Input Methods ▼
// From candles (default period = 14)
NatrInput::from_candles(&Candles, NatrParams) -> NatrInput
// From explicit OHLC slices (equal length)
NatrInput::from_slices(&[f64], &[f64], &[f64], NatrParams) -> NatrInput
// Shortcut with defaults (period = 14)
NatrInput::with_default_candles(&Candles) -> NatrInput Parameters Structure ▼
#[derive(Debug, Clone, Default)]
pub struct NatrParams {
pub period: Option<usize>, // Default: Some(14)
} Output Structure ▼
pub struct NatrOutput {
pub values: Vec<f64>, // NATR percentage values, aligned to input length
} Validation, Warmup & NaNs ▼
- Inputs must be non-empty and equal length; otherwise
NatrError::EmptyDataorNatrError::MismatchedLength. period > 0andperiod ≤ len; elseNatrError::InvalidPeriod { period, data_len }.- First valid index is max of first finite values in
high/low/close; if none,NatrError::AllValuesNaN. - Requires at least
periodvalid points after first valid index; elseNatrError::NotEnoughValidData { needed, valid }. - Warmup: indices before
first_valid_idx + period - 1areNaN. - If
closeis non‑finite or zero at an index, the output at that index isNaN.
Error Handling ▼
use vector_ta::indicators::natr::{natr, NatrError};
match natr(&input) {
Ok(output) => process(output.values),
Err(NatrError::EmptyData) => eprintln!("natr: empty data"),
Err(NatrError::InvalidPeriod { period, data_len }) => {
eprintln!("natr: invalid period {} for length {}", period, data_len);
}
Err(NatrError::NotEnoughValidData { needed, valid }) => {
eprintln!("natr: need {} valid values, found {}", needed, valid);
}
Err(NatrError::AllValuesNaN) => eprintln!("natr: all inputs are NaN"),
Err(NatrError::MismatchedLength { expected, actual }) => {
eprintln!("natr: length mismatch, expected {}, actual {}", expected, actual);
}
} Python Bindings
Basic Usage ▼
Calculate NATR using NumPy arrays (defaults: period=14). Kernel selection is optional.
import numpy as np
from vector_ta import natr
high = np.array([101.2, 102.6, 103.1, 101.8, 100.9], dtype=float)
low = np.array([ 99.8, 100.7, 101.4, 99.9, 99.6], dtype=float)
close= np.array([100.5, 102.1, 101.9, 100.8, 100.2], dtype=float)
# Default period = 14
values = natr(high, low, close, period=14) # returns np.ndarray
# Specify kernel for performance (optional): 'auto', 'scalar', 'avx2', 'avx512'
values = natr(high, low, close, period=14, kernel='auto') Batch Processing ▼
Sweep period ranges and get a 2D matrix of results:
import numpy as np
from vector_ta import natr_batch
high = np.asarray([...], dtype=float)
low = np.asarray([...], dtype=float)
close= np.asarray([...], dtype=float)
res = natr_batch(high, low, close, period_range=(7, 28, 1))
# res['values']: 2D array [rows x cols], res['periods']: 1D periods tested
matrix = res['values']
periods = res['periods'] Streaming ▼
from vector_ta import NatrStream
stream = NatrStream(14)
for h, l, c in realtime_ohlc_feed():
value_or_none = stream.update(h, l, c)
if value_or_none is not None:
handle(value_or_none) 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 natr_cuda_batch_dev, natr_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 = natr_cuda_batch_dev(
high=high,
low=low,
close=close,
period_range=(5, 30, 5),
device_id=0,
)
# Many series (time-major)
high_tm = np.asarray(load_high_time_major_matrix(), dtype=np.float32)
low_tm = np.asarray(load_low_time_major_matrix(), dtype=np.float32)
close_tm = np.asarray(load_close_time_major_matrix(), dtype=np.float32)
dev_tm = natr_cuda_many_series_one_param_dev(
high_tm=high_tm,
low_tm=low_tm,
close_tm=close_tm,
period=14,
device_id=0,
) JavaScript/WASM Bindings
Basic Usage ▼
Compute NATR from OHLC arrays:
import { natr_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]);
// period = 14
const values = natr_js(high, low, close, 14);
console.log('NATR:', values); Memory-Efficient Operations ▼
Zero-copy into pre-allocated WASM memory:
import { natr_alloc, natr_free, natr_into, memory } from 'vectorta-wasm';
const len = close.length;
const hPtr = natr_alloc(len);
const lPtr = natr_alloc(len);
const cPtr = natr_alloc(len);
const outPtr = natr_alloc(len);
// Copy inputs into WASM memory
new Float64Array(memory.buffer, hPtr, len).set(high);
new Float64Array(memory.buffer, lPtr, len).set(low);
new Float64Array(memory.buffer, cPtr, len).set(close);
// Write results into outPtr
natr_into(hPtr, lPtr, cPtr, outPtr, len, 14);
const out = new Float64Array(memory.buffer, outPtr, len).slice();
natr_free(hPtr, len); natr_free(lPtr, len); natr_free(cPtr, len); natr_free(outPtr, len); Batch Processing ▼
Sweep period ranges using a unified config object:
import { natr_batch } from 'vectorta-wasm';
const cfg = { period_range: [7, 28, 1] };
const { values, combos, rows, cols } = natr_batch(high, low, close, cfg);
// 'values' is flat [rows * cols]; 'combos' are NatrParams objects
const firstRow = values.slice(0, cols); CUDA Bindings (Rust)
use vector_ta::cuda::CudaNatr;
use vector_ta::indicators::natr::NatrBatchRange;
let cuda = CudaNatr::new(0)?;
let high: [f32] = /* ... */;
let low: [f32] = /* ... */;
let close: [f32] = /* ... */;
let sweep = NatrBatchRange::default();
let out = cuda.natr_batch_dev(&high, &low, &close, &sweep)?;
let _ = out; Performance Analysis
Across sizes, Rust CPU runs about 1.01× slower than Tulip C in this benchmark.
AMD Ryzen 9 9950X (CPU) | NVIDIA RTX 4090 (GPU) | Benchmarks: 2026-01-05
CUDA note
In our benchmark workload, the Rust CPU implementation is faster than CUDA for this indicator. Prefer the Rust/CPU path unless your workload differs.