wickra_core/indicators/
aroon_oscillator.rs1use crate::error::Result;
4use crate::ohlcv::Candle;
5use crate::traits::Indicator;
6
7use super::Aroon;
8
9#[derive(Debug, Clone)]
38pub struct AroonOscillator {
39 aroon: Aroon,
40 last: Option<f64>,
41}
42
43impl AroonOscillator {
44 pub fn new(period: usize) -> Result<Self> {
50 Ok(Self {
51 aroon: Aroon::new(period)?,
52 last: None,
53 })
54 }
55
56 pub const fn period(&self) -> usize {
58 self.aroon.period()
59 }
60
61 pub const fn value(&self) -> Option<f64> {
63 self.last
64 }
65}
66
67impl Indicator for AroonOscillator {
68 type Input = Candle;
69 type Output = f64;
70
71 #[inline]
72 fn update(&mut self, candle: Candle) -> Option<f64> {
73 let osc = self.aroon.update(candle).map(|o| o.up - o.down)?;
74 self.last = Some(osc);
75 Some(osc)
76 }
77
78 fn reset(&mut self) {
79 self.aroon.reset();
80 self.last = None;
81 }
82
83 #[inline]
84 fn warmup_period(&self) -> usize {
85 self.aroon.warmup_period()
86 }
87
88 #[inline]
89 fn is_ready(&self) -> bool {
90 self.last.is_some()
91 }
92
93 #[inline]
94 fn name(&self) -> &'static str {
95 "AroonOscillator"
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102 use crate::traits::BatchExt;
103 use approx::assert_relative_eq;
104
105 fn candle(high: f64, low: f64, close: f64, ts: i64) -> Candle {
106 Candle::new(close, high, low, close, 1.0, ts).unwrap()
107 }
108
109 #[test]
110 fn new_rejects_zero_period() {
111 assert!(AroonOscillator::new(0).is_err());
112 }
113
114 #[test]
118 fn accessors_and_metadata() {
119 let mut osc = AroonOscillator::new(7).unwrap();
120 assert_eq!(osc.period(), 7);
121 assert_eq!(osc.name(), "AroonOscillator");
122 assert_eq!(osc.value(), None);
123 for i in 0..8 {
124 osc.update(candle(100.0 + f64::from(i), 90.0, 95.0, i64::from(i)));
125 }
126 assert!(osc.value().is_some());
127 }
128
129 #[test]
130 fn pure_uptrend_yields_plus_100() {
131 let mut osc = AroonOscillator::new(5).unwrap();
133 let candles: Vec<Candle> = (0..30)
134 .map(|i| {
135 let p = 100.0 + i as f64;
136 candle(p + 1.0, p - 1.0, p, i)
137 })
138 .collect();
139 for v in osc.batch(&candles).into_iter().flatten() {
140 assert_relative_eq!(v, 100.0, epsilon = 1e-12);
141 }
142 }
143
144 #[test]
145 fn pure_downtrend_yields_minus_100() {
146 let mut osc = AroonOscillator::new(5).unwrap();
147 let candles: Vec<Candle> = (0..30)
148 .map(|i| {
149 let p = 100.0 - i as f64;
150 candle(p + 1.0, p - 1.0, p, i)
151 })
152 .collect();
153 for v in osc.batch(&candles).into_iter().flatten() {
154 assert_relative_eq!(v, -100.0, epsilon = 1e-12);
155 }
156 }
157
158 #[test]
159 fn output_stays_within_minus_100_and_100() {
160 let mut osc = AroonOscillator::new(14).unwrap();
161 let candles: Vec<Candle> = (0..200)
162 .map(|i| {
163 let mid = 100.0 + (i as f64 * 0.25).sin() * 12.0;
164 candle(mid + 2.0, mid - 2.0, mid, i)
165 })
166 .collect();
167 for v in osc.batch(&candles).into_iter().flatten() {
168 assert!((-100.0..=100.0).contains(&v), "out of range: {v}");
169 }
170 }
171
172 #[test]
173 fn warmup_period_matches_aroon() {
174 let osc = AroonOscillator::new(7).unwrap();
175 assert_eq!(osc.warmup_period(), 8);
176 }
177
178 #[test]
179 fn reset_clears_state() {
180 let mut osc = AroonOscillator::new(5).unwrap();
181 let candles: Vec<Candle> = (0..20)
182 .map(|i| candle(100.0 + i as f64, 90.0, 95.0, i))
183 .collect();
184 osc.batch(&candles);
185 assert!(osc.is_ready());
186 osc.reset();
187 assert!(!osc.is_ready());
188 assert_eq!(osc.update(candles[0]), None);
189 }
190
191 #[test]
192 fn batch_equals_streaming() {
193 let candles: Vec<Candle> = (0..60)
194 .map(|i| {
195 let mid = 100.0 + (i as f64 * 0.3).sin() * 8.0;
196 candle(mid + 2.0, mid - 2.0, mid, i)
197 })
198 .collect();
199 let batch = AroonOscillator::new(14).unwrap().batch(&candles);
200 let mut b = AroonOscillator::new(14).unwrap();
201 let streamed: Vec<_> = candles.iter().map(|c| b.update(*c)).collect();
202 assert_eq!(batch, streamed);
203 }
204}