Skip to main content

wickra_core/indicators/
adxr.rs

1//! Average Directional Movement Index Rating (ADXR).
2
3use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::indicators::adx::Adx;
7use crate::ohlcv::Candle;
8use crate::traits::Indicator;
9
10/// Wilder's Average Directional Movement Index Rating.
11///
12/// `ADXR` smooths the [`Adx`] line by averaging its current value with the value
13/// it had `period` bars ago:
14///
15/// ```text
16/// ADXR_t = (ADX_t + ADX_{t - (period - 1)}) / 2
17/// ```
18///
19/// The lookback length is the same `period` that feeds the underlying ADX.
20/// Wilder introduced ADXR alongside ADX in *New Concepts in Technical Trading
21/// Systems* (1978) as a more stable directional-strength reading: because the
22/// older `ADX` is `period - 1` bars stale, ADXR responds more slowly than ADX
23/// and is used to compare trend-strength between different instruments.
24///
25/// The first complete `ADXR` is emitted after `3 * period - 1` candles
26/// (`2 * period` to seed the ADX plus another `period - 1` to fill the
27/// lookback ring).
28///
29/// # Example
30///
31/// ```
32/// use wickra_core::{Adxr, Candle, Indicator};
33///
34/// let mut indicator = Adxr::new(5).unwrap();
35/// let mut last = None;
36/// for i in 0..80 {
37///     let base = 100.0 + f64::from(i);
38///     let candle =
39///         Candle::new(base, base + 2.0, base - 2.0, base + 1.0, 10.0, i64::from(i)).unwrap();
40///     last = indicator.update(candle);
41/// }
42/// assert!(last.is_some());
43/// ```
44#[derive(Debug, Clone)]
45pub struct Adxr {
46    period: usize,
47    adx: Adx,
48    /// Ring buffer of the most recent `period` `ADX` values; the front is the
49    /// oldest, the back is the newest. ADXR is `(back + front) / 2` once the
50    /// ring is full.
51    window: VecDeque<f64>,
52    last: Option<f64>,
53}
54
55impl Adxr {
56    /// Construct a new ADXR with the given Wilder smoothing period.
57    ///
58    /// # Errors
59    ///
60    /// Returns [`Error::PeriodZero`] if `period == 0`.
61    pub fn new(period: usize) -> Result<Self> {
62        if period == 0 {
63            return Err(Error::PeriodZero);
64        }
65        Ok(Self {
66            period,
67            adx: Adx::new(period)?,
68            window: VecDeque::with_capacity(period),
69            last: None,
70        })
71    }
72
73    /// Configured period.
74    pub const fn period(&self) -> usize {
75        self.period
76    }
77
78    /// Current value if available.
79    pub const fn value(&self) -> Option<f64> {
80        self.last
81    }
82}
83
84impl Indicator for Adxr {
85    type Input = Candle;
86    type Output = f64;
87
88    fn update(&mut self, candle: Candle) -> Option<f64> {
89        let adx_value = self.adx.update(candle)?.adx;
90        if self.window.len() == self.period {
91            self.window.pop_front();
92        }
93        self.window.push_back(adx_value);
94        if self.window.len() < self.period {
95            return None;
96        }
97        let oldest = *self.window.front().expect("ring is full");
98        let adxr = f64::midpoint(adx_value, oldest);
99        self.last = Some(adxr);
100        Some(adxr)
101    }
102
103    fn reset(&mut self) {
104        self.adx.reset();
105        self.window.clear();
106        self.last = None;
107    }
108
109    fn warmup_period(&self) -> usize {
110        // ADX warmup is `2 * period` and emits one `ADX` per subsequent candle;
111        // the ADXR ring then needs `period - 1` more candles to fill, so the
112        // first ADXR lands at `2 * period + (period - 1) = 3 * period - 1`.
113        3 * self.period - 1
114    }
115
116    fn is_ready(&self) -> bool {
117        self.last.is_some()
118    }
119
120    fn name(&self) -> &'static str {
121        "ADXR"
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    use crate::traits::BatchExt;
129    use approx::assert_relative_eq;
130
131    fn candle(h: f64, l: f64, c: f64, ts: i64) -> Candle {
132        Candle::new(c, h, l, c, 1.0, ts).unwrap()
133    }
134
135    #[test]
136    fn rejects_zero_period() {
137        assert!(matches!(Adxr::new(0), Err(Error::PeriodZero)));
138    }
139
140    #[test]
141    fn accessors_and_metadata() {
142        let mut a = Adxr::new(14).unwrap();
143        assert_eq!(a.period(), 14);
144        assert_eq!(a.warmup_period(), 41);
145        assert_eq!(a.name(), "ADXR");
146        assert!(a.value().is_none());
147        // Drive past warmup.
148        for i in 0..50_i64 {
149            let base = 100.0 + (i as f64) * 2.0;
150            a.update(candle(base + 1.0, base - 0.5, base + 0.5, i));
151        }
152        assert!(a.value().is_some());
153    }
154
155    #[test]
156    fn pure_uptrend_yields_finite_positive_adxr() {
157        let candles: Vec<Candle> = (0..80_i64)
158            .map(|i| {
159                let base = 100.0 + (i as f64) * 2.0;
160                candle(base + 1.0, base - 0.5, base + 0.5, i)
161            })
162            .collect();
163        let mut a = Adxr::new(14).unwrap();
164        let last = a.batch(&candles).into_iter().flatten().last().unwrap();
165        assert!(last > 0.0 && last <= 100.0 + 1e-9);
166    }
167
168    #[test]
169    fn constant_series_yields_zero_adxr() {
170        let candles: Vec<Candle> = (0..50_i64).map(|i| candle(10.0, 10.0, 10.0, i)).collect();
171        let mut a = Adxr::new(5).unwrap();
172        let last = a.batch(&candles).into_iter().flatten().last().unwrap();
173        assert_eq!(last, 0.0);
174    }
175
176    #[test]
177    fn first_emission_at_warmup_period() {
178        let candles: Vec<Candle> = (0..80_i64)
179            .map(|i| {
180                let p = 100.0 + ((i as f64) * 0.3).sin() * 5.0;
181                candle(p + 1.0, p - 1.0, p, i)
182            })
183            .collect();
184        let mut a = Adxr::new(5).unwrap();
185        let out = a.batch(&candles);
186        let warmup = 3 * 5 - 1; // 14
187        for v in out.iter().take(warmup - 1) {
188            assert!(v.is_none());
189        }
190        assert!(out[warmup - 1].is_some());
191    }
192
193    #[test]
194    fn reference_value_against_explicit_adx_average() {
195        // The first ADXR(p) emits at index `3p - 2` (0-based), and equals
196        // (ADX[index] + ADX[index - (p - 1)]) / 2. Verify against a separate
197        // ADX run.
198        let candles: Vec<Candle> = (0..60_i64)
199            .map(|i| {
200                let p = 100.0 + ((i as f64) * 0.2).sin() * 6.0;
201                candle(p + 1.5, p - 1.5, p, i)
202            })
203            .collect();
204        let period = 5;
205        let mut adx = Adx::new(period).unwrap();
206        let adx_out: Vec<_> = adx
207            .batch(&candles)
208            .into_iter()
209            .map(|o| o.map(|x| x.adx))
210            .collect();
211        let mut adxr = Adxr::new(period).unwrap();
212        let adxr_out = adxr.batch(&candles);
213        // First ADXR index (0-based) = 3 * period - 2 = 13.
214        let first = 3 * period - 2;
215        let prev = first - (period - 1);
216        let expected = f64::midpoint(adx_out[first].unwrap(), adx_out[prev].unwrap());
217        assert_relative_eq!(adxr_out[first].unwrap(), expected, epsilon = 1e-12);
218    }
219
220    #[test]
221    fn batch_equals_streaming() {
222        let candles: Vec<Candle> = (0..60_i64)
223            .map(|i| {
224                let p = 100.0 + ((i as f64) * 0.25).sin() * 5.0;
225                candle(p + 1.0, p - 1.0, p, i)
226            })
227            .collect();
228        let mut a = Adxr::new(7).unwrap();
229        let mut b = Adxr::new(7).unwrap();
230        assert_eq!(
231            a.batch(&candles),
232            candles.iter().map(|c| b.update(*c)).collect::<Vec<_>>()
233        );
234    }
235
236    #[test]
237    fn reset_clears_state() {
238        let candles: Vec<Candle> = (0..60_i64).map(|i| candle(11.0, 9.0, 10.0, i)).collect();
239        let mut a = Adxr::new(5).unwrap();
240        a.batch(&candles);
241        assert!(a.is_ready());
242        a.reset();
243        assert!(!a.is_ready());
244        assert_eq!(a.update(candles[0]), None);
245    }
246}