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

245 lines
7.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`:
```rust
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 `|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 `+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
```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
```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
```javascript
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
- [Indicator: Rsi](../momentum-oscillators/Indicator-Rsi.md) — shares Wilder smoothing.
- [Indicator: Aroon](../trend-directional/Indicator-Aroon.md) — alternative trend-strength
measure, range-based.
- [Indicator: MacdIndicator](../trend-directional/Indicator-MacdIndicator.md) — trend-following
momentum, useful as a confirmation against `+DI` / `DI`.
- [Warmup Periods](../../Warmup-Periods.md) — the `2 · period` ADX entry.