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        Ok(Self {
50            period,
51            candles: VecDeque::with_capacity(period + 1),
52        })
53    }
54
55    /// Configured period.
56    pub const fn period(&self) -> usize {
57        self.period
58    }
59}
60
61impl Indicator for Aroon {
62    type Input = Candle;
63    type Output = AroonOutput;
64
65    fn update(&mut self, candle: Candle) -> Option<AroonOutput> {
66        if self.candles.len() == self.period + 1 {
67            self.candles.pop_front();
68        }
69        self.candles.push_back(candle);
70        if self.candles.len() < self.period + 1 {
71            return None;
72        }
73        // Find the index (0 = oldest) of the highest high and lowest low.
74        let (mut hh_idx, mut ll_idx) = (0_usize, 0_usize);
75        let (mut hh, mut ll) = (f64::NEG_INFINITY, f64::INFINITY);
76        for (i, c) in self.candles.iter().enumerate() {
77            if c.high >= hh {
78                hh = c.high;
79                hh_idx = i;
80            }
81            if c.low <= ll {
82                ll = c.low;
83                ll_idx = i;
84            }
85        }
86        let n = self.period as f64;
87        let up = 100.0 * hh_idx as f64 / n;
88        let down = 100.0 * ll_idx as f64 / n;
89        Some(AroonOutput { up, down })
90    }
91
92    fn reset(&mut self) {
93        self.candles.clear();
94    }
95
96    fn warmup_period(&self) -> usize {
97        self.period + 1
98    }
99
100    fn is_ready(&self) -> bool {
101        self.candles.len() == self.period + 1
102    }
103
104    fn name(&self) -> &'static str {
105        "Aroon"
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use crate::traits::BatchExt;
113    use approx::assert_relative_eq;
114
115    fn c(h: f64, l: f64, cl: f64) -> Candle {
116        Candle::new(cl, h, l, cl, 1.0, 0).unwrap()
117    }
118
119    #[test]
120    fn pure_uptrend_aroon_up_100() {
121        let candles: Vec<Candle> = (1..=15)
122            .map(|i| c(f64::from(i) + 1.0, f64::from(i) - 1.0, f64::from(i)))
123            .collect();
124        let mut a = Aroon::new(14).unwrap();
125        let last = a.batch(&candles).into_iter().flatten().last().unwrap();
126        assert_relative_eq!(last.up, 100.0, epsilon = 1e-9);
127        // The lowest low is at the oldest position (index 0).
128        assert_relative_eq!(last.down, 0.0, epsilon = 1e-9);
129    }
130
131    #[test]
132    fn pure_downtrend_aroon_down_100() {
133        let candles: Vec<Candle> = (1..=15)
134            .rev()
135            .map(|i| c(f64::from(i) + 1.0, f64::from(i) - 1.0, f64::from(i)))
136            .collect();
137        let mut a = Aroon::new(14).unwrap();
138        let last = a.batch(&candles).into_iter().flatten().last().unwrap();
139        assert_relative_eq!(last.down, 100.0, epsilon = 1e-9);
140    }
141
142    #[test]
143    fn batch_equals_streaming() {
144        let candles: Vec<Candle> = (0..40)
145            .map(|i| {
146                let m = 50.0 + (f64::from(i) * 0.3).sin() * 5.0;
147                c(m + 1.0, m - 1.0, m)
148            })
149            .collect();
150        let mut a = Aroon::new(14).unwrap();
151        let mut b = Aroon::new(14).unwrap();
152        assert_eq!(
153            a.batch(&candles),
154            candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
155        );
156    }
157
158    #[test]
159    fn outputs_in_range() {
160        let candles: Vec<Candle> = (0..200)
161            .map(|i| {
162                let m = 50.0 + (f64::from(i) * 0.2).sin() * 5.0;
163                c(m + 1.0, m - 1.0, m)
164            })
165            .collect();
166        let mut a = Aroon::new(14).unwrap();
167        for o in a.batch(&candles).into_iter().flatten() {
168            assert!((0.0..=100.0).contains(&o.up));
169            assert!((0.0..=100.0).contains(&o.down));
170        }
171    }
172
173    #[test]
174    fn reset_clears_state() {
175        let candles: Vec<Candle> = (1..=20)
176            .map(|i| c(f64::from(i) + 1.0, f64::from(i) - 1.0, f64::from(i)))
177            .collect();
178        let mut a = Aroon::new(14).unwrap();
179        a.batch(&candles);
180        assert!(a.is_ready());
181        a.reset();
182        assert!(!a.is_ready());
183        assert_eq!(a.update(candles[0]), None);
184    }
185
186    /// Cover the const accessor `period` (56-58) and the Indicator-impl
187    /// `name` body (104-106). `warmup_period` is exercised elsewhere.
188    #[test]
189    fn accessors_and_metadata() {
190        let a = Aroon::new(14).unwrap();
191        assert_eq!(a.period(), 14);
192        assert_eq!(a.name(), "Aroon");
193    }
194}