Skip to main content

quantwave_core/indicators/incremental/
aroon.rs

1//! Native streaming Aroon — TA-Lib parity (`talib_rs::momentum::aroon`, `aroon_osc`).
2
3use crate::traits::Next;
4
5#[derive(Debug, Clone)]
6struct AroonMaxTracker {
7    timeperiod: usize,
8    inv_period: f64,
9    data: Vec<f64>,
10    highest: f64,
11    highest_idx: usize,
12    trailing_idx: usize,
13}
14
15impl AroonMaxTracker {
16    fn new(timeperiod: usize) -> Self {
17        Self {
18            timeperiod,
19            inv_period: 100.0 / timeperiod as f64,
20            data: Vec::new(),
21            highest: f64::NEG_INFINITY,
22            highest_idx: 0,
23            trailing_idx: 0,
24        }
25    }
26
27    fn next(&mut self, value: f64) -> f64 {
28        let timeperiod = self.timeperiod;
29        if timeperiod < 2 {
30            return f64::NAN;
31        }
32        self.data.push(value);
33        let today = self.data.len() - 1;
34
35        if today < timeperiod {
36            if today == 0 {
37                self.highest = value;
38                self.highest_idx = 0;
39            } else if value >= self.highest {
40                self.highest = value;
41                self.highest_idx = today;
42            }
43            return f64::NAN;
44        }
45
46        if today == timeperiod {
47            if value >= self.highest {
48                self.highest = value;
49                self.highest_idx = today;
50            }
51            self.trailing_idx = 1;
52            return (timeperiod - (timeperiod - self.highest_idx)) as f64 * self.inv_period;
53        }
54
55        let data = &self.data;
56        if self.highest_idx < self.trailing_idx {
57            self.highest_idx = self.trailing_idx;
58            self.highest = data[self.trailing_idx];
59            for (j, &val) in data[self.trailing_idx + 1..=today].iter().enumerate() {
60                if val >= self.highest {
61                    self.highest = val;
62                    self.highest_idx = self.trailing_idx + 1 + j;
63                }
64            }
65        } else if value >= self.highest {
66            self.highest_idx = today;
67            self.highest = value;
68        }
69
70        let out = (timeperiod - (today - self.highest_idx)) as f64 * self.inv_period;
71        self.trailing_idx += 1;
72        out
73    }
74}
75
76#[derive(Debug, Clone)]
77struct AroonMinTracker {
78    timeperiod: usize,
79    inv_period: f64,
80    data: Vec<f64>,
81    lowest: f64,
82    lowest_idx: usize,
83    trailing_idx: usize,
84}
85
86impl AroonMinTracker {
87    fn new(timeperiod: usize) -> Self {
88        Self {
89            timeperiod,
90            inv_period: 100.0 / timeperiod as f64,
91            data: Vec::new(),
92            lowest: f64::INFINITY,
93            lowest_idx: 0,
94            trailing_idx: 0,
95        }
96    }
97
98    fn next(&mut self, value: f64) -> f64 {
99        let timeperiod = self.timeperiod;
100        if timeperiod < 2 {
101            return f64::NAN;
102        }
103        self.data.push(value);
104        let today = self.data.len() - 1;
105
106        if today < timeperiod {
107            if today == 0 {
108                self.lowest = value;
109                self.lowest_idx = 0;
110            } else if value <= self.lowest {
111                self.lowest = value;
112                self.lowest_idx = today;
113            }
114            return f64::NAN;
115        }
116
117        if today == timeperiod {
118            if value <= self.lowest {
119                self.lowest = value;
120                self.lowest_idx = today;
121            }
122            self.trailing_idx = 1;
123            return (timeperiod - (timeperiod - self.lowest_idx)) as f64 * self.inv_period;
124        }
125
126        let data = &self.data;
127        if self.lowest_idx < self.trailing_idx {
128            self.lowest_idx = self.trailing_idx;
129            self.lowest = data[self.trailing_idx];
130            for (j, &val) in data[self.trailing_idx + 1..=today].iter().enumerate() {
131                if val <= self.lowest {
132                    self.lowest = val;
133                    self.lowest_idx = self.trailing_idx + 1 + j;
134                }
135            }
136        } else if value <= self.lowest {
137            self.lowest_idx = today;
138            self.lowest = value;
139        }
140
141        let out = (timeperiod - (today - self.lowest_idx)) as f64 * self.inv_period;
142        self.trailing_idx += 1;
143        out
144    }
145}
146
147/// Aroon — returns `(aroon_down, aroon_up)` for `(high, low)`.
148#[derive(Debug, Clone)]
149#[allow(non_camel_case_types)]
150pub struct AROON {
151    pub timeperiod: usize,
152    up: AroonMaxTracker,
153    down: AroonMinTracker,
154}
155
156impl AROON {
157    pub fn new(timeperiod: usize) -> Self {
158        Self {
159            timeperiod,
160            up: AroonMaxTracker::new(timeperiod),
161            down: AroonMinTracker::new(timeperiod),
162        }
163    }
164}
165
166impl Next<(f64, f64)> for AROON {
167    type Output = (f64, f64);
168
169    fn next(&mut self, (high, low): (f64, f64)) -> Self::Output {
170        let down = self.down.next(low);
171        let up = self.up.next(high);
172        (down, up)
173    }
174}
175
176/// Aroon Oscillator — `aroon_up - aroon_down`.
177#[derive(Debug, Clone)]
178#[allow(non_camel_case_types)]
179pub struct AROONOSC {
180    pub timeperiod: usize,
181    high: Vec<f64>,
182    low: Vec<f64>,
183    highest: f64,
184    highest_idx: usize,
185    lowest: f64,
186    lowest_idx: usize,
187    trailing_idx: usize,
188    inv_period: f64,
189}
190
191impl AROONOSC {
192    pub fn new(timeperiod: usize) -> Self {
193        Self {
194            timeperiod,
195            high: Vec::new(),
196            low: Vec::new(),
197            highest: f64::NEG_INFINITY,
198            highest_idx: 0,
199            lowest: f64::INFINITY,
200            lowest_idx: 0,
201            trailing_idx: 0,
202            inv_period: 100.0 / timeperiod as f64,
203        }
204    }
205}
206
207impl Next<(f64, f64)> for AROONOSC {
208    type Output = f64;
209
210    fn next(&mut self, (h, l): (f64, f64)) -> Self::Output {
211        let timeperiod = self.timeperiod;
212        if timeperiod < 2 {
213            return f64::NAN;
214        }
215        self.high.push(h);
216        self.low.push(l);
217        let today = self.high.len() - 1;
218
219        if today < timeperiod {
220            if today == 0 {
221                self.highest = h;
222                self.highest_idx = 0;
223                self.lowest = l;
224                self.lowest_idx = 0;
225            } else {
226                if h >= self.highest {
227                    self.highest = h;
228                    self.highest_idx = today;
229                }
230                if l <= self.lowest {
231                    self.lowest = l;
232                    self.lowest_idx = today;
233                }
234            }
235            return f64::NAN;
236        }
237
238        if today == timeperiod {
239            if h >= self.highest {
240                self.highest = h;
241                self.highest_idx = today;
242            }
243            if l <= self.lowest {
244                self.lowest = l;
245                self.lowest_idx = today;
246            }
247            let up = (timeperiod - (timeperiod - self.highest_idx)) as f64 * self.inv_period;
248            let down = (timeperiod - (timeperiod - self.lowest_idx)) as f64 * self.inv_period;
249            self.trailing_idx = 1;
250            return up - down;
251        }
252
253        let high = &self.high;
254        let low = &self.low;
255
256        if self.highest_idx < self.trailing_idx {
257            self.highest_idx = self.trailing_idx;
258            self.highest = high[self.trailing_idx];
259            for (j, &val) in high[self.trailing_idx + 1..=today].iter().enumerate() {
260                if val >= self.highest {
261                    self.highest = val;
262                    self.highest_idx = self.trailing_idx + 1 + j;
263                }
264            }
265        } else if h >= self.highest {
266            self.highest_idx = today;
267            self.highest = h;
268        }
269
270        if self.lowest_idx < self.trailing_idx {
271            self.lowest_idx = self.trailing_idx;
272            self.lowest = low[self.trailing_idx];
273            for (j, &val) in low[self.trailing_idx + 1..=today].iter().enumerate() {
274                if val <= self.lowest {
275                    self.lowest = val;
276                    self.lowest_idx = self.trailing_idx + 1 + j;
277                }
278            }
279        } else if l <= self.lowest {
280            self.lowest_idx = today;
281            self.lowest = l;
282        }
283
284        let up = (timeperiod - (today - self.highest_idx)) as f64 * self.inv_period;
285        let down = (timeperiod - (today - self.lowest_idx)) as f64 * self.inv_period;
286        self.trailing_idx += 1;
287        up - down
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294    use proptest::prelude::*;
295
296    proptest! {
297        #[test]
298        fn test_aroon_parity(
299            h in prop::collection::vec(1.0..100.0, 10..100),
300            l in prop::collection::vec(1.0..100.0, 10..100),
301        ) {
302            let len = h.len().min(l.len());
303            let period = 14;
304            let mut aroon = AROON::new(period);
305            let streaming: Vec<_> =
306                (0..len).map(|i| aroon.next((h[i], l[i]))).collect();
307            let (b_down, b_up) = talib_rs::momentum::aroon(&h[..len], &l[..len], period)
308                .unwrap_or_else(|_| (vec![f64::NAN; len], vec![f64::NAN; len]));
309            for (i, (s_down, s_up)) in streaming.iter().enumerate() {
310                if !s_down.is_nan() && !b_down[i].is_nan() {
311                    approx::assert_relative_eq!(*s_down, b_down[i], epsilon = 1e-6);
312                }
313                if !s_up.is_nan() && !b_up[i].is_nan() {
314                    approx::assert_relative_eq!(*s_up, b_up[i], epsilon = 1e-6);
315                }
316            }
317        }
318
319        #[test]
320        fn test_aroonosc_parity(
321            h_in in prop::collection::vec(1.0..100.0, 10..100),
322            l_in in prop::collection::vec(1.0..100.0, 10..100),
323        ) {
324            let len = h_in.len().min(l_in.len());
325            let mut in1 = Vec::with_capacity(len);
326            let mut in2 = Vec::with_capacity(len);
327            for i in 0..len {
328                let h: f64 = h_in[i];
329                let l: f64 = l_in[i];
330                in1.push(h.max(l));
331                in2.push(h.min(l));
332            }
333            let period = 14;
334            let mut osc = AROONOSC::new(period);
335            let streaming: Vec<f64> =
336                (0..len).map(|i| osc.next((in1[i], in2[i]))).collect();
337            let batch = talib_rs::momentum::aroon_osc(&in1, &in2, period)
338                .unwrap_or_else(|_| vec![f64::NAN; len]);
339            for (s, b) in streaming.iter().zip(batch.iter()) {
340                if s.is_nan() {
341                    assert!(b.is_nan());
342                } else if !b.is_nan() {
343                    approx::assert_relative_eq!(s, b, epsilon = 1e-6);
344                }
345            }
346        }
347    }
348}