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 Ok(Self {
50 period,
51 candles: VecDeque::with_capacity(period + 1),
52 })
53 }
54
55 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 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 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 #[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}