1use std::fmt::{Debug, Display};
17
18use arraydeque::{ArrayDeque, Wrapping};
19use nautilus_model::{
20 data::{Bar, QuoteTick, TradeTick},
21 enums::PriceType,
22};
23
24use crate::indicator::Indicator;
25
26pub const MAX_PERIOD: usize = 1_024;
27const MAX_CAPACITY: usize = MAX_PERIOD + 1;
28
29const ROUND_DP: f64 = 1_000_000_000_000.0;
30
31#[repr(C)]
34#[derive(Debug)]
35#[cfg_attr(
36 feature = "python",
37 pyo3::pyclass(module = "nautilus_trader.indicators")
38)]
39#[cfg_attr(
40 feature = "python",
41 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.indicators")
42)]
43pub struct AroonOscillator {
44 pub period: usize,
45 pub aroon_up: f64,
46 pub aroon_down: f64,
47 pub value: f64,
48 pub count: usize,
49 pub initialized: bool,
50 has_inputs: bool,
51 total_count: usize,
52 high_inputs: ArrayDeque<f64, MAX_CAPACITY, Wrapping>,
53 low_inputs: ArrayDeque<f64, MAX_CAPACITY, Wrapping>,
54}
55
56impl Display for AroonOscillator {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 write!(f, "{}({})", self.name(), self.period)
59 }
60}
61
62impl Indicator for AroonOscillator {
63 fn name(&self) -> String {
64 stringify!(AroonOscillator).into()
65 }
66
67 fn has_inputs(&self) -> bool {
68 self.has_inputs
69 }
70
71 fn initialized(&self) -> bool {
72 self.initialized
73 }
74
75 fn handle_quote(&mut self, quote: &QuoteTick) -> anyhow::Result<()> {
76 let price = quote.extract_price(PriceType::Mid)?.into();
77 self.update_raw(price, price);
78 Ok(())
79 }
80
81 fn handle_trade(&mut self, trade: &TradeTick) {
82 let price: f64 = trade.price.into();
83 self.update_raw(price, price);
84 }
85
86 fn handle_bar(&mut self, bar: &Bar) {
87 let high: f64 = (&bar.high).into();
88 let low: f64 = (&bar.low).into();
89 self.update_raw(high, low);
90 }
91
92 fn reset(&mut self) {
93 self.high_inputs.clear();
94 self.low_inputs.clear();
95 self.aroon_up = 0.0;
96 self.aroon_down = 0.0;
97 self.value = 0.0;
98 self.count = 0;
99 self.total_count = 0;
100 self.has_inputs = false;
101 self.initialized = false;
102 }
103}
104
105impl AroonOscillator {
106 #[must_use]
112 pub fn new(period: usize) -> Self {
113 assert!(
114 period > 0,
115 "AroonOscillator: period must be > 0 (received {period})"
116 );
117 assert!(
118 period <= MAX_PERIOD,
119 "AroonOscillator: period must be ≤ {MAX_PERIOD} (received {period})"
120 );
121
122 Self {
123 period,
124 aroon_up: 0.0,
125 aroon_down: 0.0,
126 value: 0.0,
127 count: 0,
128 total_count: 0,
129 has_inputs: false,
130 initialized: false,
131 high_inputs: ArrayDeque::new(),
132 low_inputs: ArrayDeque::new(),
133 }
134 }
135
136 pub fn update_raw(&mut self, high: f64, low: f64) {
137 debug_assert!(
138 high >= low,
139 "AroonOscillator::update_raw - high must be ≥ low"
140 );
141
142 self.total_count = self.total_count.saturating_add(1);
143
144 if self.count == self.period + 1 {
145 let _ = self.high_inputs.pop_front();
146 let _ = self.low_inputs.pop_front();
147 } else {
148 self.count += 1;
149 }
150
151 let _ = self.high_inputs.push_back(high);
152 let _ = self.low_inputs.push_back(low);
153
154 let required = self.period + 1;
155 if !self.initialized && self.total_count >= required {
156 self.initialized = true;
157 }
158 self.has_inputs = true;
159
160 if self.initialized {
161 self.calculate_aroon();
162 }
163 }
164
165 fn calculate_aroon(&mut self) {
166 debug_assert_eq!(self.high_inputs.len(), self.period + 1);
167
168 let mut periods_since_high = 0_usize;
172 let mut max_val = f64::MIN;
173 for (periods_back, &hi) in self.high_inputs.iter().rev().enumerate() {
174 if hi > max_val {
175 max_val = hi;
176 periods_since_high = periods_back;
177 }
178 }
179
180 let mut periods_since_low = 0_usize;
181 let mut min_val = f64::MAX;
182 for (periods_back, &lo) in self.low_inputs.iter().rev().enumerate() {
183 if lo < min_val {
184 min_val = lo;
185 periods_since_low = periods_back;
186 }
187 }
188
189 self.aroon_up =
190 Self::round(100.0 * (self.period - periods_since_high) as f64 / self.period as f64);
191 self.aroon_down =
192 Self::round(100.0 * (self.period - periods_since_low) as f64 / self.period as f64);
193 self.value = Self::round(self.aroon_up - self.aroon_down);
194 }
195
196 #[inline]
197 fn round(v: f64) -> f64 {
198 (v * ROUND_DP).round() / ROUND_DP
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use rstest::rstest;
205
206 use super::*;
207 use crate::indicator::Indicator;
208
209 #[rstest]
210 fn test_name() {
211 let aroon = AroonOscillator::new(10);
212 assert_eq!(aroon.name(), "AroonOscillator");
213 }
214
215 #[rstest]
216 fn test_period() {
217 let aroon = AroonOscillator::new(10);
218 assert_eq!(aroon.period, 10);
219 }
220
221 #[rstest]
222 fn test_initialized_false() {
223 let aroon = AroonOscillator::new(10);
224 assert!(!aroon.initialized());
225 }
226
227 #[rstest]
228 fn test_initialized_true() {
229 let mut aroon = AroonOscillator::new(10);
230 for _ in 0..=10 {
231 aroon.update_raw(110.08, 109.61);
232 }
233 assert!(aroon.initialized());
234 }
235
236 #[rstest]
237 fn test_value_one_input() {
238 let mut aroon = AroonOscillator::new(1);
239 aroon.update_raw(110.08, 109.61);
240 assert_eq!(aroon.aroon_up, 0.0);
241 assert_eq!(aroon.aroon_down, 0.0);
242 assert_eq!(aroon.value, 0.0);
243 assert!(!aroon.initialized());
244 aroon.update_raw(110.10, 109.70);
245 assert!(aroon.initialized());
246 assert_eq!(aroon.aroon_up, 100.0);
247 assert_eq!(aroon.aroon_down, 0.0);
248 assert_eq!(aroon.value, 100.0);
249 }
250
251 #[rstest]
252 fn test_value_twenty_inputs() {
253 let mut aroon = AroonOscillator::new(20);
254 let inputs = [
255 (110.08, 109.61),
256 (110.15, 109.91),
257 (110.10, 109.73),
258 (110.06, 109.77),
259 (110.29, 109.88),
260 (110.53, 110.29),
261 (110.61, 110.26),
262 (110.28, 110.17),
263 (110.30, 110.00),
264 (110.25, 110.01),
265 (110.25, 109.81),
266 (109.92, 109.71),
267 (110.21, 109.84),
268 (110.08, 109.95),
269 (110.20, 109.96),
270 (110.16, 109.95),
271 (109.99, 109.75),
272 (110.20, 109.73),
273 (110.10, 109.81),
274 (110.04, 109.96),
275 (110.02, 109.90),
276 ];
277
278 for &(h, l) in &inputs {
279 aroon.update_raw(h, l);
280 }
281 assert!(aroon.initialized());
282 assert_eq!(aroon.aroon_up, 30.0);
283 assert_eq!(aroon.aroon_down, 0.0);
284 assert_eq!(aroon.value, 30.0);
285 }
286
287 #[rstest]
288 fn test_reset() {
289 let mut aroon = AroonOscillator::new(10);
290 for _ in 0..12 {
291 aroon.update_raw(110.08, 109.61);
292 }
293 aroon.reset();
294 assert!(!aroon.initialized());
295 assert_eq!(aroon.aroon_up, 0.0);
296 assert_eq!(aroon.aroon_down, 0.0);
297 assert_eq!(aroon.value, 0.0);
298 }
299
300 #[rstest]
301 fn test_initialized_boundary() {
302 let mut aroon = AroonOscillator::new(5);
303 for _ in 0..5 {
304 aroon.update_raw(1.0, 0.0);
305 assert!(!aroon.initialized());
306 }
307 aroon.update_raw(1.0, 0.0);
308 assert!(aroon.initialized());
309 }
310
311 #[rstest]
312 #[case(1, 0)]
313 #[case(5, 0)]
314 #[case(5, 2)]
315 #[case(10, 0)]
316 #[case(10, 9)]
317 fn test_formula_equivalence(#[case] period: usize, #[case] high_idx: usize) {
318 let mut aroon = AroonOscillator::new(period);
319 for idx in 0..=period {
320 let h = if idx == high_idx { 1_000.0 } else { idx as f64 };
321 aroon.update_raw(h, h);
322 }
323 assert!(aroon.initialized());
324 let expected = 100.0 * (high_idx as f64) / period as f64;
325 let diff = aroon.aroon_up - expected;
326 assert!(diff.abs() < 1e-6);
327 }
328
329 #[rstest]
330 fn test_window_size_period_plus_one() {
331 let period = 7;
332 let mut aroon = AroonOscillator::new(period);
333 for _ in 0..=period {
334 aroon.update_raw(1.0, 0.0);
335 }
336 assert_eq!(aroon.high_inputs.len(), period + 1);
337 assert_eq!(aroon.low_inputs.len(), period + 1);
338 }
339
340 #[rstest]
341 fn test_lowest_low_at_oldest_bar() {
342 let mut aroon = AroonOscillator::new(5);
343 aroon.update_raw(10.0, 0.0);
344 let inputs = [
345 (11.0, 9.0),
346 (12.0, 9.5),
347 (13.0, 9.2),
348 (14.0, 9.3),
349 (15.0, 9.4),
350 ];
351
352 for &(h, l) in &inputs {
353 aroon.update_raw(h, l);
354 }
355 assert!(aroon.initialized());
356 assert_eq!(aroon.aroon_up, 100.0);
357 assert_eq!(aroon.aroon_down, 0.0);
358 assert_eq!(aroon.value, 100.0);
359 }
360
361 #[rstest]
362 fn test_tie_favors_most_recent_occurrence() {
363 let mut aroon = AroonOscillator::new(4);
364 let inputs = [
365 (110.0, 100.0),
366 (110.0, 100.0),
367 (105.0, 101.0),
368 (105.0, 101.0),
369 (105.0, 101.0),
370 ];
371
372 for &(h, l) in &inputs {
373 aroon.update_raw(h, l);
374 }
375 assert!(aroon.initialized());
376 assert_eq!(aroon.aroon_up, 25.0);
379 assert_eq!(aroon.aroon_down, 25.0);
380 assert_eq!(aroon.value, 0.0);
381 }
382
383 #[rstest]
384 fn test_max_period_preserves_oldest_high_until_rollover() {
385 let mut aroon = AroonOscillator::new(MAX_PERIOD);
386
387 aroon.update_raw(1_000.0, 5.0);
388 for _ in 1..MAX_PERIOD {
389 aroon.update_raw(10.0, 1.0);
390 }
391
392 assert!(!aroon.initialized());
393 assert_eq!(aroon.count, MAX_PERIOD);
394
395 aroon.update_raw(10.0, 1.0);
396
397 assert!(aroon.initialized());
398 assert_eq!(aroon.count, MAX_PERIOD + 1);
399 assert_eq!(aroon.high_inputs.len(), MAX_PERIOD + 1);
400 assert_eq!(aroon.low_inputs.len(), MAX_PERIOD + 1);
401 assert_eq!(aroon.aroon_up, 0.0);
402 assert_eq!(aroon.aroon_down, 100.0);
403 assert_eq!(aroon.value, -100.0);
404
405 aroon.update_raw(10.0, 1.0);
406
407 assert_eq!(aroon.high_inputs.len(), MAX_PERIOD + 1);
408 assert_eq!(aroon.low_inputs.len(), MAX_PERIOD + 1);
409 assert_eq!(aroon.aroon_up, 100.0);
410 assert_eq!(aroon.aroon_down, 100.0);
411 assert_eq!(aroon.value, 0.0);
412 }
413
414 #[rstest]
415 fn test_max_period_preserves_oldest_low_until_rollover() {
416 let mut aroon = AroonOscillator::new(MAX_PERIOD);
417
418 aroon.update_raw(10.0, 0.0);
419 for _ in 1..MAX_PERIOD {
420 aroon.update_raw(10.0, 5.0);
421 }
422
423 assert!(!aroon.initialized());
424 assert_eq!(aroon.count, MAX_PERIOD);
425
426 aroon.update_raw(10.0, 5.0);
427
428 assert!(aroon.initialized());
429 assert_eq!(aroon.count, MAX_PERIOD + 1);
430 assert_eq!(aroon.high_inputs.len(), MAX_PERIOD + 1);
431 assert_eq!(aroon.low_inputs.len(), MAX_PERIOD + 1);
432 assert_eq!(aroon.aroon_up, 100.0);
433 assert_eq!(aroon.aroon_down, 0.0);
434 assert_eq!(aroon.value, 100.0);
435
436 aroon.update_raw(10.0, 5.0);
437
438 assert_eq!(aroon.high_inputs.len(), MAX_PERIOD + 1);
439 assert_eq!(aroon.low_inputs.len(), MAX_PERIOD + 1);
440 assert_eq!(aroon.aroon_up, 100.0);
441 assert_eq!(aroon.aroon_down, 100.0);
442 assert_eq!(aroon.value, 0.0);
443 }
444}