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.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 #[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}