nautilus_indicators/momentum/
aroon.rs1use std::{
17 collections::VecDeque,
18 fmt::{Debug, Display},
19};
20
21use nautilus_core::correctness::FAILED;
22use nautilus_model::{
23 data::{Bar, QuoteTick, TradeTick},
24 enums::PriceType,
25};
26
27pub use crate::support::MAX_PERIOD;
28use crate::{indicator::Indicator, support::is_valid_high_low};
29
30#[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 high_inputs: VecDeque<f64>,
51 low_inputs: VecDeque<f64>,
52}
53
54impl Display for AroonOscillator {
55 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56 write!(f, "{}({})", self.name(), self.period)
57 }
58}
59
60impl Indicator for AroonOscillator {
61 fn name(&self) -> String {
62 stringify!(AroonOscillator).into()
63 }
64
65 fn has_inputs(&self) -> bool {
66 self.has_inputs
67 }
68
69 fn initialized(&self) -> bool {
70 self.initialized
71 }
72
73 fn handle_quote(&mut self, quote: &QuoteTick) -> anyhow::Result<()> {
74 let price = quote.extract_price(PriceType::Mid)?.into();
75 self.update_raw(price, price);
76 Ok(())
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.has_inputs = false;
98 self.initialized = false;
99 }
100}
101
102impl AroonOscillator {
103 #[must_use]
109 pub fn new(period: usize) -> Self {
110 Self::new_checked(period).expect(FAILED)
111 }
112
113 pub(crate) fn new_checked(period: usize) -> anyhow::Result<Self> {
114 anyhow::ensure!(
115 period > 0,
116 "AroonOscillator: period must be > 0 (received {period})"
117 );
118 anyhow::ensure!(
119 period <= MAX_PERIOD,
120 "AroonOscillator: period must be ≤ {MAX_PERIOD} (received {period})"
121 );
122
123 Ok(Self {
124 period,
125 aroon_up: 0.0,
126 aroon_down: 0.0,
127 value: 0.0,
128 count: 0,
129 has_inputs: false,
130 initialized: false,
131 high_inputs: VecDeque::with_capacity(period + 1),
132 low_inputs: VecDeque::with_capacity(period + 1),
133 })
134 }
135
136 pub fn update_raw(&mut self, high: f64, low: f64) {
137 if !is_valid_high_low(high, low) {
138 return;
139 }
140
141 if self.count == self.period + 1 {
142 let _ = self.high_inputs.pop_front();
143 let _ = self.low_inputs.pop_front();
144 } else {
145 self.count += 1;
146 }
147
148 self.high_inputs.push_back(high);
149 self.low_inputs.push_back(low);
150
151 let required = self.period + 1;
152 if !self.initialized && self.count >= required {
153 self.initialized = true;
154 }
155 self.has_inputs = true;
156
157 if self.initialized {
158 self.calculate_aroon();
159 }
160 }
161
162 fn calculate_aroon(&mut self) {
165 debug_assert_eq!(self.high_inputs.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 = 0_usize;
177 let mut min_val = f64::MAX;
178 for (idx, &lo) in self.low_inputs.iter().enumerate() {
179 if lo <= min_val {
180 min_val = lo;
181 min_idx = idx;
182 }
183 }
184
185 self.aroon_up = 100.0 * max_idx as f64 / self.period as f64;
186 self.aroon_down = 100.0 * min_idx as f64 / self.period as f64;
187 self.value = self.aroon_up - self.aroon_down;
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use rstest::rstest;
194
195 use super::*;
196 use crate::indicator::Indicator;
197
198 #[rstest]
199 fn test_name() {
200 let aroon = AroonOscillator::new(10);
201 assert_eq!(aroon.name(), "AroonOscillator");
202 }
203
204 #[rstest]
205 fn test_period() {
206 let aroon = AroonOscillator::new(10);
207 assert_eq!(aroon.period, 10);
208 }
209
210 #[rstest]
211 fn test_initialized_false() {
212 let aroon = AroonOscillator::new(10);
213 assert!(!aroon.initialized());
214 }
215
216 #[rstest]
217 fn test_initialized_true() {
218 let mut aroon = AroonOscillator::new(10);
219 for _ in 0..=10 {
220 aroon.update_raw(110.08, 109.61);
221 }
222 assert!(aroon.initialized());
223 }
224
225 #[rstest]
226 fn test_value_one_input() {
227 let mut aroon = AroonOscillator::new(1);
228 aroon.update_raw(110.08, 109.61);
229 assert_eq!(aroon.aroon_up, 0.0);
230 assert_eq!(aroon.aroon_down, 0.0);
231 assert_eq!(aroon.value, 0.0);
232 assert!(!aroon.initialized());
233 aroon.update_raw(110.10, 109.70);
234 assert!(aroon.initialized());
235 assert_eq!(aroon.aroon_up, 100.0);
236 assert_eq!(aroon.aroon_down, 0.0);
237 assert_eq!(aroon.value, 100.0);
238 }
239
240 #[rstest]
241 fn test_value_twenty_inputs() {
242 let mut aroon = AroonOscillator::new(20);
243 let inputs = [
244 (110.08, 109.61),
245 (110.15, 109.91),
246 (110.10, 109.73),
247 (110.06, 109.77),
248 (110.29, 109.88),
249 (110.53, 110.29),
250 (110.61, 110.26),
251 (110.28, 110.17),
252 (110.30, 110.00),
253 (110.25, 110.01),
254 (110.25, 109.81),
255 (109.92, 109.71),
256 (110.21, 109.84),
257 (110.08, 109.95),
258 (110.20, 109.96),
259 (110.16, 109.95),
260 (109.99, 109.75),
261 (110.20, 109.73),
262 (110.10, 109.81),
263 (110.04, 109.96),
264 (110.02, 109.90),
265 ];
266
267 for &(h, l) in &inputs {
268 aroon.update_raw(h, l);
269 }
270 assert!(aroon.initialized());
271 assert_eq!(aroon.aroon_up, 30.0);
272 assert_eq!(aroon.aroon_down, 0.0);
273 assert_eq!(aroon.value, 30.0);
274 }
275
276 #[rstest]
277 fn test_reset() {
278 let mut aroon = AroonOscillator::new(10);
279 for _ in 0..12 {
280 aroon.update_raw(110.08, 109.61);
281 }
282 aroon.reset();
283 assert!(!aroon.initialized());
284 assert_eq!(aroon.aroon_up, 0.0);
285 assert_eq!(aroon.aroon_down, 0.0);
286 assert_eq!(aroon.value, 0.0);
287 }
288
289 #[rstest]
290 fn test_initialized_boundary() {
291 let mut aroon = AroonOscillator::new(5);
292 for _ in 0..5 {
293 aroon.update_raw(1.0, 0.0);
294 assert!(!aroon.initialized());
295 }
296 aroon.update_raw(1.0, 0.0);
297 assert!(aroon.initialized());
298 }
299
300 #[rstest]
301 #[case(1, 0)]
302 #[case(5, 0)]
303 #[case(5, 2)]
304 #[case(10, 0)]
305 #[case(10, 9)]
306 fn test_formula_equivalence(#[case] period: usize, #[case] high_idx: usize) {
307 let mut aroon = AroonOscillator::new(period);
308 for idx in 0..=period {
309 let h = if idx == high_idx { 1_000.0 } else { idx as f64 };
310 aroon.update_raw(h, h);
311 }
312 assert!(aroon.initialized());
313 let expected = 100.0 * (high_idx as f64) / period as f64;
314 let diff = aroon.aroon_up - expected;
315 assert!(diff.abs() < 1e-6);
316 }
317
318 #[rstest]
319 fn test_window_size_period_plus_one() {
320 let period = 7;
321 let mut aroon = AroonOscillator::new(period);
322 for _ in 0..=period {
323 aroon.update_raw(1.0, 0.0);
324 }
325 assert_eq!(aroon.high_inputs.len(), period + 1);
326 assert_eq!(aroon.low_inputs.len(), period + 1);
327 }
328
329 #[rstest]
330 fn test_oldest_low_reaches_zero() {
331 let mut aroon = AroonOscillator::new(5);
334 aroon.update_raw(10.0, 0.0);
335 let inputs = [
336 (11.0, 9.0),
337 (12.0, 9.5),
338 (13.0, 9.2),
339 (14.0, 9.3),
340 (15.0, 9.4),
341 ];
342
343 for &(h, l) in &inputs {
344 aroon.update_raw(h, l);
345 }
346 assert!(aroon.initialized());
347 assert_eq!(aroon.aroon_up, 100.0);
348 assert_eq!(aroon.aroon_down, 0.0);
349 assert_eq!(aroon.value, 100.0);
350 }
351
352 #[rstest]
353 fn test_flat_series_ties_keep_newest_extreme() {
354 let mut aroon = AroonOscillator::new(5);
355 for _ in 0..=5 {
356 aroon.update_raw(1.0, 0.5);
357 }
358 assert!(aroon.initialized());
359 assert_eq!(aroon.aroon_up, 100.0);
360 assert_eq!(aroon.aroon_down, 100.0);
361 assert_eq!(aroon.value, 0.0);
362 }
363}