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        if period > crate::error::MAX_PERIOD {
66            return Err(Error::InvalidPeriod {
67                message: crate::error::PERIOD_ABOVE_MAX,
68            });
69        }
70        Ok(Self {
71            period,
72            adx: Adx::new(period)?,
73            window: VecDeque::with_capacity(period),
74            last: None,
75        })
76    }
77
78    /// Configured period.
79    pub const fn period(&self) -> usize {
80        self.period
81    }
82
83    /// Current value if available.
84    pub const fn value(&self) -> Option<f64> {
85        self.last
86    }
87}
88
89impl Indicator for Adxr {
90    type Input = Candle;
91    type Output = f64;
92
93    #[inline]
94    fn update(&mut self, candle: Candle) -> Option<f64> {
95        let adx_value = self.adx.update(candle)?.adx;
96        if self.window.len() == self.period {
97            self.window.pop_front();
98        }
99        self.window.push_back(adx_value);
100        if self.window.len() < self.period {
101            return None;
102        }
103        let oldest = *self.window.front().expect("ring is full");
104        let adxr = f64::midpoint(adx_value, oldest);
105        self.last = Some(adxr);
106        Some(adxr)
107    }
108
109    fn reset(&mut self) {
110        self.adx.reset();
111        self.window.clear();
112        self.last = None;
113    }
114
115    #[inline]
116    fn warmup_period(&self) -> usize {
117        // ADX warmup is `2 * period` and emits one `ADX` per subsequent candle;
118        // the ADXR ring then needs `period - 1` more candles to fill, so the
119        // first ADXR lands at `2 * period + (period - 1) = 3 * period - 1`.
120        3 * self.period - 1
121    }
122
123    #[inline]
124    fn is_ready(&self) -> bool {
125        self.last.is_some()
126    }
127
128    #[inline]
129    fn name(&self) -> &'static str {
130        "ADXR"
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use crate::traits::BatchExt;
138    use approx::assert_relative_eq;
139
140    fn candle(h: f64, l: f64, c: f64, ts: i64) -> Candle {
141        Candle::new(c, h, l, c, 1.0, ts).unwrap()
142    }
143
144    #[test]
145    fn rejects_zero_period() {
146        assert!(matches!(Adxr::new(0), Err(Error::PeriodZero)));
147    }
148
149    #[test]
150    fn accessors_and_metadata() {
151        let mut a = Adxr::new(14).unwrap();
152        assert_eq!(a.period(), 14);
153        assert_eq!(a.warmup_period(), 41);
154        assert_eq!(a.name(), "ADXR");
155        assert!(a.value().is_none());
156        // Drive past warmup.
157        for i in 0..50_i64 {
158            let base = 100.0 + (i as f64) * 2.0;
159            a.update(candle(base + 1.0, base - 0.5, base + 0.5, i));
160        }
161        assert!(a.value().is_some());
162    }
163
164    #[test]
165    fn pure_uptrend_yields_finite_positive_adxr() {
166        let candles: Vec<Candle> = (0..80_i64)
167            .map(|i| {
168                let base = 100.0 + (i as f64) * 2.0;
169                candle(base + 1.0, base - 0.5, base + 0.5, i)
170            })
171            .collect();
172        let mut a = Adxr::new(14).unwrap();
173        let last = a.batch(&candles).into_iter().flatten().last().unwrap();
174        assert!(last > 0.0 && last <= 100.0 + 1e-9);
175    }
176
177    #[test]
178    fn constant_series_yields_zero_adxr() {
179        let candles: Vec<Candle> = (0..50_i64).map(|i| candle(10.0, 10.0, 10.0, i)).collect();
180        let mut a = Adxr::new(5).unwrap();
181        let last = a.batch(&candles).into_iter().flatten().last().unwrap();
182        assert_eq!(last, 0.0);
183    }
184
185    #[test]
186    fn first_emission_at_warmup_period() {
187        let candles: Vec<Candle> = (0..80_i64)
188            .map(|i| {
189                let p = 100.0 + ((i as f64) * 0.3).sin() * 5.0;
190                candle(p + 1.0, p - 1.0, p, i)
191            })
192            .collect();
193        let mut a = Adxr::new(5).unwrap();
194        let out = a.batch(&candles);
195        let warmup = 3 * 5 - 1; // 14
196        for v in out.iter().take(warmup - 1) {
197            assert!(v.is_none());
198        }
199        assert!(out[warmup - 1].is_some());
200    }
201
202    #[test]
203    fn reference_value_against_explicit_adx_average() {
204        // The first ADXR(p) emits at index `3p - 2` (0-based), and equals
205        // (ADX[index] + ADX[index - (p - 1)]) / 2. Verify against a separate
206        // ADX run.
207        let candles: Vec<Candle> = (0..60_i64)
208            .map(|i| {
209                let p = 100.0 + ((i as f64) * 0.2).sin() * 6.0;
210                candle(p + 1.5, p - 1.5, p, i)
211            })
212            .collect();
213        let period = 5;
214        let mut adx = Adx::new(period).unwrap();
215        let adx_out: Vec<_> = adx
216            .batch(&candles)
217            .into_iter()
218            .map(|o| o.map(|x| x.adx))
219            .collect();
220        let mut adxr = Adxr::new(period).unwrap();
221        let adxr_out = adxr.batch(&candles);
222        // First ADXR index (0-based) = 3 * period - 2 = 13.
223        let first = 3 * period - 2;
224        let prev = first - (period - 1);
225        let expected = f64::midpoint(adx_out[first].unwrap(), adx_out[prev].unwrap());
226        assert_relative_eq!(adxr_out[first].unwrap(), expected, epsilon = 1e-12);
227    }
228
229    #[test]
230    fn batch_equals_streaming() {
231        let candles: Vec<Candle> = (0..60_i64)
232            .map(|i| {
233                let p = 100.0 + ((i as f64) * 0.25).sin() * 5.0;
234                candle(p + 1.0, p - 1.0, p, i)
235            })
236            .collect();
237        let mut a = Adxr::new(7).unwrap();
238        let mut b = Adxr::new(7).unwrap();
239        assert_eq!(
240            a.batch(&candles),
241            candles.iter().map(|c| b.update(*c)).collect::<Vec<_>>()
242        );
243    }
244
245    #[test]
246    fn reset_clears_state() {
247        let candles: Vec<Candle> = (0..60_i64).map(|i| candle(11.0, 9.0, 10.0, i)).collect();
248        let mut a = Adxr::new(5).unwrap();
249        a.batch(&candles);
250        assert!(a.is_ready());
251        a.reset();
252        assert!(!a.is_ready());
253        assert_eq!(a.update(candles[0]), None);
254    }
255}