Files
wickra/docs/wiki/indicators/trend-directional/Indicator-Adx.md
T
kingchenc d2f99efd78 F13c: restructure the indicator catalogue into eight families
The original taxonomy was four classical families plus a statistics group,
with the F1-F12 expansion slotted in as sub-categories. This regroups the
whole 71-indicator catalogue into eight top-level families, each with at
least five members:

  Moving Averages (12), Momentum Oscillators (13), Trend & Directional (9),
  Price Oscillators (5), Volatility & Bands (12), Trailing Stops (5),
  Volume (9), Price Statistics (7).

- Wiki: docs/wiki/indicators/ reorganised into eight family folders; all 71
  indicator pages moved with `git mv`. Every internal cross-link is
  normalised to `../<family>/Indicator-X.md`, each page's `Family` field is
  set to its new family, and two pre-existing `../Indicator-Chaining.md`
  links (should have been `../../`) are corrected. A link check confirms
  every relative wiki link resolves.
- Indicators-Overview.md fully rewritten around the eight families;
  Home.md indicator reference and the README family table follow suit.
- Warmup-Periods.md gains the eight F13 indicators; CHANGELOG records the
  46-indicator expansion (25 -> 71) and the eight-family taxonomy.
- Tests: Node indicators.test.js and Python test_new_indicators.py cover
  all eight new indicators (Node 91/91, Python 117/117 green).

cargo fmt + clippy (core/wickra/data/wasm/node) clean; 508 core tests,
25 data tests and 74 doctests green.
2026-05-22 21:21:56 +02:00

7.8 KiB
Raw Blame History

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_t

where 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) / period

When +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:

type Input  = Candle;
type Output = AdxOutput;
fn update(&mut self, candle: Candle) -> Option<AdxOutput>;

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 `

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 +DM always positive, DM always zero), +DI saturates at 100, DI at 0, and ADX climbs toward 100. The example below produces exactly that.
  • Flat market (no high/low movement). Every TR, +DM, DM is zero, so the divide-by-zero guards return +DI = DI = 0 and DX = 0; ADX then sits at 0 indefinitely.
  • Reset. reset() clears prev, all seed sums and counts, all smoothed values, the DX buffer, and adx_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=100

Python

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 < 20 is typically read as a ranging market; ADX > 25 as a "real" trend; ADX > 40 as a strong trend. ADX itself is direction-agnostic — you need +DI vs DI to know which way the trend points.
  • DI crossover. +DI crossing above DI is a bullish directional signal; the mirror is bearish. Many traders only act on a crossover when ADX > 25 to 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 +DI still dominates DI).

Common pitfalls

  • Long warmup. ADX needs 2 · period candles before the first emission — twice as many as most other Wilder indicators. A common bug is reusing an "RSI fits in period + 1 bars" mental model and reading garbage during the ADX warmup; check is_ready() or test for NaN on the adx column.
  • Plotted on the same axis as DI. +DI, DI, and ADX all live in [0, 100] and are typically overlaid. The crossover signal is between +DI and DI only — ADX does 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