nautilus_indicators/momentum/
aroon.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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::{
17    collections::VecDeque,
18    fmt::{Debug, Display},
19};
20
21use nautilus_model::{
22    data::{Bar, QuoteTick, TradeTick},
23    enums::PriceType,
24};
25
26use crate::indicator::Indicator;
27
28/// The Aroon Oscillator calculates the Aroon Up and Aroon Down indicators to
29/// determine if an instrument is trending, and the strength of the trend.
30#[repr(C)]
31#[derive(Debug)]
32#[cfg_attr(
33    feature = "python",
34    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.indicators")
35)]
36pub struct AroonOscillator {
37    pub period: usize,
38    pub high_inputs: VecDeque<f64>,
39    pub low_inputs: VecDeque<f64>,
40    pub aroon_up: f64,
41    pub aroon_down: f64,
42    pub value: f64,
43    pub count: usize,
44    pub initialized: bool,
45    has_inputs: bool,
46}
47
48impl Display for AroonOscillator {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        write!(f, "{}({})", self.name(), self.period)
51    }
52}
53
54impl Indicator for AroonOscillator {
55    fn name(&self) -> String {
56        stringify!(AroonOscillator).to_string()
57    }
58
59    fn has_inputs(&self) -> bool {
60        self.has_inputs
61    }
62
63    fn initialized(&self) -> bool {
64        self.initialized
65    }
66
67    fn handle_quote(&mut self, quote: &QuoteTick) {
68        let price = quote.extract_price(PriceType::Mid).into();
69        self.update_raw(price, price);
70    }
71
72    fn handle_trade(&mut self, trade: &TradeTick) {
73        let price = trade.price.into();
74        self.update_raw(price, price);
75    }
76
77    fn handle_bar(&mut self, bar: &Bar) {
78        self.update_raw((&bar.close).into(), (&bar.close).into());
79    }
80
81    fn reset(&mut self) {
82        self.high_inputs.clear();
83        self.low_inputs.clear();
84        self.aroon_up = 0.0;
85        self.aroon_down = 0.0;
86        self.value = 0.0;
87        self.count = 0;
88        self.has_inputs = false;
89        self.initialized = false;
90    }
91}
92
93impl AroonOscillator {
94    /// Creates a new [`AroonOscillator`] instance.
95    #[must_use]
96    pub fn new(period: usize) -> Self {
97        Self {
98            period,
99            high_inputs: VecDeque::with_capacity(period),
100            low_inputs: VecDeque::with_capacity(period),
101            aroon_up: 0.0,
102            aroon_down: 0.0,
103            value: 0.0,
104            count: 0,
105            has_inputs: false,
106            initialized: false,
107        }
108    }
109
110    pub fn update_raw(&mut self, high: f64, low: f64) {
111        if self.high_inputs.len() == self.period {
112            self.high_inputs.pop_back();
113        }
114        if self.low_inputs.len() == self.period {
115            self.low_inputs.pop_back();
116        }
117
118        self.high_inputs.push_front(high);
119        self.low_inputs.push_front(low);
120
121        self.increment_count();
122        if self.initialized {
123            // Makes sure we calculate with stable period
124            self.calculate_aroon();
125        }
126    }
127
128    fn calculate_aroon(&mut self) {
129        let periods_since_high = self
130            .high_inputs
131            .iter()
132            .enumerate()
133            .fold((0, f64::MIN), |(max_idx, max_val), (idx, &val)| {
134                if val > max_val {
135                    (idx, val)
136                } else {
137                    (max_idx, max_val)
138                }
139            })
140            .0;
141
142        let periods_since_low = self
143            .low_inputs
144            .iter()
145            .enumerate()
146            .fold((0, f64::MAX), |(min_idx, min_val), (idx, &val)| {
147                if val < min_val {
148                    (idx, val)
149                } else {
150                    (min_idx, min_val)
151                }
152            })
153            .0;
154
155        self.aroon_up = 100.0 * ((self.period - periods_since_high) as f64 / self.period as f64);
156        self.aroon_down = 100.0 * ((self.period - periods_since_low) as f64 / self.period as f64);
157        self.value = self.aroon_up - self.aroon_down;
158    }
159
160    const fn increment_count(&mut self) {
161        self.count += 1;
162
163        if !self.initialized {
164            self.has_inputs = true;
165            if self.count >= self.period {
166                self.initialized = true;
167            }
168        }
169    }
170}
171
172////////////////////////////////////////////////////////////////////////////////
173// Tests
174////////////////////////////////////////////////////////////////////////////////
175#[cfg(test)]
176mod tests {
177    use rstest::rstest;
178
179    use super::*;
180    use crate::indicator::Indicator;
181
182    #[rstest]
183    fn test_name_returns_expected_string() {
184        let aroon = AroonOscillator::new(10);
185        assert_eq!(aroon.name(), "AroonOscillator");
186    }
187
188    #[rstest]
189    fn test_period() {
190        let aroon = AroonOscillator::new(10);
191        assert_eq!(aroon.period, 10);
192    }
193
194    #[rstest]
195    fn test_initialized_without_inputs_returns_false() {
196        let aroon = AroonOscillator::new(10);
197        assert!(!aroon.initialized());
198    }
199
200    #[rstest]
201    fn test_initialized_with_required_inputs_returns_true() {
202        let mut aroon = AroonOscillator::new(10);
203        for _ in 0..20 {
204            aroon.update_raw(110.08, 109.61);
205        }
206        assert!(aroon.initialized());
207    }
208
209    #[rstest]
210    fn test_value_with_one_input() {
211        let mut aroon = AroonOscillator::new(1);
212        aroon.update_raw(110.08, 109.61);
213        assert_eq!(aroon.aroon_up, 100.0);
214        assert_eq!(aroon.aroon_down, 100.0);
215        assert_eq!(aroon.value, 0.0);
216    }
217
218    #[rstest]
219    fn test_value_with_twenty_inputs() {
220        let mut aroon = AroonOscillator::new(20);
221        let inputs = [
222            (110.08, 109.61),
223            (110.15, 109.91),
224            (110.1, 109.73),
225            (110.06, 109.77),
226            (110.29, 109.88),
227            (110.53, 110.29),
228            (110.61, 110.26),
229            (110.28, 110.17),
230            (110.3, 110.0),
231            (110.25, 110.01),
232            (110.25, 109.81),
233            (109.92, 109.71),
234            (110.21, 109.84),
235            (110.08, 109.95),
236            (110.2, 109.96),
237            (110.16, 109.95),
238            (109.99, 109.75),
239            (110.2, 109.73),
240            (110.1, 109.81),
241            (110.04, 109.96),
242        ];
243        for &(high, low) in &inputs {
244            aroon.update_raw(high, low);
245        }
246        assert_eq!(aroon.aroon_up, 35.0);
247        assert_eq!(aroon.aroon_down, 5.0);
248        assert_eq!(aroon.value, 30.0);
249    }
250
251    #[rstest]
252    fn test_reset_successfully_returns_indicator_to_fresh_state() {
253        let mut aroon = AroonOscillator::new(10);
254        for _ in 0..1000 {
255            aroon.update_raw(110.08, 109.61);
256        }
257        aroon.reset();
258        assert!(!aroon.initialized());
259        assert_eq!(aroon.aroon_up, 0.0);
260        assert_eq!(aroon.aroon_down, 0.0);
261        assert_eq!(aroon.value, 0.0);
262    }
263}