Skip to main content

nautilus_indicators/momentum/
aroon.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use 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/// The Aroon Oscillator calculates the Aroon Up and Aroon Down indicators to
32/// determine if an instrument is trending, and the strength of the trend.
33#[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    /// Creates a new [`AroonOscillator`] instance.
107    ///
108    /// # Panics
109    ///
110    /// Panics if `period` is not positive (> 0).
111    #[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        // Scan the full window from newest to oldest so the enumeration index
169        // is the periods-since count directly and ties resolve to the most
170        // recent occurrence of the extreme.
171        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        // Highest high and lowest low both occur twice; the most recent
377        // occurrence (index 1, three periods back) determines the counts
378        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}