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