nautilus_indicators/momentum/
aroon.rs1use 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#[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 #[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}