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;
27
28const ROUND_DP: f64 = 1_000_000_000_000.0;
29
30/// The Aroon Oscillator calculates the Aroon Up and Aroon Down indicators to
31/// determine if an instrument is trending, and the strength of the trend.
32#[repr(C)]
33#[derive(Debug)]
34#[cfg_attr(
35    feature = "python",
36    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.indicators")
37)]
38#[cfg_attr(
39    feature = "python",
40    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.indicators")
41)]
42pub struct AroonOscillator {
43    pub period: usize,
44    pub aroon_up: f64,
45    pub aroon_down: f64,
46    pub value: f64,
47    pub count: usize,
48    pub initialized: bool,
49    has_inputs: bool,
50    total_count: usize,
51    high_inputs: ArrayDeque<f64, MAX_PERIOD, Wrapping>,
52    low_inputs: ArrayDeque<f64, MAX_PERIOD, Wrapping>,
53}
54
55impl Display for AroonOscillator {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        write!(f, "{}({})", self.name(), self.period)
58    }
59}
60
61impl Indicator for AroonOscillator {
62    fn name(&self) -> String {
63        stringify!(AroonOscillator).into()
64    }
65
66    fn has_inputs(&self) -> bool {
67        self.has_inputs
68    }
69
70    fn initialized(&self) -> bool {
71        self.initialized
72    }
73
74    fn handle_quote(&mut self, quote: &QuoteTick) {
75        let price = quote.extract_price(PriceType::Mid).into();
76        self.update_raw(price, price);
77    }
78
79    fn handle_trade(&mut self, trade: &TradeTick) {
80        let price: f64 = trade.price.into();
81        self.update_raw(price, price);
82    }
83
84    fn handle_bar(&mut self, bar: &Bar) {
85        let high: f64 = (&bar.high).into();
86        let low: f64 = (&bar.low).into();
87        self.update_raw(high, low);
88    }
89
90    fn reset(&mut self) {
91        self.high_inputs.clear();
92        self.low_inputs.clear();
93        self.aroon_up = 0.0;
94        self.aroon_down = 0.0;
95        self.value = 0.0;
96        self.count = 0;
97        self.total_count = 0;
98        self.has_inputs = false;
99        self.initialized = false;
100    }
101}
102
103impl AroonOscillator {
104    /// Creates a new [`AroonOscillator`] instance.
105    ///
106    /// # Panics
107    ///
108    /// Panics if `period` is not positive (> 0).
109    #[must_use]
110    pub fn new(period: usize) -> Self {
111        assert!(
112            period > 0,
113            "AroonOscillator: period must be > 0 (received {period})"
114        );
115        assert!(
116            period <= MAX_PERIOD,
117            "AroonOscillator: period must be ≤ {MAX_PERIOD} (received {period})"
118        );
119
120        Self {
121            period,
122            aroon_up: 0.0,
123            aroon_down: 0.0,
124            value: 0.0,
125            count: 0,
126            total_count: 0,
127            has_inputs: false,
128            initialized: false,
129            high_inputs: ArrayDeque::new(),
130            low_inputs: ArrayDeque::new(),
131        }
132    }
133
134    pub fn update_raw(&mut self, high: f64, low: f64) {
135        debug_assert!(
136            high >= low,
137            "AroonOscillator::update_raw - high must be ≥ low"
138        );
139
140        self.total_count = self.total_count.saturating_add(1);
141
142        if self.count == self.period + 1 {
143            let _ = self.high_inputs.pop_front();
144            let _ = self.low_inputs.pop_front();
145        } else {
146            self.count += 1;
147        }
148
149        let _ = self.high_inputs.push_back(high);
150        let _ = self.low_inputs.push_back(low);
151
152        let required = self.period + 1;
153        if !self.initialized && self.total_count >= required {
154            self.initialized = true;
155        }
156        self.has_inputs = true;
157
158        if self.initialized {
159            self.calculate_aroon();
160        }
161    }
162
163    fn calculate_aroon(&mut self) {
164        let len = self.high_inputs.len();
165        debug_assert!(len == self.period + 1);
166
167        let mut max_idx = 0_usize;
168        let mut max_val = f64::MIN;
169        for (idx, &hi) in self.high_inputs.iter().enumerate() {
170            if hi > max_val {
171                max_val = hi;
172                max_idx = idx;
173            }
174        }
175
176        let mut min_idx_rel = 0_usize;
177        let mut min_val = f64::MAX;
178        for (idx, &lo) in self.low_inputs.iter().skip(1).enumerate() {
179            if lo < min_val {
180                min_val = lo;
181                min_idx_rel = idx;
182            }
183        }
184
185        let periods_since_high = len - 1 - max_idx;
186        let periods_since_low = self.period - 1 - min_idx_rel;
187
188        self.aroon_up =
189            Self::round(100.0 * (self.period - periods_since_high) as f64 / self.period as f64);
190        self.aroon_down =
191            Self::round(100.0 * (self.period - periods_since_low) as f64 / self.period as f64);
192        self.value = Self::round(self.aroon_up - self.aroon_down);
193    }
194
195    #[inline]
196    fn round(v: f64) -> f64 {
197        (v * ROUND_DP).round() / ROUND_DP
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use rstest::rstest;
204
205    use super::*;
206    use crate::indicator::Indicator;
207
208    #[rstest]
209    fn test_name() {
210        let aroon = AroonOscillator::new(10);
211        assert_eq!(aroon.name(), "AroonOscillator");
212    }
213
214    #[rstest]
215    fn test_period() {
216        let aroon = AroonOscillator::new(10);
217        assert_eq!(aroon.period, 10);
218    }
219
220    #[rstest]
221    fn test_initialized_false() {
222        let aroon = AroonOscillator::new(10);
223        assert!(!aroon.initialized());
224    }
225
226    #[rstest]
227    fn test_initialized_true() {
228        let mut aroon = AroonOscillator::new(10);
229        for _ in 0..=10 {
230            aroon.update_raw(110.08, 109.61);
231        }
232        assert!(aroon.initialized());
233    }
234
235    #[rstest]
236    fn test_value_one_input() {
237        let mut aroon = AroonOscillator::new(1);
238        aroon.update_raw(110.08, 109.61);
239        assert_eq!(aroon.aroon_up, 0.0);
240        assert_eq!(aroon.aroon_down, 0.0);
241        assert_eq!(aroon.value, 0.0);
242        assert!(!aroon.initialized());
243        aroon.update_raw(110.10, 109.70);
244        assert!(aroon.initialized());
245        assert_eq!(aroon.aroon_up, 100.0);
246        assert_eq!(aroon.aroon_down, 100.0);
247        assert_eq!(aroon.value, 0.0);
248    }
249
250    #[rstest]
251    fn test_value_twenty_inputs() {
252        let mut aroon = AroonOscillator::new(20);
253        let inputs = [
254            (110.08, 109.61),
255            (110.15, 109.91),
256            (110.10, 109.73),
257            (110.06, 109.77),
258            (110.29, 109.88),
259            (110.53, 110.29),
260            (110.61, 110.26),
261            (110.28, 110.17),
262            (110.30, 110.00),
263            (110.25, 110.01),
264            (110.25, 109.81),
265            (109.92, 109.71),
266            (110.21, 109.84),
267            (110.08, 109.95),
268            (110.20, 109.96),
269            (110.16, 109.95),
270            (109.99, 109.75),
271            (110.20, 109.73),
272            (110.10, 109.81),
273            (110.04, 109.96),
274            (110.02, 109.90),
275        ];
276
277        for &(h, l) in &inputs {
278            aroon.update_raw(h, l);
279        }
280        assert!(aroon.initialized());
281        assert_eq!(aroon.aroon_up, 30.0);
282        assert_eq!(aroon.value, -25.0);
283    }
284
285    #[rstest]
286    fn test_reset() {
287        let mut aroon = AroonOscillator::new(10);
288        for _ in 0..12 {
289            aroon.update_raw(110.08, 109.61);
290        }
291        aroon.reset();
292        assert!(!aroon.initialized());
293        assert_eq!(aroon.aroon_up, 0.0);
294        assert_eq!(aroon.aroon_down, 0.0);
295        assert_eq!(aroon.value, 0.0);
296    }
297
298    #[rstest]
299    fn test_initialized_boundary() {
300        let mut aroon = AroonOscillator::new(5);
301        for _ in 0..5 {
302            aroon.update_raw(1.0, 0.0);
303            assert!(!aroon.initialized());
304        }
305        aroon.update_raw(1.0, 0.0);
306        assert!(aroon.initialized());
307    }
308
309    #[rstest]
310    #[case(1, 0)]
311    #[case(5, 0)]
312    #[case(5, 2)]
313    #[case(10, 0)]
314    #[case(10, 9)]
315    fn test_formula_equivalence(#[case] period: usize, #[case] high_idx: usize) {
316        let mut aroon = AroonOscillator::new(period);
317        for idx in 0..=period {
318            let h = if idx == high_idx { 1_000.0 } else { idx as f64 };
319            aroon.update_raw(h, h);
320        }
321        assert!(aroon.initialized());
322        let expected = 100.0 * (high_idx as f64) / period as f64;
323        let diff = aroon.aroon_up - expected;
324        assert!(diff.abs() < 1e-6);
325    }
326
327    #[rstest]
328    fn test_window_size_period_plus_one() {
329        let period = 7;
330        let mut aroon = AroonOscillator::new(period);
331        for _ in 0..=period {
332            aroon.update_raw(1.0, 0.0);
333        }
334        assert_eq!(aroon.high_inputs.len(), period + 1);
335        assert_eq!(aroon.low_inputs.len(), period + 1);
336    }
337
338    #[rstest]
339    fn test_ignore_oldest_low() {
340        let mut aroon = AroonOscillator::new(5);
341        aroon.update_raw(10.0, 0.0);
342        let inputs = [
343            (11.0, 9.0),
344            (12.0, 9.5),
345            (13.0, 9.2),
346            (14.0, 9.3),
347            (15.0, 9.4),
348        ];
349
350        for &(h, l) in &inputs {
351            aroon.update_raw(h, l);
352        }
353        assert!(aroon.initialized());
354        assert_eq!(aroon.aroon_up, 100.0);
355        assert_eq!(aroon.aroon_down, 20.0);
356        assert_eq!(aroon.value, 80.0);
357    }
358}