Files
wickra/docs/wiki/indicators/volatility/Indicator-Atr.md
T

227 lines
7.8 KiB
Markdown
Raw Normal View History

2026-05-22 16:18:04 +02:00
# ATR (Average True Range)
> Wilder's volatility benchmark: an exponentially-smoothed average of the
> per-bar true range that absorbs overnight gaps and is dimensioned in price
> units.
## Quick reference
| Item | Value |
|---------------------|--------------------------------------------------------------------------------------|
| Family | Volatility |
| Sub-category | range-average |
| Input type | `Candle` (uses `high`, `low`, `close`) |
| Output type | `f64` |
| Output range | unbounded `≥ 0` |
| Default parameters | `period = 14` (Wilder) |
| Warmup period | `period` (14 for defaults) |
| Interpretation | dollar-denominated volatility scale; rises in chop and expansion |
## Formula
For each candle, the **true range** is
`TR_t = max(H_t - L_t, |H_t - C_{t-1}|, |L_t - C_{t-1}|)` when a previous
close is available, otherwise `TR_t = H_t - L_t` (see `Candle::true_range`
in `crates/wickra-core/src/ohlcv.rs`).
ATR is then Wilder-smoothed:
```
seed_ATR_period = (TR_1 + TR_2 + … + TR_period) / period
ATR_t = ((period - 1) * ATR_{t-1} + TR_t) / period for t > period
```
This is mathematically the same recursion as an EMA with `alpha = 1/period`
(Wilder smoothing), seeded with a simple mean of the first `period` true
ranges (`crates/wickra-core/src/indicators/atr.rs:58-69`).
## Parameters
| Name | Type | Default | Constraint | Source |
|----------|---------|---------|------------|-------------------------------------|
| `period` | `usize` | `14` | `> 0` | `Atr::new` (`atr.rs:26`) |
Python default from `#[pyo3(signature = (period=14))]` in
`bindings/python/src/lib.rs`. `period == 0` returns `Error::PeriodZero`.
## Inputs / Outputs
```rust
impl Indicator for Atr {
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64>;
fn warmup_period(&self) -> usize { self.period }
}
```
- **Rust input.** A full `Candle` struct; only `high`, `low`, and `close`
are read (`prev_close` is cached internally between calls).
- **Python streaming.** Accepts either a 6-tuple
`(open, high, low, close, volume, timestamp)` or a dict with keys
`open`, `high`, `low`, `close`, `volume`, and optional `timestamp`.
- **Python batch.** `ATR.batch(high, low, close)` takes three equal-length
`numpy.ndarray` columns and returns a 1-D `np.ndarray` with `NaN` for
every warmup row.
- **Node streaming.** `atr.update(high, low, close)` returns `number | null`.
- **Node batch.** `atr.batch(high, low, close)` returns `Array<number>` of
the same length, `NaN` during warmup.
## Warmup
`warmup_period() == period`. The first `period - 1` candles return `None`
(or `NaN`/`null` in batch); the `period`-th candle returns the seed value
`(TR_1 + … + TR_period) / period`. Each subsequent candle applies the
Wilder recursion.
Verified for `period = 3`: the first non-`None` output is at index `2`
(the 3rd candle).
## Edge cases
- **First candle.** `Candle::true_range(None)` falls back to `high - low`
because there is no previous close yet. The first TR is the bar range.
- **Gaps.** With a previous close at `5.0` and a candle of `H=10, L=9`,
`TR = max(1, 5, 4) = 5` — i.e. `|H - prev_close|` dominates. The
pinned test `gap_up_uses_high_minus_prev_close` covers exactly this.
- **Constant input.** A series of identical candles (no gaps, fixed range)
yields a constant ATR equal to the bar range, even before the seed is
complete — the smoothing has nothing to smooth.
- **Non-negativity.** ATR is always `≥ 0`. The Rust test `never_negative`
pins this property across a sinusoidal price series.
- **NaN / infinity.** `Candle::new` rejects non-finite `open`/`high`/
`low`/`close`/`volume`; constructing the candle returns
`Error::InvalidCandle` before it can ever reach ATR.
- **Reset.** `reset()` clears `prev_close`, the seed buffer, and the
running average; the next call behaves as if the indicator were
freshly constructed.
## Examples
### Rust
```rust
use wickra::{Atr, BatchExt, Candle, Indicator};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let candles = vec![
Candle::new(10.0, 11.0, 9.0, 10.5, 1.0, 0)?,
Candle::new(10.5, 12.0, 10.0, 11.5, 1.0, 0)?,
Candle::new(11.5, 13.0, 11.0, 12.5, 1.0, 0)?,
Candle::new(12.5, 14.0, 12.0, 13.5, 1.0, 0)?,
Candle::new(13.5, 15.0, 13.0, 14.5, 1.0, 0)?,
];
let mut atr = Atr::new(3)?;
println!("{:?}", atr.batch(&candles));
Ok(())
}
```
Output:
```
[None, None, Some(2.0), Some(2.0), Some(2.0)]
```
Every bar has range `2.0` and no gap-driven TR component, so both the
seed `(2 + 2 + 2) / 3 = 2.0` and every subsequent Wilder update stay at
`2.0`.
### Python
```python
import numpy as np
import wickra as ta
atr = ta.ATR(3)
high = np.array([11.0, 12.0, 13.0, 14.0, 15.0])
low = np.array([ 9.0, 10.0, 11.0, 12.0, 13.0])
close = np.array([10.5, 11.5, 12.5, 13.5, 14.5])
print(atr.batch(high, low, close))
```
Output:
```
[nan nan 2. 2. 2.]
```
### Node
```js
const w = require('wickra');
const atr = new w.ATR(3);
console.log(atr.batch(
[11, 12, 13, 14, 15],
[ 9, 10, 11, 12, 13],
[10.5, 11.5, 12.5, 13.5, 14.5],
));
```
Output:
```
[ NaN, NaN, 2, 2, 2 ]
```
Streaming form (`atr.update(high, low, close)`):
```js
const w = require('wickra');
const atr = new w.ATR(3);
console.log(atr.update(11, 9, 10.5));
console.log(atr.update(12, 10, 11.5));
console.log(atr.update(13, 11, 12.5));
console.log(atr.update(14, 12, 13.5));
```
Output:
```
null
null
2
2
```
## Interpretation
- **Stop sizing.** A common pattern is "place a stop `k * ATR` away from
entry," with `k` typically in `[1.5, 3.0]` depending on the timeframe.
ATR's units are price, so the stop distance is directly tradable.
- **Position sizing.** `risk_per_trade / ATR` gives a quantity that
normalises risk across assets of very different price levels.
- **Regime detection.** Persistently rising ATR signals an expansion
regime; persistently low ATR signals consolidation, often preceding
expansion (the volatility-of-volatility argument).
## Common pitfalls
- **Wilder smoothing vs EMA.** Wilder's smoothing factor is `1/period`,
not the EMA's `2/(period+1)`. They look similar but produce different
numbers; a 14-period Wilder ATR is **not** the same as a 14-period
EMA of true range. Wickra uses the Wilder recursion explicitly.
- **Off-by-one seeding.** ATR(14) emits its first value on the 14th
candle, not the 15th — unlike RSI(14) which needs 15 candles for 14
diffs. The difference is that ATR's seed uses `period` true ranges
directly (and `TR_1` is well-defined even without a previous close),
while RSI(14) needs 14 *differences* between consecutive closes.
## References
- J. Welles Wilder Jr., *New Concepts in Technical Trading Systems*,
Trend Research, 1978. Chapter on the Average True Range and the
Wilder smoothing constant.
## See also
- [Bollinger Bands](Indicator-BollingerBands.md) — stddev-based volatility
envelope around an SMA.
- [Keltner Channels](Indicator-Keltner.md) — directly composes EMA + ATR.
- [Donchian Channels](Indicator-Donchian.md) — rolling high/low without
any smoothing.
- [PSAR](Indicator-Psar.md) — uses ATR-like volatility tracking implicitly
through its acceleration factor.