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.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) -> anyhow::Result<()> {
75        let price = quote.extract_price(PriceType::Mid)?.into();
76        self.update_raw(price, price);
77        Ok(())
78    }
79
80    fn handle_trade(&mut self, trade: &TradeTick) {
81        let price: f64 = trade.price.into();
82        self.update_raw(price, price);
83    }
84
85    fn handle_bar(&mut self, bar: &Bar) {
86        let high: f64 = (&bar.high).into();
87        let low: f64 = (&bar.low).into();
88        self.update_raw(high, low);
89    }
90
91    fn reset(&mut self) {
92        self.high_inputs.clear();
93        self.low_inputs.clear();
94        self.aroon_up = 0.0;
95        self.aroon_down = 0.0;
96        self.value = 0.0;
97        self.count = 0;
98        self.total_count = 0;
99        self.has_inputs = false;
100        self.initialized = false;
101    }
102}
103
104impl AroonOscillator {
105    /// Creates a new [`AroonOscillator`] instance.
106    ///
107    /// # Panics
108    ///
109    /// Panics if `period` is not positive (> 0).
110    #[must_use]
111    pub fn new(period: usize) -> Self {
112        assert!(
113            period > 0,
114            "AroonOscillator: period must be > 0 (received {period})"
115        );
116        assert!(
117            period <= MAX_PERIOD,
118            "AroonOscillator: period must be ≤ {MAX_PERIOD} (received {period})"
119        );
120
121        Self {
122            period,
123            aroon_up: 0.0,
124            aroon_down: 0.0,
125            value: 0.0,
126            count: 0,
127            total_count: 0,
128            has_inputs: false,
129            initialized: false,
130            high_inputs: ArrayDeque::new(),
131            low_inputs: ArrayDeque::new(),
132        }
133    }
134
135    pub fn update_raw(&mut self, high: f64, low: f64) {
136        debug_assert!(
137            high >= low,
138            "AroonOscillator::update_raw - high must be ≥ low"
139        );
140
141        self.total_count = self.total_count.saturating_add(1);
142
143        if self.count == self.period + 1 {
144            let _ = self.high_inputs.pop_front();
145            let _ = self.low_inputs.pop_front();
146        } else {
147            self.count += 1;
148        }
149
150        let _ = self.high_inputs.push_back(high);
151        let _ = self.low_inputs.push_back(low);
152
153        let required = self.period + 1;
154        if !self.initialized && self.total_count >= required {
155            self.initialized = true;
156        }
157        self.has_inputs = true;
158
159        if self.initialized {
160            self.calculate_aroon();
161        }
162    }
163
164    fn calculate_aroon(&mut self) {
165        let len = self.high_inputs.len();
166        debug_assert_eq!(len, self.period + 1);
167
168        let mut max_idx = 0_usize;
169        let mut max_val = f64::MIN;
170        for (idx, &hi) in self.high_inputs.iter().enumerate() {
171            if hi > max_val {
172                max_val = hi;
173                max_idx = idx;
174            }
175        }
176
177        let mut min_idx_rel = 0_usize;
178        let mut min_val = f64::MAX;
179        for (idx, &lo) in self.low_inputs.iter().skip(1).enumerate() {
180            if lo < min_val {
181                min_val = lo;
182                min_idx_rel = idx;
183            }
184        }
185
186        let periods_since_high = len - 1 - max_idx;
187        let periods_since_low = self.period - 1 - min_idx_rel;
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, 100.0);
248        assert_eq!(aroon.value, 0.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.value, -25.0);
284    }
285
286    #[rstest]
287    fn test_reset() {
288        let mut aroon = AroonOscillator::new(10);
289        for _ in 0..12 {
290            aroon.update_raw(110.08, 109.61);
291        }
292        aroon.reset();
293        assert!(!aroon.initialized());
294        assert_eq!(aroon.aroon_up, 0.0);
295        assert_eq!(aroon.aroon_down, 0.0);
296        assert_eq!(aroon.value, 0.0);
297    }
298
299    #[rstest]
300    fn test_initialized_boundary() {
301        let mut aroon = AroonOscillator::new(5);
302        for _ in 0..5 {
303            aroon.update_raw(1.0, 0.0);
304            assert!(!aroon.initialized());
305        }
306        aroon.update_raw(1.0, 0.0);
307        assert!(aroon.initialized());
308    }
309
310    #[rstest]
311    #[case(1, 0)]
312    #[case(5, 0)]
313    #[case(5, 2)]
314    #[case(10, 0)]
315    #[case(10, 9)]
316    fn test_formula_equivalence(#[case] period: usize, #[case] high_idx: usize) {
317        let mut aroon = AroonOscillator::new(period);
318        for idx in 0..=period {
319            let h = if idx == high_idx { 1_000.0 } else { idx as f64 };
320            aroon.update_raw(h, h);
321        }
322        assert!(aroon.initialized());
323        let expected = 100.0 * (high_idx as f64) / period as f64;
324        let diff = aroon.aroon_up - expected;
325        assert!(diff.abs() < 1e-6);
326    }
327
328    #[rstest]
329    fn test_window_size_period_plus_one() {
330        let period = 7;
331        let mut aroon = AroonOscillator::new(period);
332        for _ in 0..=period {
333            aroon.update_raw(1.0, 0.0);
334        }
335        assert_eq!(aroon.high_inputs.len(), period + 1);
336        assert_eq!(aroon.low_inputs.len(), period + 1);
337    }
338
339    #[rstest]
340    fn test_ignore_oldest_low() {
341        let mut aroon = AroonOscillator::new(5);
342        aroon.update_raw(10.0, 0.0);
343        let inputs = [
344            (11.0, 9.0),
345            (12.0, 9.5),
346            (13.0, 9.2),
347            (14.0, 9.3),
348            (15.0, 9.4),
349        ];
350
351        for &(h, l) in &inputs {
352            aroon.update_raw(h, l);
353        }
354        assert!(aroon.initialized());
355        assert_eq!(aroon.aroon_up, 100.0);
356        assert_eq!(aroon.aroon_down, 20.0);
357        assert_eq!(aroon.value, 80.0);
358    }
359}