quantwave_core/indicators/incremental/
aroon.rs1use 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#[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#[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}