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 fn update(&mut self, candle: Candle) -> Option<f64> {
72 let osc = self.aroon.update(candle).map(|o| o.up - o.down)?;
73 self.last = Some(osc);
74 Some(osc)
75 }
76
77 fn reset(&mut self) {
78 self.aroon.reset();
79 self.last = None;
80 }
81
82 fn warmup_period(&self) -> usize {
83 self.aroon.warmup_period()
84 }
85
86 fn is_ready(&self) -> bool {
87 self.last.is_some()
88 }
89
90 fn name(&self) -> &'static str {
91 "AroonOscillator"
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98 use crate::traits::BatchExt;
99 use approx::assert_relative_eq;
100
101 fn candle(high: f64, low: f64, close: f64, ts: i64) -> Candle {
102 Candle::new(close, high, low, close, 1.0, ts).unwrap()
103 }
104
105 #[test]
106 fn new_rejects_zero_period() {
107 assert!(AroonOscillator::new(0).is_err());
108 }
109
110 #[test]
114 fn accessors_and_metadata() {
115 let mut osc = AroonOscillator::new(7).unwrap();
116 assert_eq!(osc.period(), 7);
117 assert_eq!(osc.name(), "AroonOscillator");
118 assert_eq!(osc.value(), None);
119 for i in 0..8 {
120 osc.update(candle(100.0 + f64::from(i), 90.0, 95.0, i64::from(i)));
121 }
122 assert!(osc.value().is_some());
123 }
124
125 #[test]
126 fn pure_uptrend_yields_plus_100() {
127 let mut osc = AroonOscillator::new(5).unwrap();
129 let candles: Vec<Candle> = (0..30)
130 .map(|i| {
131 let p = 100.0 + i as f64;
132 candle(p + 1.0, p - 1.0, p, i)
133 })
134 .collect();
135 for v in osc.batch(&candles).into_iter().flatten() {
136 assert_relative_eq!(v, 100.0, epsilon = 1e-12);
137 }
138 }
139
140 #[test]
141 fn pure_downtrend_yields_minus_100() {
142 let mut osc = AroonOscillator::new(5).unwrap();
143 let candles: Vec<Candle> = (0..30)
144 .map(|i| {
145 let p = 100.0 - i as f64;
146 candle(p + 1.0, p - 1.0, p, i)
147 })
148 .collect();
149 for v in osc.batch(&candles).into_iter().flatten() {
150 assert_relative_eq!(v, -100.0, epsilon = 1e-12);
151 }
152 }
153
154 #[test]
155 fn output_stays_within_minus_100_and_100() {
156 let mut osc = AroonOscillator::new(14).unwrap();
157 let candles: Vec<Candle> = (0..200)
158 .map(|i| {
159 let mid = 100.0 + (i as f64 * 0.25).sin() * 12.0;
160 candle(mid + 2.0, mid - 2.0, mid, i)
161 })
162 .collect();
163 for v in osc.batch(&candles).into_iter().flatten() {
164 assert!((-100.0..=100.0).contains(&v), "out of range: {v}");
165 }
166 }
167
168 #[test]
169 fn warmup_period_matches_aroon() {
170 let osc = AroonOscillator::new(7).unwrap();
171 assert_eq!(osc.warmup_period(), 8);
172 }
173
174 #[test]
175 fn reset_clears_state() {
176 let mut osc = AroonOscillator::new(5).unwrap();
177 let candles: Vec<Candle> = (0..20)
178 .map(|i| candle(100.0 + i as f64, 90.0, 95.0, i))
179 .collect();
180 osc.batch(&candles);
181 assert!(osc.is_ready());
182 osc.reset();
183 assert!(!osc.is_ready());
184 assert_eq!(osc.update(candles[0]), None);
185 }
186
187 #[test]
188 fn batch_equals_streaming() {
189 let candles: Vec<Candle> = (0..60)
190 .map(|i| {
191 let mid = 100.0 + (i as f64 * 0.3).sin() * 8.0;
192 candle(mid + 2.0, mid - 2.0, mid, i)
193 })
194 .collect();
195 let batch = AroonOscillator::new(14).unwrap().batch(&candles);
196 let mut b = AroonOscillator::new(14).unwrap();
197 let streamed: Vec<_> = candles.iter().map(|c| b.update(*c)).collect();
198 assert_eq!(batch, streamed);
199 }
200}