wickra_core/indicators/
aroon.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::ohlcv::Candle;
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone, Copy, PartialEq)]
11pub struct AroonOutput {
12 pub up: f64,
14 pub down: f64,
16}
17
18#[derive(Debug, Clone)]
37pub struct Aroon {
38 period: usize,
39 candles: VecDeque<Candle>,
40}
41
42impl Aroon {
43 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 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 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 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 #[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}