4.6 KiB
ADX: Average Directional Index
"Is the market trending?" is the only question that matters. ADX answers it, loudly.
The Average Directional Index (ADX) is the industry-standard filter for trend strength. It ignores direction entirely, focusing solely on the velocity of price expansion. It allows systems to switch context: deploying trend-following logic when the market moves, and mean-reversion logic when it chops.
The 1978 Standard
J. Welles Wilder Jr. was a mechanical engineer, and it shows. Introduced in New Concepts in Technical Trading Systems (1978), the ADX is a machine built from moving parts. It doesn't just smooth price; it deconstructs range expansion, normalizes it against volatility, and then smooths the result twice.
It is not a modern, low-lag indicator. It is a heavy, momentum-based flywheel that takes time to spin up and time to spin down.
Architecture & Physics
The ADX is a "derivative of a derivative." The calculation pipeline is deep, which creates significant lag but offers exceptional noise reduction.
- Decomposition: We break price action into Directional Movement (+DM, -DM) and Volatility (True Range).
- Normalization: Raw movement is meaningless without context. We normalize DM by TR to get Directional Indicators (+DI, -DI).
- Oscillation: We derive the Directional Index (DX) from the ratio of the difference to the sum of the DIs.
- Smoothing: Finally, we smooth the DX to get ADX.
The Stability Problem
Because ADX relies on recursive smoothing (RMA) at multiple stages, it is notoriously slow to converge. A "cold" start requires at least 2 \times Period bars to produce data that even remotely resembles a mature series, and often 3-4 \times Period to match external libraries (like TA-Lib) within 4 decimal places.
Our implementation handles this by tracking the "warmup" state explicitly. We do not output garbage during the convergence phase if we can avoid it, but users must be aware that ADX is history-dependent.
Zero-Allocation Design
The calculation path is hot. We use stackalloc for internal buffers and struct-based state management. There are no new keywords in the update loop. The memory footprint is fixed at initialization: 48 bytes for the state struct and a small ring buffer for the period window.
Mathematical Foundation
The math is classic Wilder: recursive, stateful, and robust.
1. Directional Movement (DM)
We compare today's range to yesterday's.
\text{UpMove} = H_t - H_{t-1}
\text{DownMove} = L_{t-1} - L_t
+DM = \begin{cases} \text{UpMove} & \text{if } \text{UpMove} > \text{DownMove} \text{ and } \text{UpMove} > 0 \\ 0 & \text{otherwise} \end{cases}
-DM = \begin{cases} \text{DownMove} & \text{if } \text{DownMove} > \text{UpMove} \text{ and } \text{DownMove} > 0 \\ 0 & \text{otherwise} \end{cases}
2. Smoothing (RMA)
Wilder's Moving Average (RMA) is an exponential moving average with \alpha = 1/N. We smooth +DM, -DM, and TR (True Range).
+DM_{smoothed} = RMA(+DM, N)
-DM_{smoothed} = RMA(-DM, N)
TR_{smoothed} = RMA(TR, N)
3. Directional Indicators (DI)
+DI = 100 \times \frac{+DM_{smoothed}}{TR_{smoothed}}
-DI = 100 \times \frac{-DM_{smoothed}}{TR_{smoothed}}
4. The Index (DX and ADX)
DX = 100 \times \frac{|+DI - -DI|}{+DI + -DI}
ADX = RMA(DX, N)
Performance Profile
We optimize for throughput. The recursive nature of RMA allows for O(1) updates, but the initial calculation over a span requires O(N).
| Metric | Complexity | Notes |
|---|---|---|
| Throughput | 5ns / bar | Measured on Apple M1 Max, .NET 8.0 |
| Allocations | 0 bytes | Hot path is allocation-free |
| Complexity | O(1) | Streaming updates are constant time |
| Precision | double |
Necessary to prevent drift in recursive sums |
Validation
We validate against TA-Lib (the industry reference).
- Convergence: Matches TA-Lib to within
1e-9after ~100 bars of warmup. - Edge Cases: Handles
NaNinputs by carrying forward the last valid state, preventing the "poisoning" of the recursive chain. - Drift: Periodic re-summation is not required here as RMA is self-correcting over time, unlike simple accumulation.
Common Pitfalls
- Period Sensitivity: The standard period is 14. Lowering it (e.g., 7) makes ADX twitchy and prone to false positives. Raising it (e.g., 30) turns it into a geological indicator—accurate, but late.
- The "Turn": ADX peaks after the trend has exhausted. It is a lagging indicator of trend strength, not a leading indicator of price reversal.