Skip to main content

wickra_core/indicators/
aroon.rs

1//! Aroon Up / Down indicator.
2
3use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::ohlcv::Candle;
7use crate::traits::Indicator;
8
9/// Aroon output: up and down strengths in [0, 100].
10#[derive(Debug, Clone, Copy, PartialEq)]
11pub struct AroonOutput {
12    /// Time since the highest high, expressed as a percentage of the window.
13    pub up: f64,
14    /// Time since the lowest low, same convention.
15    pub down: f64,
16}
17
18/// Aroon indicator: tracks how many bars since the highest high and lowest low
19/// inside a `period + 1`-bar window. Returned as a percentage.
20///
21/// # Example
22///
23/// ```
24/// use wickra_core::{Candle, Indicator, Aroon};
25///
26/// let mut indicator = Aroon::new(5).unwrap();
27/// let mut last = None;
28/// for i in 0..80 {
29///     let base = 100.0 + f64::from(i);
30///     let candle =
31///         Candle::new(base, base + 2.0, base - 2.0, base + 1.0, 10.0, i64::from(i)).unwrap();
32///     last = indicator.update(candle);
33/// }
34/// assert!(last.is_some());
35/// ```
36#[derive(Debug, Clone)]
37pub struct Aroon {
38    period: usize,
39    candles: VecDeque<Candle>,
40}
41
42impl Aroon {
43    /// # Errors
44    /// Returns [`Error::PeriodZero`] if `period == 0`.
45    pub fn new(period: usize) -> Result<Self> {
46        if period == 0 {
47            return Err(Error::PeriodZero);
48        }
49        if period > crate::error::MAX_PERIOD {
50            return Err(Error::InvalidPeriod {
51                message: crate::error::PERIOD_ABOVE_MAX,
52            });
53        }
54        Ok(Self {
55            period,
56            candles: VecDeque::with_capacity(period + 1),
57        })
58    }
59
60    /// Configured period.
61    pub const fn period(&self) -> usize {
62        self.period
63    }
64}
65
66impl Indicator for Aroon {
67    type Input = Candle;
68    type Output = AroonOutput;
69
70    #[inline]
71    fn update(&mut self, candle: Candle) -> Option<AroonOutput> {
72        if self.candles.len() == self.period + 1 {
73            self.candles.pop_front();
74        }
75        self.candles.push_back(candle);
76        if self.candles.len() < self.period + 1 {
77            return None;
78        }
79        // Find the index (0 = oldest) of the highest high and lowest low.
80        let (mut hh_idx, mut ll_idx) = (0_usize, 0_usize);
81        let (mut hh, mut ll) = (f64::NEG_INFINITY, f64::INFINITY);
82        for (i, c) in self.candles.iter().enumerate() {
83            if c.high >= hh {
84                hh = c.high;
85                hh_idx = i;
86            }
87            if c.low <= ll {
88                ll = c.low;
89                ll_idx = i;
90            }
91        }
92        let n = self.period as f64;
93        let up = 100.0 * hh_idx as f64 / n;
94        let down = 100.0 * ll_idx as f64 / n;
95        Some(AroonOutput { up, down })
96    }
97
98    fn reset(&mut self) {
99        self.candles.clear();
100    }
101
102    #[inline]
103    fn warmup_period(&self) -> usize {
104        self.period + 1
105    }
106
107    #[inline]
108    fn is_ready(&self) -> bool {
109        self.candles.len() == self.period + 1
110    }
111
112    #[inline]
113    fn name(&self) -> &'static str {
114        "Aroon"
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use crate::traits::BatchExt;
122    use approx::assert_relative_eq;
123
124    fn c(h: f64, l: f64, cl: f64) -> Candle {
125        Candle::new(cl, h, l, cl, 1.0, 0).unwrap()
126    }
127
128    #[test]
129    fn pure_uptrend_aroon_up_100() {
130        let candles: Vec<Candle> = (1..=15)
131            .map(|i| c(f64::from(i) + 1.0, f64::from(i) - 1.0, f64::from(i)))
132            .collect();
133        let mut a = Aroon::new(14).unwrap();
134        let last = a.batch(&candles).into_iter().flatten().last().unwrap();
135        assert_relative_eq!(last.up, 100.0, epsilon = 1e-9);
136        // The lowest low is at the oldest position (index 0).
137        assert_relative_eq!(last.down, 0.0, epsilon = 1e-9);
138    }
139
140    #[test]
141    fn pure_downtrend_aroon_down_100() {
142        let candles: Vec<Candle> = (1..=15)
143            .rev()
144            .map(|i| c(f64::from(i) + 1.0, f64::from(i) - 1.0, f64::from(i)))
145            .collect();
146        let mut a = Aroon::new(14).unwrap();
147        let last = a.batch(&candles).into_iter().flatten().last().unwrap();
148        assert_relative_eq!(last.down, 100.0, epsilon = 1e-9);
149    }
150
151    #[test]
152    fn batch_equals_streaming() {
153        let candles: Vec<Candle> = (0..40)
154            .map(|i| {
155                let m = 50.0 + (f64::from(i) * 0.3).sin() * 5.0;
156                c(m + 1.0, m - 1.0, m)
157            })
158            .collect();
159        let mut a = Aroon::new(14).unwrap();
160        let mut b = Aroon::new(14).unwrap();
161        assert_eq!(
162            a.batch(&candles),
163            candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
164        );
165    }
166
167    #[test]
168    fn outputs_in_range() {
169        let candles: Vec<Candle> = (0..200)
170            .map(|i| {
171                let m = 50.0 + (f64::from(i) * 0.2).sin() * 5.0;
172                c(m + 1.0, m - 1.0, m)
173            })
174            .collect();
175        let mut a = Aroon::new(14).unwrap();
176        for o in a.batch(&candles).into_iter().flatten() {
177            assert!((0.0..=100.0).contains(&o.up));
178            assert!((0.0..=100.0).contains(&o.down));
179        }
180    }
181
182    #[test]
183    fn reset_clears_state() {
184        let candles: Vec<Candle> = (1..=20)
185            .map(|i| c(f64::from(i) + 1.0, f64::from(i) - 1.0, f64::from(i)))
186            .collect();
187        let mut a = Aroon::new(14).unwrap();
188        a.batch(&candles);
189        assert!(a.is_ready());
190        a.reset();
191        assert!(!a.is_ready());
192        assert_eq!(a.update(candles[0]), None);
193    }
194
195    /// Cover the const accessor `period` (56-58) and the Indicator-impl
196    /// `name` body (104-106). `warmup_period` is exercised elsewhere.
197    #[test]
198    fn accessors_and_metadata() {
199        let a = Aroon::new(14).unwrap();
200        assert_eq!(a.period(), 14);
201        assert_eq!(a.name(), "Aroon");
202    }
203}