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.
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user