ADX
Wilder's Average Directional Index — the smoothed strength of a trend, plus the two directional components (
+DI,−DI) that say which direction the trend is going.
Quick reference
| Field | Value |
|---|---|
| Family | Trend & Directional |
| Input type | Candle |
| Output type | AdxOutput { plus_di, minus_di, adx } |
| Output range | each field in [0, 100] |
| Default parameters | period = 14 (Python) |
| Warmup period | 2 · period (28 for period = 14) |
| Interpretation | adx > 25 means a meaningful trend; the dominant DI gives its direction |
Formula
For each new candle at time t (with previous candle t-1):
+DM_t = high_t − high_{t-1} if (high_t − high_{t-1}) > (low_{t-1} − low_t)
and (high_t − high_{t-1}) > 0
= 0 otherwise
−DM_t = low_{t-1} − low_t if (low_{t-1} − low_t) > (high_t − high_{t-1})
and (low_{t-1} − low_t) > 0
= 0 otherwise
TR_t = max(high_t − low_t,
|high_t − close_{t-1}|,
|low_t − close_{t-1}|)Wilder's smoothing is applied to all three series. Seeding is a simple sum over the first period post-prev candles; after seeding the update rule for any of these is
S_t = S_{t-1} − S_{t-1} / period + X_twhere X_t is TR_t, +DM_t, or −DM_t. The directional indicators and DX then are
+DI_t = 100 · (+DM smoothed)_t / (TR smoothed)_t
−DI_t = 100 · (−DM smoothed)_t / (TR smoothed)_t
DX_t = 100 · |+DI_t − −DI_t| / (+DI_t + −DI_t)ADX_t is itself a Wilder-smoothed DX series, seeded as the mean of the first period DX values, and then updated with α = 1/period:
ADX_t = (ADX_{t-1} · (period − 1) + DX_t) / periodWhen +DI + −DI == 0, DX is 0; when TR == 0, both DI lines are 0. These are the divide-by-zero guards in Adx::update.
Parameters
| Name | Type | Default (Python) | Valid range | Description |
|---|---|---|---|---|
period | usize | 14 | >= 1 | Wilder smoothing length shared by +DM, −DM, TR, and ADX. |
Adx::new(0) returns Error::PeriodZero.
Inputs / Outputs
From impl Indicator for Adx:
use wickra::{Indicator, Adx, Candle, AdxOutput};
// Adx: Input = Candle, Output = AdxOutput
const _: fn(&mut Adx, Candle) -> Option<AdxOutput> = <Adx as Indicator>::update;AdxOutput:
| Field | Description |
|---|---|
plus_di | Plus Directional Indicator (+DI) — strength of upward movement. |
minus_di | Minus Directional Indicator (−DI) — strength of downward movement. |
adx | Average Directional Index — smoothed |DX|, a directionless trend-strength measure. |
Python's ADX.batch(high, low, close) returns a (n, 3) float64 array with columns [plus_di, minus_di, adx]; warmup rows are entirely NaN. The streaming update(candle) returns a (plus_di, minus_di, adx) tuple or None.
Node's ADX.batch(high, low, close) returns a flat number[] of length n * 3, interleaved [plus_di_0, minus_di_0, adx_0, plus_di_1, …]. Only batch is exposed on the Node binding — no update.
Warmup
warmup_period() returns 2 · period. The first candle just provides a "previous" reference (no DM/TR can be computed yet); the next period candles seed the smoothed +DM, −DM, and TR sums; the next period candles after that produce DX values that seed ADX. For period = 14 that's 1 + 14 + 13 = 28 candles before the first full AdxOutput, which matches 2 · 14 = 28.
Edge cases
- Strong unidirectional trend. If every candle is strictly higher than the last (with
+DMalways positive,−DMalways zero),+DIsaturates at100,−DIat0, andADXclimbs toward100. The example below produces exactly that. - Flat market (no high/low movement). Every
TR,+DM,−DMis zero, so the divide-by-zero guards return+DI = −DI = 0andDX = 0;ADXthen sits at0indefinitely. - Reset.
reset()clearsprev, all seed sums and counts, all smoothed values, the DX buffer, andadx_value.
Examples
Rust
use wickra::{Adx, BatchExt, Candle, Indicator};
let candles: Vec<Candle> = (0..40)
.map(|i| {
let base = 100.0 + i as f64 * 2.0;
Candle::new(base + 0.5, base + 1.0, base - 0.5, base + 0.5, 1.0, 0).unwrap()
})
.collect();
let mut adx = Adx::new(14)?;
let out = adx.batch(&candles);
let v = out[27].unwrap();
println!("row 27 +DI={} -DI={} ADX={}", v.plus_di, v.minus_di, v.adx);
let v = out[39].unwrap();
println!("row 39 +DI={} -DI={} ADX={}", v.plus_di, v.minus_di, v.adx);
# Ok::<(), wickra::Error>(())Verified output:
row 27 +DI=80 -DI=0 ADX=100
row 39 +DI=80 -DI=0 ADX=100Python
import numpy as np
import wickra as ta
n = 40
i = np.arange(n, dtype=float)
base = 100.0 + i * 2.0
high = base + 1.0
low = base - 0.5
close = base + 0.5
adx = ta.ADX(14)
out = adx.batch(high, low, close)
print('warmup:', adx.warmup_period())
print('shape :', out.shape)
print('row 27:', out[27])
print('row 39:', out[39])Verified output:
warmup: 28
shape : (40, 3)
row 27: [ 80. 0. 100.]
row 39: [ 80. 0. 100.]Node
const wickra = require('wickra');
const n = 40;
const high = [], low = [], close = [];
for (let i = 0; i < n; i++) {
const b = 100 + i * 2;
high.push(b + 1);
low.push(b - 0.5);
close.push(b + 0.5);
}
const adx = new wickra.ADX(14);
const out = adx.batch(high, low, close);
console.log('len :', out.length);
console.log('row 27:', { plusDi: out[27 * 3], minusDi: out[27 * 3 + 1], adx: out[27 * 3 + 2] });
console.log('row 39:', { plusDi: out[39 * 3], minusDi: out[39 * 3 + 1], adx: out[39 * 3 + 2] });Verified output:
len : 120
row 27: { plusDi: 80, minusDi: 0, adx: 100 }
row 39: { plusDi: 80, minusDi: 0, adx: 100 }Interpretation
- Trend-strength bands.
ADX < 20is typically read as a ranging market;ADX > 25as a "real" trend;ADX > 40as a strong trend. ADX itself is direction-agnostic — you need+DIvs−DIto know which way the trend points. - DI crossover.
+DIcrossing above−DIis a bullish directional signal; the mirror is bearish. Many traders only act on a crossover whenADX > 25to filter out crossovers in a ranging market. - ADX peaks. A rising ADX confirms trend continuation; a falling ADX from a high level suggests the current trend is exhausting (even if
+DIstill dominates−DI).
Common pitfalls
- Long warmup. ADX needs
2 · periodcandles before the first emission — twice as many as most other Wilder indicators. A common bug is reusing an "RSI fits inperiod + 1bars" mental model and reading garbage during the ADX warmup; checkis_ready()or test forNaNon theadxcolumn. - Plotted on the same axis as DI.
+DI,−DI, andADXall live in[0, 100]and are typically overlaid. The crossover signal is between+DIand−DIonly —ADXdoes not cross either of them for any directional meaning.
References
- J. Welles Wilder, New Concepts in Technical Trading Systems, Trend Research, 1978 — the original publication of
+DI,−DI,DX,ADX, and the smoothing scheme they share with RSI and ATR.
See also
- Indicator: Rsi — shares Wilder smoothing.
- Indicator: Aroon — alternative trend-strength measure, range-based.
- Indicator: MacdIndicator — trend-following momentum, useful as a confirmation against
+DI/−DI. - Warmup Periods — the
2 · periodADX entry.