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 = "posei_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)]
201mod tests {
202 use rstest::rstest;
203
204 use super::*;
205 use crate::indicator::Indicator;
206
207 #[rstest]
208 fn test_name() {
209 let aroon = AroonOscillator::new(10);
210 assert_eq!(aroon.name(), "AroonOscillator");
211 }
212
213 #[rstest]
214 fn test_period() {
215 let aroon = AroonOscillator::new(10);
216 assert_eq!(aroon.period, 10);
217 }
218
219 #[rstest]
220 fn test_initialized_false() {
221 let aroon = AroonOscillator::new(10);
222 assert!(!aroon.initialized());
223 }
224
225 #[rstest]
226 fn test_initialized_true() {
227 let mut aroon = AroonOscillator::new(10);
228 for _ in 0..=10 {
229 aroon.update_raw(110.08, 109.61);
230 }
231 assert!(aroon.initialized());
232 }
233
234 #[rstest]
235 fn test_value_one_input() {
236 let mut aroon = AroonOscillator::new(1);
237 aroon.update_raw(110.08, 109.61);
238 assert_eq!(aroon.aroon_up, 0.0);
239 assert_eq!(aroon.aroon_down, 0.0);
240 assert_eq!(aroon.value, 0.0);
241 assert!(!aroon.initialized());
242 aroon.update_raw(110.10, 109.70);
243 assert!(aroon.initialized());
244 assert_eq!(aroon.aroon_up, 100.0);
245 assert_eq!(aroon.aroon_down, 100.0);
246 assert_eq!(aroon.value, 0.0);
247 }
248
249 #[rstest]
250 fn test_value_twenty_inputs() {
251 let mut aroon = AroonOscillator::new(20);
252 let inputs = [
253 (110.08, 109.61),
254 (110.15, 109.91),
255 (110.10, 109.73),
256 (110.06, 109.77),
257 (110.29, 109.88),
258 (110.53, 110.29),
259 (110.61, 110.26),
260 (110.28, 110.17),
261 (110.30, 110.00),
262 (110.25, 110.01),
263 (110.25, 109.81),
264 (109.92, 109.71),
265 (110.21, 109.84),
266 (110.08, 109.95),
267 (110.20, 109.96),
268 (110.16, 109.95),
269 (109.99, 109.75),
270 (110.20, 109.73),
271 (110.10, 109.81),
272 (110.04, 109.96),
273 (110.02, 109.90),
274 ];
275 for &(h, l) in &inputs {
276 aroon.update_raw(h, l);
277 }
278 assert!(aroon.initialized());
279 assert_eq!(aroon.aroon_up, 30.0);
280 assert_eq!(aroon.value, -25.0);
281 }
282
283 #[rstest]
284 fn test_reset() {
285 let mut aroon = AroonOscillator::new(10);
286 for _ in 0..12 {
287 aroon.update_raw(110.08, 109.61);
288 }
289 aroon.reset();
290 assert!(!aroon.initialized());
291 assert_eq!(aroon.aroon_up, 0.0);
292 assert_eq!(aroon.aroon_down, 0.0);
293 assert_eq!(aroon.value, 0.0);
294 }
295
296 #[rstest]
297 fn test_initialized_boundary() {
298 let mut aroon = AroonOscillator::new(5);
299 for _ in 0..5 {
300 aroon.update_raw(1.0, 0.0);
301 assert!(!aroon.initialized());
302 }
303 aroon.update_raw(1.0, 0.0);
304 assert!(aroon.initialized());
305 }
306
307 #[rstest]
308 #[case(1, 0)]
309 #[case(5, 0)]
310 #[case(5, 2)]
311 #[case(10, 0)]
312 #[case(10, 9)]
313 fn test_formula_equivalence(#[case] period: usize, #[case] high_idx: usize) {
314 let mut aroon = AroonOscillator::new(period);
315 for idx in 0..=period {
316 let h = if idx == high_idx { 1_000.0 } else { idx as f64 };
317 aroon.update_raw(h, h);
318 }
319 assert!(aroon.initialized());
320 let expected = 100.0 * (high_idx as f64) / period as f64;
321 let diff = aroon.aroon_up - expected;
322 assert!(diff.abs() < 1e-6);
323 }
324
325 #[rstest]
326 fn test_window_size_period_plus_one() {
327 let period = 7;
328 let mut aroon = AroonOscillator::new(period);
329 for _ in 0..=period {
330 aroon.update_raw(1.0, 0.0);
331 }
332 assert_eq!(aroon.high_inputs.len(), period + 1);
333 assert_eq!(aroon.low_inputs.len(), period + 1);
334 }
335
336 #[rstest]
337 fn test_ignore_oldest_low() {
338 let mut aroon = AroonOscillator::new(5);
339 aroon.update_raw(10.0, 0.0);
340 let inputs = [
341 (11.0, 9.0),
342 (12.0, 9.5),
343 (13.0, 9.2),
344 (14.0, 9.3),
345 (15.0, 9.4),
346 ];
347 for &(h, l) in &inputs {
348 aroon.update_raw(h, l);
349 }
350 assert!(aroon.initialized());
351 assert_eq!(aroon.aroon_up, 100.0);
352 assert_eq!(aroon.aroon_down, 20.0);
353 assert_eq!(aroon.value, 80.0);
354 }
355}