1#[cfg(all(feature = "python", feature = "cuda"))]
2use numpy::PyUntypedArrayMethods;
3#[cfg(feature = "python")]
4use numpy::{IntoPyArray, PyArray1};
5#[cfg(feature = "python")]
6use pyo3::exceptions::PyValueError;
7#[cfg(feature = "python")]
8use pyo3::prelude::*;
9#[cfg(feature = "python")]
10use pyo3::types::{PyDict, PyList};
11
12#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
13use js_sys;
14#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
15use serde::{Deserialize, Serialize};
16#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
17use wasm_bindgen::prelude::*;
18
19#[cfg(all(feature = "python", feature = "cuda"))]
20use crate::cuda::{cuda_available, CudaAroon};
21use crate::utilities::data_loader::{source_type, Candles};
22#[cfg(all(feature = "python", feature = "cuda"))]
23use crate::utilities::dlpack_cuda::export_f32_cuda_dlpack_2d;
24use crate::utilities::enums::Kernel;
25use crate::utilities::helpers::{
26 alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
27 make_uninit_matrix,
28};
29#[cfg(feature = "python")]
30use crate::utilities::kernel_validation::validate_kernel;
31#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
32use core::arch::x86_64::*;
33#[cfg(not(target_arch = "wasm32"))]
34use rayon::prelude::*;
35use std::collections::VecDeque;
36use std::convert::AsRef;
37use std::mem::{ManuallyDrop, MaybeUninit};
38use thiserror::Error;
39
40#[derive(Debug, Clone)]
41pub enum AroonData<'a> {
42 Candles { candles: &'a Candles },
43 SlicesHL { high: &'a [f64], low: &'a [f64] },
44}
45
46#[derive(Debug, Clone)]
47#[cfg_attr(
48 all(target_arch = "wasm32", feature = "wasm"),
49 derive(Serialize, Deserialize)
50)]
51pub struct AroonParams {
52 pub length: Option<usize>,
53}
54
55impl Default for AroonParams {
56 fn default() -> Self {
57 Self { length: Some(14) }
58 }
59}
60
61#[derive(Debug, Clone)]
62pub struct AroonInput<'a> {
63 pub data: AroonData<'a>,
64 pub params: AroonParams,
65}
66
67impl<'a> AroonInput<'a> {
68 #[inline]
69 pub fn from_candles(c: &'a Candles, p: AroonParams) -> Self {
70 Self {
71 data: AroonData::Candles { candles: c },
72 params: p,
73 }
74 }
75 #[inline]
76 pub fn from_slices_hl(high: &'a [f64], low: &'a [f64], p: AroonParams) -> Self {
77 Self {
78 data: AroonData::SlicesHL { high, low },
79 params: p,
80 }
81 }
82 #[inline]
83 pub fn with_default_candles(c: &'a Candles) -> Self {
84 Self::from_candles(c, AroonParams::default())
85 }
86 #[inline]
87 pub fn get_length(&self) -> usize {
88 self.params.length.unwrap_or(14)
89 }
90}
91
92impl<'a> AsRef<[f64]> for AroonInput<'a> {
93 fn as_ref(&self) -> &[f64] {
94 match &self.data {
95 AroonData::Candles { candles } => &candles.high,
96 AroonData::SlicesHL { high, .. } => high,
97 }
98 }
99}
100
101#[derive(Debug, Clone)]
102pub struct AroonOutput {
103 pub aroon_up: Vec<f64>,
104 pub aroon_down: Vec<f64>,
105}
106
107#[derive(Copy, Clone, Debug)]
108pub struct AroonBuilder {
109 length: Option<usize>,
110 kernel: Kernel,
111}
112
113impl Default for AroonBuilder {
114 fn default() -> Self {
115 Self {
116 length: None,
117 kernel: Kernel::Auto,
118 }
119 }
120}
121impl AroonBuilder {
122 #[inline(always)]
123 pub fn new() -> Self {
124 Self::default()
125 }
126 #[inline(always)]
127 pub fn length(mut self, n: usize) -> Self {
128 self.length = Some(n);
129 self
130 }
131 #[inline(always)]
132 pub fn kernel(mut self, k: Kernel) -> Self {
133 self.kernel = k;
134 self
135 }
136 #[inline(always)]
137 pub fn apply(self, c: &Candles) -> Result<AroonOutput, AroonError> {
138 let p = AroonParams {
139 length: self.length,
140 };
141 let i = AroonInput::from_candles(c, p);
142 aroon_with_kernel(&i, self.kernel)
143 }
144 #[inline(always)]
145 pub fn apply_slices(self, high: &[f64], low: &[f64]) -> Result<AroonOutput, AroonError> {
146 let p = AroonParams {
147 length: self.length,
148 };
149 let i = AroonInput::from_slices_hl(high, low, p);
150 aroon_with_kernel(&i, self.kernel)
151 }
152 #[inline(always)]
153 pub fn into_stream(self) -> Result<AroonStream, AroonError> {
154 let p = AroonParams {
155 length: self.length,
156 };
157 AroonStream::try_new(p)
158 }
159}
160
161#[derive(Debug, Error)]
162pub enum AroonError {
163 #[error("aroon: All values are NaN.")]
164 AllValuesNaN,
165 #[error("aroon: Input data slice is empty.")]
166 EmptyInputData,
167 #[error("aroon: Invalid length: length = {length}, data length = {data_len}")]
168 InvalidLength { length: usize, data_len: usize },
169 #[error("aroon: Not enough valid data: needed = {needed}, valid = {valid}")]
170 NotEnoughValidData { needed: usize, valid: usize },
171 #[error("aroon: Mismatch in high/low slice length: high_len={high_len}, low_len={low_len}")]
172 MismatchSliceLength { high_len: usize, low_len: usize },
173 #[error("aroon: Output length mismatch: expected = {expected}, got = {got}")]
174 OutputLengthMismatch { expected: usize, got: usize },
175 #[error("aroon: Invalid range: start={start}, end={end}, step={step}")]
176 InvalidRange {
177 start: usize,
178 end: usize,
179 step: usize,
180 },
181 #[error("aroon: Invalid kernel for batch: {0:?}")]
182 InvalidKernelForBatch(Kernel),
183}
184
185#[inline(always)]
186fn first_valid_pair(high: &[f64], low: &[f64]) -> Option<usize> {
187 high.iter()
188 .zip(low.iter())
189 .position(|(h, l)| h.is_finite() && l.is_finite())
190}
191
192#[inline]
193pub fn aroon(input: &AroonInput) -> Result<AroonOutput, AroonError> {
194 aroon_with_kernel(input, Kernel::Auto)
195}
196
197pub fn aroon_with_kernel(input: &AroonInput, kernel: Kernel) -> Result<AroonOutput, AroonError> {
198 let (high, low): (&[f64], &[f64]) = match &input.data {
199 AroonData::Candles { candles } => {
200 (source_type(candles, "high"), source_type(candles, "low"))
201 }
202 AroonData::SlicesHL { high, low } => (*high, *low),
203 };
204 if high.is_empty() || low.is_empty() {
205 return Err(AroonError::EmptyInputData);
206 }
207 if high.len() != low.len() {
208 return Err(AroonError::MismatchSliceLength {
209 high_len: high.len(),
210 low_len: low.len(),
211 });
212 }
213 let len = high.len();
214 let length = input.get_length();
215
216 if length == 0 || length > len {
217 return Err(AroonError::InvalidLength {
218 length,
219 data_len: len,
220 });
221 }
222 if len < length {
223 return Err(AroonError::NotEnoughValidData {
224 needed: length,
225 valid: len,
226 });
227 }
228
229 let chosen = match kernel {
230 Kernel::Auto => Kernel::Scalar,
231 other => other,
232 };
233
234 let first = first_valid_pair(high, low).ok_or(AroonError::AllValuesNaN)?;
235 let warmup_period = first + length;
236 let mut up = alloc_with_nan_prefix(len, warmup_period);
237 let mut down = alloc_with_nan_prefix(len, warmup_period);
238
239 unsafe {
240 match chosen {
241 Kernel::Scalar | Kernel::ScalarBatch => {
242 aroon_scalar(high, low, length, &mut up, &mut down)
243 }
244 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
245 Kernel::Avx2 | Kernel::Avx2Batch => aroon_avx2(high, low, length, &mut up, &mut down),
246 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
247 Kernel::Avx512 | Kernel::Avx512Batch => {
248 aroon_avx512(high, low, length, &mut up, &mut down)
249 }
250 _ => unreachable!(),
251 }
252 }
253
254 let warm = warmup_period.min(len);
255 for v in &mut up[..warm] {
256 *v = f64::NAN;
257 }
258 for v in &mut down[..warm] {
259 *v = f64::NAN;
260 }
261
262 Ok(AroonOutput {
263 aroon_up: up,
264 aroon_down: down,
265 })
266}
267
268#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
269#[inline]
270pub fn aroon_into(
271 input: &AroonInput,
272 out_up: &mut [f64],
273 out_down: &mut [f64],
274) -> Result<(), AroonError> {
275 aroon_into_slice(out_up, out_down, input, Kernel::Auto)
276}
277
278#[inline]
279pub fn aroon_scalar(high: &[f64], low: &[f64], length: usize, up: &mut [f64], down: &mut [f64]) {
280 let len = high.len();
281 assert!(
282 length >= 1 && length <= len,
283 "Invalid length: {} for data of size {}",
284 length,
285 len
286 );
287 assert!(
288 low.len() == len && up.len() == len && down.len() == len,
289 "Slice lengths must match"
290 );
291
292 let scale_100 = 100.0 / (length as f64);
293
294 #[inline(always)]
295 fn pair_is_finite(h: f64, l: f64) -> bool {
296 const EXP_MASK: u64 = 0x7ff0_0000_0000_0000;
297 (h.to_bits() & EXP_MASK) != EXP_MASK && (l.to_bits() & EXP_MASK) != EXP_MASK
298 }
299
300 #[inline(always)]
301 fn aroon_percent(dist: usize, scale_100: f64) -> f64 {
302 let v = 100.0 - (dist as f64) * scale_100;
303 v.max(0.0)
304 }
305
306 let mut all_finite = true;
307 unsafe {
308 let hp = high.as_ptr();
309 let lp = low.as_ptr();
310 let mut i = 0usize;
311 while i < len {
312 if !pair_is_finite(*hp.add(i), *lp.add(i)) {
313 all_finite = false;
314 break;
315 }
316 i += 1;
317 }
318 }
319
320 if all_finite {
321 unsafe {
322 let hp = high.as_ptr();
323 let lp = low.as_ptr();
324 let up_ptr = up.as_mut_ptr();
325 let dn_ptr = down.as_mut_ptr();
326
327 if length < len {
328 let i0 = length;
329 let mut maxi = 0usize;
330 let mut mini = 0usize;
331 let mut max = *hp;
332 let mut min = *lp;
333
334 let mut j = 1usize;
335 while j <= i0 {
336 let hv = *hp.add(j);
337 if hv > max {
338 max = hv;
339 maxi = j;
340 }
341 let lv = *lp.add(j);
342 if lv < min {
343 min = lv;
344 mini = j;
345 }
346 j += 1;
347 }
348
349 *up_ptr.add(i0) = aroon_percent(i0 - maxi, scale_100);
350 *dn_ptr.add(i0) = aroon_percent(i0 - mini, scale_100);
351
352 let mut i = i0 + 1;
353 while i < len {
354 let start = i - length;
355 let h = *hp.add(i);
356 let l = *lp.add(i);
357
358 if maxi < start {
359 maxi = start;
360 max = *hp.add(maxi);
361 let mut j = start + 1;
362 while j <= i {
363 let hv = *hp.add(j);
364 if hv > max {
365 max = hv;
366 maxi = j;
367 }
368 j += 1;
369 }
370 } else if h > max {
371 maxi = i;
372 max = h;
373 }
374
375 if mini < start {
376 mini = start;
377 min = *lp.add(mini);
378 let mut j = start + 1;
379 while j <= i {
380 let lv = *lp.add(j);
381 if lv < min {
382 min = lv;
383 mini = j;
384 }
385 j += 1;
386 }
387 } else if l < min {
388 mini = i;
389 min = l;
390 }
391
392 *up_ptr.add(i) = aroon_percent(i - maxi, scale_100);
393 *dn_ptr.add(i) = aroon_percent(i - mini, scale_100);
394
395 i += 1;
396 }
397 }
398 }
399
400 return;
401 }
402
403 let window = length + 1;
404 let mut invalid_count: usize = 0;
405
406 let mut have_extremes = false;
407 let mut maxi: usize = 0;
408 let mut mini: usize = 0;
409 let mut max = 0.0f64;
410 let mut min = 0.0f64;
411
412 for i in 0..len {
413 let h = high[i];
414 let l = low[i];
415 if !pair_is_finite(h, l) {
416 invalid_count += 1;
417 }
418 if i >= window {
419 let leave = i - window;
420 if !pair_is_finite(high[leave], low[leave]) {
421 invalid_count -= 1;
422 }
423 }
424
425 if i < length {
426 continue;
427 }
428
429 if invalid_count != 0 {
430 up[i] = f64::NAN;
431 down[i] = f64::NAN;
432 have_extremes = false;
433 continue;
434 }
435
436 let start = i - length;
437
438 if !have_extremes {
439 maxi = start;
440 mini = start;
441 max = high[start];
442 min = low[start];
443 for j in (start + 1)..=i {
444 let hv = high[j];
445 if hv > max {
446 max = hv;
447 maxi = j;
448 }
449 let lv = low[j];
450 if lv < min {
451 min = lv;
452 mini = j;
453 }
454 }
455 have_extremes = true;
456 } else {
457 if maxi < start {
458 maxi = start;
459 max = high[maxi];
460 for j in (start + 1)..=i {
461 let hv = high[j];
462 if hv > max {
463 max = hv;
464 maxi = j;
465 }
466 }
467 } else if h > max {
468 maxi = i;
469 max = h;
470 }
471
472 if mini < start {
473 mini = start;
474 min = low[mini];
475 for j in (start + 1)..=i {
476 let lv = low[j];
477 if lv < min {
478 min = lv;
479 mini = j;
480 }
481 }
482 } else if l < min {
483 mini = i;
484 min = l;
485 }
486 }
487
488 let dist_hi = i - maxi;
489 let dist_lo = i - mini;
490 up[i] = aroon_percent(dist_hi, scale_100);
491 down[i] = aroon_percent(dist_lo, scale_100);
492 }
493}
494
495#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
496#[inline]
497pub fn aroon_avx512(high: &[f64], low: &[f64], length: usize, up: &mut [f64], down: &mut [f64]) {
498 unsafe {
499 use core::arch::x86_64::*;
500
501 let len = high.len();
502 debug_assert_eq!(low.len(), len);
503 debug_assert_eq!(up.len(), len);
504 debug_assert_eq!(down.len(), len);
505 if length == 0 || length > len {
506 return;
507 }
508
509 let hi_ptr = high.as_ptr();
510 let lo_ptr = low.as_ptr();
511 let up_ptr = up.as_mut_ptr();
512 let dn_ptr = down.as_mut_ptr();
513
514 let scale = 100.0 / (length as f64);
515 let window = length + 1;
516
517 let sign_mask = _mm512_set1_pd(-0.0);
518 let max_finite = _mm512_set1_pd(f64::MAX);
519
520 #[inline(always)]
521 unsafe fn lanes_all_finite_512(
522 h: __m512d,
523 l: __m512d,
524 sign_mask: __m512d,
525 max_finite: __m512d,
526 ) -> bool {
527 let h_abs = _mm512_andnot_pd(sign_mask, h);
528 let l_abs = _mm512_andnot_pd(sign_mask, l);
529 let ok_h: __mmask8 = _mm512_cmp_pd_mask(h_abs, max_finite, _CMP_LE_OQ);
530 let ok_l: __mmask8 = _mm512_cmp_pd_mask(l_abs, max_finite, _CMP_LE_OQ);
531 (ok_h & ok_l) == 0xFF
532 }
533
534 for i in length..len {
535 let start = i - length;
536 let base_h = hi_ptr.add(start);
537 let base_l = lo_ptr.add(start);
538
539 let mut best_h = core::f64::NEG_INFINITY;
540 let mut best_l = core::f64::INFINITY;
541 let mut best_h_off = 0usize;
542 let mut best_l_off = 0usize;
543
544 let mut j = 0usize;
545 let mut invalid = false;
546
547 while j + 8 <= window {
548 let h8 = _mm512_loadu_pd(base_h.add(j));
549 let l8 = _mm512_loadu_pd(base_l.add(j));
550
551 if !lanes_all_finite_512(h8, l8, sign_mask, max_finite) {
552 invalid = true;
553 break;
554 }
555
556 let mut hv = [0.0f64; 8];
557 let mut lv = [0.0f64; 8];
558 _mm512_storeu_pd(hv.as_mut_ptr(), h8);
559 _mm512_storeu_pd(lv.as_mut_ptr(), l8);
560
561 if hv[0] > best_h {
562 best_h = hv[0];
563 best_h_off = j;
564 }
565 if lv[0] < best_l {
566 best_l = lv[0];
567 best_l_off = j;
568 }
569 if hv[1] > best_h {
570 best_h = hv[1];
571 best_h_off = j + 1;
572 }
573 if lv[1] < best_l {
574 best_l = lv[1];
575 best_l_off = j + 1;
576 }
577 if hv[2] > best_h {
578 best_h = hv[2];
579 best_h_off = j + 2;
580 }
581 if lv[2] < best_l {
582 best_l = lv[2];
583 best_l_off = j + 2;
584 }
585 if hv[3] > best_h {
586 best_h = hv[3];
587 best_h_off = j + 3;
588 }
589 if lv[3] < best_l {
590 best_l = lv[3];
591 best_l_off = j + 3;
592 }
593 if hv[4] > best_h {
594 best_h = hv[4];
595 best_h_off = j + 4;
596 }
597 if lv[4] < best_l {
598 best_l = lv[4];
599 best_l_off = j + 4;
600 }
601 if hv[5] > best_h {
602 best_h = hv[5];
603 best_h_off = j + 5;
604 }
605 if lv[5] < best_l {
606 best_l = lv[5];
607 best_l_off = j + 5;
608 }
609 if hv[6] > best_h {
610 best_h = hv[6];
611 best_h_off = j + 6;
612 }
613 if lv[6] < best_l {
614 best_l = lv[6];
615 best_l_off = j + 6;
616 }
617 if hv[7] > best_h {
618 best_h = hv[7];
619 best_h_off = j + 7;
620 }
621 if lv[7] < best_l {
622 best_l = lv[7];
623 best_l_off = j + 7;
624 }
625
626 j += 8;
627 }
628
629 if !invalid {
630 while j < window {
631 let h = *base_h.add(j);
632 let l = *base_l.add(j);
633 const EXP_MASK: u64 = 0x7ff0_0000_0000_0000;
634 let hb = h.to_bits();
635 let lb = l.to_bits();
636 if (hb & EXP_MASK) == EXP_MASK || (lb & EXP_MASK) == EXP_MASK {
637 invalid = true;
638 break;
639 }
640 if h > best_h {
641 best_h = h;
642 best_h_off = j;
643 }
644 if l < best_l {
645 best_l = l;
646 best_l_off = j;
647 }
648 j += 1;
649 }
650 }
651
652 if invalid {
653 *up_ptr.add(i) = f64::NAN;
654 *dn_ptr.add(i) = f64::NAN;
655 } else {
656 let dist_hi = length - best_h_off;
657 let dist_lo = length - best_l_off;
658 let up_val = (-(dist_hi as f64)).mul_add(scale, 100.0);
659 let dn_val = (-(dist_lo as f64)).mul_add(scale, 100.0);
660 *up_ptr.add(i) = if dist_hi == 0 {
661 100.0
662 } else if dist_hi >= length {
663 0.0
664 } else {
665 up_val
666 };
667 *dn_ptr.add(i) = if dist_lo == 0 {
668 100.0
669 } else if dist_lo >= length {
670 0.0
671 } else {
672 dn_val
673 };
674 }
675 }
676 }
677}
678#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
679#[inline]
680pub fn aroon_avx2(high: &[f64], low: &[f64], length: usize, up: &mut [f64], down: &mut [f64]) {
681 unsafe {
682 use core::arch::x86_64::*;
683
684 let len = high.len();
685 debug_assert_eq!(low.len(), len);
686 debug_assert_eq!(up.len(), len);
687 debug_assert_eq!(down.len(), len);
688 if length == 0 || length > len {
689 return;
690 }
691
692 let hi_ptr = high.as_ptr();
693 let lo_ptr = low.as_ptr();
694 let up_ptr = up.as_mut_ptr();
695 let dn_ptr = down.as_mut_ptr();
696
697 let scale = 100.0 / (length as f64);
698 let window = length + 1;
699
700 let sign_mask = _mm256_set1_pd(-0.0);
701 let max_finite = _mm256_set1_pd(f64::MAX);
702
703 #[inline(always)]
704 unsafe fn lanes_all_finite(
705 h: __m256d,
706 l: __m256d,
707 sign_mask: __m256d,
708 max_finite: __m256d,
709 ) -> bool {
710 let h_abs = _mm256_andnot_pd(sign_mask, h);
711 let l_abs = _mm256_andnot_pd(sign_mask, l);
712 let ok_h = _mm256_cmp_pd(h_abs, max_finite, _CMP_LE_OQ);
713 let ok_l = _mm256_cmp_pd(l_abs, max_finite, _CMP_LE_OQ);
714 let ok = _mm256_and_pd(ok_h, ok_l);
715 _mm256_movemask_pd(ok) == 0b1111
716 }
717
718 for i in length..len {
719 let start = i - length;
720 let base_h = hi_ptr.add(start);
721 let base_l = lo_ptr.add(start);
722
723 let mut best_h = core::f64::NEG_INFINITY;
724 let mut best_l = core::f64::INFINITY;
725 let mut best_h_off = 0usize;
726 let mut best_l_off = 0usize;
727
728 let mut j = 0usize;
729 let mut invalid = false;
730
731 while j + 4 <= window {
732 let h4 = _mm256_loadu_pd(base_h.add(j));
733 let l4 = _mm256_loadu_pd(base_l.add(j));
734
735 if !lanes_all_finite(h4, l4, sign_mask, max_finite) {
736 invalid = true;
737 break;
738 }
739
740 let mut hv = [0.0f64; 4];
741 let mut lv = [0.0f64; 4];
742 _mm256_storeu_pd(hv.as_mut_ptr(), h4);
743 _mm256_storeu_pd(lv.as_mut_ptr(), l4);
744
745 if hv[0] > best_h {
746 best_h = hv[0];
747 best_h_off = j;
748 }
749 if lv[0] < best_l {
750 best_l = lv[0];
751 best_l_off = j;
752 }
753 if hv[1] > best_h {
754 best_h = hv[1];
755 best_h_off = j + 1;
756 }
757 if lv[1] < best_l {
758 best_l = lv[1];
759 best_l_off = j + 1;
760 }
761 if hv[2] > best_h {
762 best_h = hv[2];
763 best_h_off = j + 2;
764 }
765 if lv[2] < best_l {
766 best_l = lv[2];
767 best_l_off = j + 2;
768 }
769 if hv[3] > best_h {
770 best_h = hv[3];
771 best_h_off = j + 3;
772 }
773 if lv[3] < best_l {
774 best_l = lv[3];
775 best_l_off = j + 3;
776 }
777
778 j += 4;
779 }
780
781 if !invalid {
782 while j < window {
783 let h = *base_h.add(j);
784 let l = *base_l.add(j);
785 const EXP_MASK: u64 = 0x7ff0_0000_0000_0000;
786 let hb = h.to_bits();
787 let lb = l.to_bits();
788 if (hb & EXP_MASK) == EXP_MASK || (lb & EXP_MASK) == EXP_MASK {
789 invalid = true;
790 break;
791 }
792 if h > best_h {
793 best_h = h;
794 best_h_off = j;
795 }
796 if l < best_l {
797 best_l = l;
798 best_l_off = j;
799 }
800 j += 1;
801 }
802 }
803
804 if invalid {
805 *up_ptr.add(i) = f64::NAN;
806 *dn_ptr.add(i) = f64::NAN;
807 } else {
808 let dist_hi = length - best_h_off;
809 let dist_lo = length - best_l_off;
810 let up_val = (-(dist_hi as f64)).mul_add(scale, 100.0);
811 let dn_val = (-(dist_lo as f64)).mul_add(scale, 100.0);
812 *up_ptr.add(i) = if dist_hi == 0 {
813 100.0
814 } else if dist_hi >= length {
815 0.0
816 } else {
817 up_val
818 };
819 *dn_ptr.add(i) = if dist_lo == 0 {
820 100.0
821 } else if dist_lo >= length {
822 0.0
823 } else {
824 dn_val
825 };
826 }
827 }
828 }
829}
830#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
831#[inline]
832pub unsafe fn aroon_avx512_short(
833 high: &[f64],
834 low: &[f64],
835 length: usize,
836 up: &mut [f64],
837 down: &mut [f64],
838) {
839 aroon_avx512(high, low, length, up, down)
840}
841#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
842#[inline]
843pub unsafe fn aroon_avx512_long(
844 high: &[f64],
845 low: &[f64],
846 length: usize,
847 up: &mut [f64],
848 down: &mut [f64],
849) {
850 aroon_avx512(high, low, length, up, down)
851}
852
853#[derive(Debug)]
854pub struct AroonStream {
855 length: usize,
856 buf_size: usize,
857 head: usize,
858 count: usize,
859 t: usize,
860 scale_100: f64,
861
862 flags: Vec<u8>,
863 invalid_count: usize,
864
865 maxq: VecDeque<(f64, usize)>,
866 minq: VecDeque<(f64, usize)>,
867}
868
869impl AroonStream {
870 #[inline]
871 pub fn try_new(params: AroonParams) -> Result<Self, AroonError> {
872 let length = params.length.unwrap_or(14);
873 if length == 0 {
874 return Err(AroonError::InvalidLength {
875 length: 0,
876 data_len: 0,
877 });
878 }
879 let buf_size = length + 1;
880 Ok(AroonStream {
881 length,
882 buf_size,
883 head: 0,
884 count: 0,
885 t: 0,
886 scale_100: 100.0 / (length as f64),
887 flags: vec![0u8; buf_size],
888 invalid_count: 0,
889 maxq: VecDeque::with_capacity(buf_size),
890 minq: VecDeque::with_capacity(buf_size),
891 })
892 }
893
894 #[inline(always)]
895 fn pct_from_distance(&self, dist: usize) -> f64 {
896 if dist == 0 {
897 100.0
898 } else if dist >= self.length {
899 0.0
900 } else {
901 (-(dist as f64)).mul_add(self.scale_100, 100.0)
902 }
903 }
904
905 #[inline(always)]
906 pub fn update(&mut self, high: f64, low: f64) -> Option<(f64, f64)> {
907 let i = self.t;
908
909 if self.count == self.buf_size {
910 let old = self.flags[self.head] as usize;
911 self.invalid_count -= old;
912 } else {
913 self.count += 1;
914 }
915
916 let invalid = !(high.is_finite() && low.is_finite());
917 let new_flag = invalid as u8;
918
919 self.flags[self.head] = new_flag;
920 self.invalid_count += new_flag as usize;
921 self.head += 1;
922 if self.head == self.buf_size {
923 self.head = 0;
924 }
925
926 let earliest = i.saturating_sub(self.length);
927 while let Some(&(_, idx)) = self.maxq.front() {
928 if idx < earliest {
929 self.maxq.pop_front();
930 } else {
931 break;
932 }
933 }
934 while let Some(&(_, idx)) = self.minq.front() {
935 if idx < earliest {
936 self.minq.pop_front();
937 } else {
938 break;
939 }
940 }
941
942 if !invalid {
943 while let Some(&(v, _)) = self.maxq.back() {
944 if high > v {
945 self.maxq.pop_back();
946 } else {
947 break;
948 }
949 }
950 self.maxq.push_back((high, i));
951
952 while let Some(&(v, _)) = self.minq.back() {
953 if low < v {
954 self.minq.pop_back();
955 } else {
956 break;
957 }
958 }
959 self.minq.push_back((low, i));
960 }
961
962 let out = if self.count == self.buf_size && self.invalid_count == 0 {
963 debug_assert!(self.maxq.front().is_some() && self.minq.front().is_some());
964 let max_idx = self.maxq.front().unwrap().1;
965 let min_idx = self.minq.front().unwrap().1;
966
967 let dist_hi = i - max_idx;
968 let dist_lo = i - min_idx;
969
970 let up = self.pct_from_distance(dist_hi);
971 let down = self.pct_from_distance(dist_lo);
972 Some((up, down))
973 } else {
974 None
975 };
976
977 self.t = i + 1;
978 out
979 }
980}
981
982#[derive(Clone, Debug)]
983pub struct AroonBatchRange {
984 pub length: (usize, usize, usize),
985}
986impl Default for AroonBatchRange {
987 fn default() -> Self {
988 Self {
989 length: (14, 263, 1),
990 }
991 }
992}
993
994#[inline(always)]
995fn expand_grid_aroon(r: &AroonBatchRange) -> Result<Vec<AroonParams>, AroonError> {
996 fn axis_usize((start, end, step): (usize, usize, usize)) -> Result<Vec<usize>, AroonError> {
997 if step == 0 || start == end {
998 return Ok(vec![start]);
999 }
1000 let mut vals = Vec::new();
1001 if start < end {
1002 let mut v = start;
1003 while v <= end {
1004 vals.push(v);
1005 match v.checked_add(step) {
1006 Some(next) => {
1007 if next == v {
1008 break;
1009 }
1010 v = next;
1011 }
1012 None => break,
1013 }
1014 }
1015 } else {
1016 let mut v = start;
1017 loop {
1018 vals.push(v);
1019 if v == end {
1020 break;
1021 }
1022 let next = v.saturating_sub(step);
1023 if next == v {
1024 break;
1025 }
1026 v = next;
1027 if v < end {
1028 break;
1029 }
1030 }
1031 }
1032 if vals.is_empty() {
1033 return Err(AroonError::InvalidRange { start, end, step });
1034 }
1035 Ok(vals)
1036 }
1037 let lengths = axis_usize(r.length)?;
1038 let mut out = Vec::with_capacity(lengths.len());
1039 for &l in &lengths {
1040 out.push(AroonParams { length: Some(l) });
1041 }
1042 Ok(out)
1043}
1044
1045#[derive(Clone, Debug, Default)]
1046pub struct AroonBatchBuilder {
1047 range: AroonBatchRange,
1048 kernel: Kernel,
1049}
1050impl AroonBatchBuilder {
1051 pub fn new() -> Self {
1052 Self::default()
1053 }
1054 pub fn kernel(mut self, k: Kernel) -> Self {
1055 self.kernel = k;
1056 self
1057 }
1058 #[inline]
1059 pub fn length_range(mut self, start: usize, end: usize, step: usize) -> Self {
1060 self.range.length = (start, end, step);
1061 self
1062 }
1063 #[inline]
1064 pub fn length_static(mut self, x: usize) -> Self {
1065 self.range.length = (x, x, 0);
1066 self
1067 }
1068 pub fn apply_slices(self, high: &[f64], low: &[f64]) -> Result<AroonBatchOutput, AroonError> {
1069 aroon_batch_with_kernel(high, low, &self.range, self.kernel)
1070 }
1071 pub fn with_default_slices(
1072 high: &[f64],
1073 low: &[f64],
1074 k: Kernel,
1075 ) -> Result<AroonBatchOutput, AroonError> {
1076 AroonBatchBuilder::new().kernel(k).apply_slices(high, low)
1077 }
1078 pub fn apply_candles(self, c: &Candles) -> Result<AroonBatchOutput, AroonError> {
1079 self.apply_slices(source_type(c, "high"), source_type(c, "low"))
1080 }
1081 pub fn with_default_candles(c: &Candles) -> Result<AroonBatchOutput, AroonError> {
1082 AroonBatchBuilder::new()
1083 .kernel(Kernel::Auto)
1084 .apply_candles(c)
1085 }
1086}
1087
1088pub struct AroonBatchOutput {
1089 pub up: Vec<f64>,
1090 pub down: Vec<f64>,
1091 pub combos: Vec<AroonParams>,
1092 pub rows: usize,
1093 pub cols: usize,
1094}
1095impl AroonBatchOutput {
1096 pub fn row_for_params(&self, p: &AroonParams) -> Option<usize> {
1097 self.combos
1098 .iter()
1099 .position(|c| c.length.unwrap_or(14) == p.length.unwrap_or(14))
1100 }
1101 pub fn up_for(&self, p: &AroonParams) -> Option<&[f64]> {
1102 self.row_for_params(p).and_then(|row| {
1103 row.checked_mul(self.cols)
1104 .and_then(|start| self.up.get(start..start + self.cols))
1105 })
1106 }
1107 pub fn down_for(&self, p: &AroonParams) -> Option<&[f64]> {
1108 self.row_for_params(p).and_then(|row| {
1109 row.checked_mul(self.cols)
1110 .and_then(|start| self.down.get(start..start + self.cols))
1111 })
1112 }
1113}
1114
1115#[inline(always)]
1116fn expand_grid(r: &AroonBatchRange) -> Vec<AroonParams> {
1117 fn axis_usize((start, end, step): (usize, usize, usize)) -> Vec<usize> {
1118 if step == 0 || start == end {
1119 return vec![start];
1120 }
1121 (start..=end).step_by(step).collect()
1122 }
1123 let lengths = axis_usize(r.length);
1124 let mut out = Vec::with_capacity(lengths.len());
1125 for &l in &lengths {
1126 out.push(AroonParams { length: Some(l) });
1127 }
1128 out
1129}
1130
1131pub fn aroon_batch_with_kernel(
1132 high: &[f64],
1133 low: &[f64],
1134 sweep: &AroonBatchRange,
1135 k: Kernel,
1136) -> Result<AroonBatchOutput, AroonError> {
1137 let kernel = match k {
1138 Kernel::Auto => Kernel::ScalarBatch,
1139 other if other.is_batch() => other,
1140 other => return Err(AroonError::InvalidKernelForBatch(other)),
1141 };
1142 let simd = match kernel {
1143 Kernel::Avx512Batch => Kernel::Avx512,
1144 Kernel::Avx2Batch => Kernel::Avx2,
1145 Kernel::ScalarBatch => Kernel::Scalar,
1146 _ => unreachable!(),
1147 };
1148 aroon_batch_par_slice(high, low, sweep, simd)
1149}
1150
1151#[inline(always)]
1152pub fn aroon_batch_slice(
1153 high: &[f64],
1154 low: &[f64],
1155 sweep: &AroonBatchRange,
1156 kern: Kernel,
1157) -> Result<AroonBatchOutput, AroonError> {
1158 aroon_batch_inner(high, low, sweep, kern, false)
1159}
1160#[inline(always)]
1161pub fn aroon_batch_par_slice(
1162 high: &[f64],
1163 low: &[f64],
1164 sweep: &AroonBatchRange,
1165 kern: Kernel,
1166) -> Result<AroonBatchOutput, AroonError> {
1167 aroon_batch_inner(high, low, sweep, kern, true)
1168}
1169#[inline(always)]
1170fn aroon_batch_inner(
1171 high: &[f64],
1172 low: &[f64],
1173 sweep: &AroonBatchRange,
1174 kern: Kernel,
1175 parallel: bool,
1176) -> Result<AroonBatchOutput, AroonError> {
1177 let combos = expand_grid_aroon(sweep)?;
1178 if high.len() != low.len() {
1179 return Err(AroonError::MismatchSliceLength {
1180 high_len: high.len(),
1181 low_len: low.len(),
1182 });
1183 }
1184 let len = high.len();
1185 let first = first_valid_pair(high, low).ok_or(AroonError::AllValuesNaN)?;
1186 let max_l = combos.iter().map(|c| c.length.unwrap()).max().unwrap();
1187 if len.saturating_sub(first) < max_l {
1188 return Err(AroonError::NotEnoughValidData {
1189 needed: max_l,
1190 valid: len.saturating_sub(first),
1191 });
1192 }
1193 let rows = combos.len();
1194 let cols = len;
1195
1196 let mut buf_up_mu = make_uninit_matrix(rows, cols);
1197 let mut buf_down_mu = make_uninit_matrix(rows, cols);
1198
1199 let warmup_periods: Vec<usize> = combos
1200 .iter()
1201 .map(|c| first.saturating_add(c.length.unwrap()))
1202 .collect();
1203
1204 init_matrix_prefixes(&mut buf_up_mu, cols, &warmup_periods);
1205 init_matrix_prefixes(&mut buf_down_mu, cols, &warmup_periods);
1206
1207 let mut buf_up_guard = ManuallyDrop::new(buf_up_mu);
1208 let mut buf_down_guard = ManuallyDrop::new(buf_down_mu);
1209 let up: &mut [f64] = unsafe {
1210 core::slice::from_raw_parts_mut(buf_up_guard.as_mut_ptr() as *mut f64, buf_up_guard.len())
1211 };
1212 let down: &mut [f64] = unsafe {
1213 core::slice::from_raw_parts_mut(
1214 buf_down_guard.as_mut_ptr() as *mut f64,
1215 buf_down_guard.len(),
1216 )
1217 };
1218
1219 aroon_batch_inner_into(high, low, sweep, kern, parallel, up, down)?;
1220
1221 for (row, &warmup) in warmup_periods.iter().enumerate() {
1222 let row_start = row * cols;
1223 let warm_end = (row_start + warmup).min(row_start + cols);
1224 for i in row_start..warm_end {
1225 up[i] = f64::NAN;
1226 down[i] = f64::NAN;
1227 }
1228 }
1229
1230 let up_values = unsafe {
1231 Vec::from_raw_parts(
1232 buf_up_guard.as_mut_ptr() as *mut f64,
1233 buf_up_guard.len(),
1234 buf_up_guard.capacity(),
1235 )
1236 };
1237 let down_values = unsafe {
1238 Vec::from_raw_parts(
1239 buf_down_guard.as_mut_ptr() as *mut f64,
1240 buf_down_guard.len(),
1241 buf_down_guard.capacity(),
1242 )
1243 };
1244
1245 Ok(AroonBatchOutput {
1246 up: up_values,
1247 down: down_values,
1248 combos,
1249 rows,
1250 cols,
1251 })
1252}
1253
1254#[inline(always)]
1255fn aroon_batch_inner_into(
1256 high: &[f64],
1257 low: &[f64],
1258 sweep: &AroonBatchRange,
1259 kern: Kernel,
1260 parallel: bool,
1261 out_up: &mut [f64],
1262 out_down: &mut [f64],
1263) -> Result<Vec<AroonParams>, AroonError> {
1264 let combos = expand_grid_aroon(sweep)?;
1265 if high.len() != low.len() {
1266 return Err(AroonError::MismatchSliceLength {
1267 high_len: high.len(),
1268 low_len: low.len(),
1269 });
1270 }
1271 let len = high.len();
1272 let first = first_valid_pair(high, low).ok_or(AroonError::AllValuesNaN)?;
1273 let max_l = combos.iter().map(|c| c.length.unwrap()).max().unwrap();
1274 if len.saturating_sub(first) < max_l {
1275 return Err(AroonError::NotEnoughValidData {
1276 needed: max_l,
1277 valid: len.saturating_sub(first),
1278 });
1279 }
1280
1281 let rows = combos.len();
1282 let cols = len;
1283 let expected = rows.checked_mul(cols).ok_or(AroonError::InvalidRange {
1284 start: sweep.length.0,
1285 end: sweep.length.1,
1286 step: sweep.length.2,
1287 })?;
1288
1289 if out_up.len() != expected || out_down.len() != expected {
1290 return Err(AroonError::OutputLengthMismatch {
1291 expected,
1292 got: out_up.len().max(out_down.len()),
1293 });
1294 }
1295
1296 let first = first_valid_pair(high, low).ok_or(AroonError::AllValuesNaN)?;
1297
1298 let warmup_periods: Vec<usize> = combos.iter().map(|c| first + c.length.unwrap()).collect();
1299
1300 for (row, &warmup) in warmup_periods.iter().enumerate() {
1301 let row_start = row * cols;
1302 for i in 0..warmup.min(cols) {
1303 out_up[row_start + i] = f64::NAN;
1304 out_down[row_start + i] = f64::NAN;
1305 }
1306 }
1307
1308 let do_row = |row: usize, out_up: &mut [f64], out_down: &mut [f64]| unsafe {
1309 let length = combos[row].length.unwrap();
1310 match kern {
1311 Kernel::Scalar => aroon_row_scalar(high, low, length, out_up, out_down),
1312 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1313 Kernel::Avx2 => aroon_row_avx2(high, low, length, out_up, out_down),
1314 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1315 Kernel::Avx512 => aroon_row_avx512(high, low, length, out_up, out_down),
1316 _ => unreachable!(),
1317 }
1318 };
1319 if parallel {
1320 #[cfg(not(target_arch = "wasm32"))]
1321 {
1322 out_up
1323 .par_chunks_mut(cols)
1324 .zip(out_down.par_chunks_mut(cols))
1325 .enumerate()
1326 .for_each(|(row, (u, d))| do_row(row, u, d));
1327 }
1328
1329 #[cfg(target_arch = "wasm32")]
1330 {
1331 for (row, (u, d)) in out_up
1332 .chunks_mut(cols)
1333 .zip(out_down.chunks_mut(cols))
1334 .enumerate()
1335 {
1336 do_row(row, u, d);
1337 }
1338 }
1339 } else {
1340 for (row, (u, d)) in out_up
1341 .chunks_mut(cols)
1342 .zip(out_down.chunks_mut(cols))
1343 .enumerate()
1344 {
1345 do_row(row, u, d);
1346 }
1347 }
1348
1349 for (row, &warmup) in warmup_periods.iter().enumerate() {
1350 let row_start = row * cols;
1351 let warm_end = (row_start + warmup).min(row_start + cols);
1352 for i in row_start..warm_end {
1353 out_up[i] = f64::NAN;
1354 out_down[i] = f64::NAN;
1355 }
1356 }
1357
1358 Ok(combos)
1359}
1360
1361#[cfg(all(feature = "python", feature = "cuda"))]
1362#[pyfunction(name = "aroon_cuda_batch_dev")]
1363#[pyo3(signature = (high_f32, low_f32, length_range, device_id=0))]
1364pub fn aroon_cuda_batch_dev_py<'py>(
1365 py: Python<'py>,
1366 high_f32: numpy::PyReadonlyArray1<'py, f32>,
1367 low_f32: numpy::PyReadonlyArray1<'py, f32>,
1368 length_range: (usize, usize, usize),
1369 device_id: usize,
1370) -> PyResult<(AroonDeviceArrayF32Py, AroonDeviceArrayF32Py)> {
1371 if !cuda_available() {
1372 return Err(PyValueError::new_err("CUDA not available"));
1373 }
1374 let h = high_f32.as_slice()?;
1375 let l = low_f32.as_slice()?;
1376 let sweep = AroonBatchRange {
1377 length: length_range,
1378 };
1379 let (up_dev, dn_dev, ctx_arc, dev_id) = py.allow_threads(|| {
1380 let cuda = CudaAroon::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
1381 let res = cuda
1382 .aroon_batch_dev(h, l, &sweep)
1383 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1384 Ok::<_, PyErr>((
1385 res.outputs.first,
1386 res.outputs.second,
1387 cuda.context_arc_clone(),
1388 cuda.device_id(),
1389 ))
1390 })?;
1391 Ok((
1392 AroonDeviceArrayF32Py {
1393 inner: up_dev,
1394 _ctx: ctx_arc.clone(),
1395 device_id: dev_id,
1396 },
1397 AroonDeviceArrayF32Py {
1398 inner: dn_dev,
1399 _ctx: ctx_arc,
1400 device_id: dev_id,
1401 },
1402 ))
1403}
1404
1405#[cfg(all(feature = "python", feature = "cuda"))]
1406#[pyfunction(name = "aroon_cuda_many_series_one_param_dev")]
1407#[pyo3(signature = (high_tm_f32, low_tm_f32, length, device_id=0))]
1408pub fn aroon_cuda_many_series_one_param_dev_py<'py>(
1409 py: Python<'py>,
1410 high_tm_f32: numpy::PyReadonlyArray2<'py, f32>,
1411 low_tm_f32: numpy::PyReadonlyArray2<'py, f32>,
1412 length: usize,
1413 device_id: usize,
1414) -> PyResult<(AroonDeviceArrayF32Py, AroonDeviceArrayF32Py)> {
1415 if !cuda_available() {
1416 return Err(PyValueError::new_err("CUDA not available"));
1417 }
1418 let shape = high_tm_f32.shape();
1419 if shape.len() != 2 || low_tm_f32.shape() != shape {
1420 return Err(PyValueError::new_err("expected two matching 2D arrays"));
1421 }
1422 let rows = shape[0];
1423 let cols = shape[1];
1424 let h = high_tm_f32.as_slice()?;
1425 let l = low_tm_f32.as_slice()?;
1426 let (up_dev, dn_dev, ctx_arc, dev_id) = py.allow_threads(|| {
1427 let cuda = CudaAroon::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
1428 let pair = cuda
1429 .aroon_many_series_one_param_time_major_dev(h, l, cols, rows, length)
1430 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1431 Ok::<_, PyErr>((
1432 pair.first,
1433 pair.second,
1434 cuda.context_arc_clone(),
1435 cuda.device_id(),
1436 ))
1437 })?;
1438 Ok((
1439 AroonDeviceArrayF32Py {
1440 inner: up_dev,
1441 _ctx: ctx_arc.clone(),
1442 device_id: dev_id,
1443 },
1444 AroonDeviceArrayF32Py {
1445 inner: dn_dev,
1446 _ctx: ctx_arc,
1447 device_id: dev_id,
1448 },
1449 ))
1450}
1451
1452#[cfg(all(feature = "python", feature = "cuda"))]
1453#[pyclass(
1454 module = "ta_indicators.cuda",
1455 name = "AroonDeviceArrayF32",
1456 unsendable
1457)]
1458pub struct AroonDeviceArrayF32Py {
1459 pub(crate) inner: crate::cuda::moving_averages::alma_wrapper::DeviceArrayF32,
1460 pub(crate) _ctx: std::sync::Arc<cust::context::Context>,
1461 pub(crate) device_id: u32,
1462}
1463
1464#[cfg(all(feature = "python", feature = "cuda"))]
1465#[pymethods]
1466impl AroonDeviceArrayF32Py {
1467 #[getter]
1468 fn __cuda_array_interface__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
1469 let d = PyDict::new(py);
1470 d.set_item("shape", (self.inner.rows, self.inner.cols))?;
1471 d.set_item("typestr", "<f4")?;
1472 d.set_item(
1473 "strides",
1474 (
1475 self.inner.cols * std::mem::size_of::<f32>(),
1476 std::mem::size_of::<f32>(),
1477 ),
1478 )?;
1479 d.set_item("data", (self.inner.device_ptr() as usize, false))?;
1480
1481 d.set_item("version", 3)?;
1482 Ok(d)
1483 }
1484
1485 fn __dlpack_device__(&self) -> (i32, i32) {
1486 (2, self.device_id as i32)
1487 }
1488
1489 #[pyo3(signature=(stream=None, max_version=None, dl_device=None, copy=None))]
1490 fn __dlpack__<'py>(
1491 &mut self,
1492 py: Python<'py>,
1493 stream: Option<pyo3::PyObject>,
1494 max_version: Option<pyo3::PyObject>,
1495 dl_device: Option<pyo3::PyObject>,
1496 copy: Option<pyo3::PyObject>,
1497 ) -> PyResult<PyObject> {
1498 use cust::memory::DeviceBuffer;
1499 use pyo3::types::PyAny;
1500 use pyo3::Bound;
1501
1502 let (dev_ty, alloc_dev) = self.__dlpack_device__();
1503 if let Some(dev_obj) = dl_device.as_ref() {
1504 if let Ok((want_ty, want_dev)) = dev_obj.extract::<(i32, i32)>(py) {
1505 if want_ty != dev_ty || want_dev != alloc_dev {
1506 let wants_copy = copy
1507 .as_ref()
1508 .and_then(|c| c.extract::<bool>(py).ok())
1509 .unwrap_or(false);
1510 if wants_copy {
1511 return Err(PyValueError::new_err(
1512 "device copy not implemented for __dlpack__",
1513 ));
1514 } else {
1515 return Err(PyValueError::new_err("dl_device mismatch for __dlpack__"));
1516 }
1517 }
1518 }
1519 }
1520 let _ = stream;
1521
1522 let dummy =
1523 DeviceBuffer::from_slice(&[]).map_err(|e| PyValueError::new_err(e.to_string()))?;
1524 let inner = std::mem::replace(
1525 &mut self.inner,
1526 crate::cuda::moving_averages::alma_wrapper::DeviceArrayF32 {
1527 buf: dummy,
1528 rows: 0,
1529 cols: 0,
1530 },
1531 );
1532 let rows = inner.rows;
1533 let cols = inner.cols;
1534 let buf = inner.buf;
1535
1536 let max_version_bound: Option<Bound<'py, PyAny>> =
1537 max_version.map(|obj| obj.into_bound(py));
1538
1539 export_f32_cuda_dlpack_2d(py, buf, rows, cols, alloc_dev, max_version_bound)
1540 }
1541}
1542
1543#[inline(always)]
1544pub unsafe fn aroon_row_scalar(
1545 high: &[f64],
1546 low: &[f64],
1547 length: usize,
1548 out_up: &mut [f64],
1549 out_down: &mut [f64],
1550) {
1551 aroon_scalar(high, low, length, out_up, out_down)
1552}
1553
1554#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1555#[inline(always)]
1556pub unsafe fn aroon_row_avx2(
1557 high: &[f64],
1558 low: &[f64],
1559 length: usize,
1560 out_up: &mut [f64],
1561 out_down: &mut [f64],
1562) {
1563 aroon_avx2(high, low, length, out_up, out_down)
1564}
1565
1566#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1567#[inline(always)]
1568pub unsafe fn aroon_row_avx512(
1569 high: &[f64],
1570 low: &[f64],
1571 length: usize,
1572 out_up: &mut [f64],
1573 out_down: &mut [f64],
1574) {
1575 if length <= 32 {
1576 aroon_row_avx512_short(high, low, length, out_up, out_down)
1577 } else {
1578 aroon_row_avx512_long(high, low, length, out_up, out_down)
1579 }
1580}
1581#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1582#[inline(always)]
1583pub unsafe fn aroon_row_avx512_short(
1584 high: &[f64],
1585 low: &[f64],
1586 length: usize,
1587 out_up: &mut [f64],
1588 out_down: &mut [f64],
1589) {
1590 aroon_avx512(high, low, length, out_up, out_down)
1591}
1592#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1593#[inline(always)]
1594pub unsafe fn aroon_row_avx512_long(
1595 high: &[f64],
1596 low: &[f64],
1597 length: usize,
1598 out_up: &mut [f64],
1599 out_down: &mut [f64],
1600) {
1601 aroon_avx512(high, low, length, out_up, out_down)
1602}
1603#[cfg(test)]
1604mod tests {
1605 use super::*;
1606 use crate::skip_if_unsupported;
1607 use crate::utilities::data_loader::read_candles_from_csv;
1608 use crate::utilities::enums::Kernel;
1609
1610 fn check_aroon_partial_params(
1611 test_name: &str,
1612 kernel: Kernel,
1613 ) -> Result<(), Box<dyn std::error::Error>> {
1614 skip_if_unsupported!(kernel, test_name);
1615 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1616 let candles = read_candles_from_csv(file_path)?;
1617 let partial_params = AroonParams { length: None };
1618 let input = AroonInput::from_candles(&candles, partial_params);
1619 let result = aroon_with_kernel(&input, kernel)?;
1620 assert_eq!(result.aroon_up.len(), candles.close.len());
1621 assert_eq!(result.aroon_down.len(), candles.close.len());
1622 Ok(())
1623 }
1624
1625 fn check_aroon_accuracy(
1626 test_name: &str,
1627 kernel: Kernel,
1628 ) -> Result<(), Box<dyn std::error::Error>> {
1629 skip_if_unsupported!(kernel, test_name);
1630 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1631 let candles = read_candles_from_csv(file_path)?;
1632 let input = AroonInput::with_default_candles(&candles);
1633 let result = aroon_with_kernel(&input, kernel)?;
1634
1635 let expected_up_last_five = [21.43, 14.29, 7.14, 0.0, 0.0];
1636 let expected_down_last_five = [71.43, 64.29, 57.14, 50.0, 42.86];
1637
1638 assert!(
1639 result.aroon_up.len() >= 5 && result.aroon_down.len() >= 5,
1640 "Not enough Aroon values"
1641 );
1642
1643 let start_index = result.aroon_up.len().saturating_sub(5);
1644
1645 let up_last_five = &result.aroon_up[start_index..];
1646 let down_last_five = &result.aroon_down[start_index..];
1647
1648 for (i, &value) in up_last_five.iter().enumerate() {
1649 assert!(
1650 (value - expected_up_last_five[i]).abs() < 1e-2,
1651 "Aroon Up mismatch at index {}: expected {}, got {}",
1652 i,
1653 expected_up_last_five[i],
1654 value
1655 );
1656 }
1657
1658 for (i, &value) in down_last_five.iter().enumerate() {
1659 assert!(
1660 (value - expected_down_last_five[i]).abs() < 1e-2,
1661 "Aroon Down mismatch at index {}: expected {}, got {}",
1662 i,
1663 expected_down_last_five[i],
1664 value
1665 );
1666 }
1667
1668 Ok(())
1669 }
1670
1671 fn check_aroon_default_candles(
1672 test_name: &str,
1673 kernel: Kernel,
1674 ) -> Result<(), Box<dyn std::error::Error>> {
1675 skip_if_unsupported!(kernel, test_name);
1676 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1677 let candles = read_candles_from_csv(file_path)?;
1678 let input = AroonInput::with_default_candles(&candles);
1679 match input.data {
1680 AroonData::Candles { .. } => {}
1681 _ => panic!("Expected AroonData::Candles variant"),
1682 }
1683 let result = aroon_with_kernel(&input, kernel)?;
1684 assert_eq!(result.aroon_up.len(), candles.close.len());
1685 assert_eq!(result.aroon_down.len(), candles.close.len());
1686 Ok(())
1687 }
1688
1689 fn check_aroon_zero_length(
1690 test_name: &str,
1691 kernel: Kernel,
1692 ) -> Result<(), Box<dyn std::error::Error>> {
1693 skip_if_unsupported!(kernel, test_name);
1694 let high = [10.0, 11.0, 12.0];
1695 let low = [9.0, 10.0, 11.0];
1696 let params = AroonParams { length: Some(0) };
1697 let input = AroonInput::from_slices_hl(&high, &low, params);
1698 let result = aroon_with_kernel(&input, kernel);
1699 assert!(result.is_err(), "Expected error for zero length");
1700 Ok(())
1701 }
1702
1703 fn check_aroon_length_exceeds_data(
1704 test_name: &str,
1705 kernel: Kernel,
1706 ) -> Result<(), Box<dyn std::error::Error>> {
1707 skip_if_unsupported!(kernel, test_name);
1708 let high = [10.0, 11.0, 12.0];
1709 let low = [9.0, 10.0, 11.0];
1710 let params = AroonParams { length: Some(14) };
1711 let input = AroonInput::from_slices_hl(&high, &low, params);
1712 let result = aroon_with_kernel(&input, kernel);
1713 assert!(result.is_err(), "Expected error for length > data.len()");
1714 Ok(())
1715 }
1716
1717 fn check_aroon_very_small_data_set(
1718 test_name: &str,
1719 kernel: Kernel,
1720 ) -> Result<(), Box<dyn std::error::Error>> {
1721 skip_if_unsupported!(kernel, test_name);
1722 let high = [100.0];
1723 let low = [99.5];
1724 let params = AroonParams { length: Some(14) };
1725 let input = AroonInput::from_slices_hl(&high, &low, params);
1726 let result = aroon_with_kernel(&input, kernel);
1727 assert!(
1728 result.is_err(),
1729 "Expected error for data smaller than length"
1730 );
1731 Ok(())
1732 }
1733
1734 fn check_aroon_reinput(
1735 test_name: &str,
1736 kernel: Kernel,
1737 ) -> Result<(), Box<dyn std::error::Error>> {
1738 skip_if_unsupported!(kernel, test_name);
1739 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1740 let candles = read_candles_from_csv(file_path)?;
1741 let first_params = AroonParams { length: Some(14) };
1742 let first_input = AroonInput::from_candles(&candles, first_params);
1743 let first_result = aroon_with_kernel(&first_input, kernel)?;
1744 assert_eq!(first_result.aroon_up.len(), candles.close.len());
1745 assert_eq!(first_result.aroon_down.len(), candles.close.len());
1746 let second_params = AroonParams { length: Some(5) };
1747 let second_input = AroonInput::from_slices_hl(&candles.high, &candles.low, second_params);
1748 let second_result = aroon_with_kernel(&second_input, kernel)?;
1749 assert_eq!(second_result.aroon_up.len(), candles.close.len());
1750 assert_eq!(second_result.aroon_down.len(), candles.close.len());
1751 Ok(())
1752 }
1753
1754 fn check_aroon_nan_handling(
1755 test_name: &str,
1756 kernel: Kernel,
1757 ) -> Result<(), Box<dyn std::error::Error>> {
1758 skip_if_unsupported!(kernel, test_name);
1759 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1760 let candles = read_candles_from_csv(file_path)?;
1761 let params = AroonParams { length: Some(14) };
1762 let input = AroonInput::from_candles(&candles, params);
1763 let result = aroon_with_kernel(&input, kernel)?;
1764 assert_eq!(result.aroon_up.len(), candles.close.len());
1765 assert_eq!(result.aroon_down.len(), candles.close.len());
1766 if result.aroon_up.len() > 240 {
1767 for i in 240..result.aroon_up.len() {
1768 assert!(
1769 !result.aroon_up[i].is_nan(),
1770 "Found NaN in aroon_up at {}",
1771 i
1772 );
1773 assert!(
1774 !result.aroon_down[i].is_nan(),
1775 "Found NaN in aroon_down at {}",
1776 i
1777 );
1778 }
1779 }
1780 Ok(())
1781 }
1782
1783 fn check_aroon_streaming(
1784 test_name: &str,
1785 kernel: Kernel,
1786 ) -> Result<(), Box<dyn std::error::Error>> {
1787 skip_if_unsupported!(kernel, test_name);
1788 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1789 let candles = read_candles_from_csv(file_path)?;
1790 let length = 14;
1791
1792 let input = AroonInput::from_candles(
1793 &candles,
1794 AroonParams {
1795 length: Some(length),
1796 },
1797 );
1798 let batch_output = aroon_with_kernel(&input, kernel)?;
1799
1800 let mut stream = AroonStream::try_new(AroonParams {
1801 length: Some(length),
1802 })?;
1803 let mut stream_up = Vec::with_capacity(candles.close.len());
1804 let mut stream_down = Vec::with_capacity(candles.close.len());
1805 for (&h, &l) in candles.high.iter().zip(&candles.low) {
1806 match stream.update(h, l) {
1807 Some((up, down)) => {
1808 stream_up.push(up);
1809 stream_down.push(down);
1810 }
1811 None => {
1812 stream_up.push(f64::NAN);
1813 stream_down.push(f64::NAN);
1814 }
1815 }
1816 }
1817 assert_eq!(batch_output.aroon_up.len(), stream_up.len());
1818 assert_eq!(batch_output.aroon_down.len(), stream_down.len());
1819 for (i, (&b, &s)) in batch_output.aroon_up.iter().zip(&stream_up).enumerate() {
1820 if b.is_nan() && s.is_nan() {
1821 continue;
1822 }
1823 let diff = (b - s).abs();
1824 assert!(
1825 diff < 1e-8,
1826 "[{}] Aroon streaming f64 mismatch at idx {}: batch={}, stream={}, diff={}",
1827 test_name,
1828 i,
1829 b,
1830 s,
1831 diff
1832 );
1833 }
1834 for (i, (&b, &s)) in batch_output.aroon_down.iter().zip(&stream_down).enumerate() {
1835 if b.is_nan() && s.is_nan() {
1836 continue;
1837 }
1838 let diff = (b - s).abs();
1839 assert!(
1840 diff < 1e-8,
1841 "[{}] Aroon streaming f64 mismatch at idx {}: batch={}, stream={}, diff={}",
1842 test_name,
1843 i,
1844 b,
1845 s,
1846 diff
1847 );
1848 }
1849 Ok(())
1850 }
1851
1852 #[cfg(debug_assertions)]
1853 fn check_aroon_no_poison(
1854 test_name: &str,
1855 kernel: Kernel,
1856 ) -> Result<(), Box<dyn std::error::Error>> {
1857 skip_if_unsupported!(kernel, test_name);
1858
1859 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1860 let candles = read_candles_from_csv(file_path)?;
1861
1862 let test_params = vec![
1863 AroonParams::default(),
1864 AroonParams { length: Some(1) },
1865 AroonParams { length: Some(2) },
1866 AroonParams { length: Some(5) },
1867 AroonParams { length: Some(10) },
1868 AroonParams { length: Some(20) },
1869 AroonParams { length: Some(50) },
1870 AroonParams { length: Some(100) },
1871 AroonParams { length: Some(200) },
1872 ];
1873
1874 for (param_idx, params) in test_params.iter().enumerate() {
1875 let input = AroonInput::from_candles(&candles, params.clone());
1876 let output = aroon_with_kernel(&input, kernel)?;
1877
1878 for (i, &val) in output.aroon_up.iter().enumerate() {
1879 if val.is_nan() {
1880 continue;
1881 }
1882
1883 let bits = val.to_bits();
1884
1885 if bits == 0x11111111_11111111 {
1886 panic!(
1887 "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at index {} \
1888 in aroon_up output with params: length={} (param set {})",
1889 test_name,
1890 val,
1891 bits,
1892 i,
1893 params.length.unwrap_or(14),
1894 param_idx
1895 );
1896 }
1897
1898 if bits == 0x22222222_22222222 {
1899 panic!(
1900 "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at index {} \
1901 in aroon_up output with params: length={} (param set {})",
1902 test_name,
1903 val,
1904 bits,
1905 i,
1906 params.length.unwrap_or(14),
1907 param_idx
1908 );
1909 }
1910
1911 if bits == 0x33333333_33333333 {
1912 panic!(
1913 "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at index {} \
1914 in aroon_up output with params: length={} (param set {})",
1915 test_name,
1916 val,
1917 bits,
1918 i,
1919 params.length.unwrap_or(14),
1920 param_idx
1921 );
1922 }
1923 }
1924
1925 for (i, &val) in output.aroon_down.iter().enumerate() {
1926 if val.is_nan() {
1927 continue;
1928 }
1929
1930 let bits = val.to_bits();
1931
1932 if bits == 0x11111111_11111111 {
1933 panic!(
1934 "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at index {} \
1935 in aroon_down output with params: length={} (param set {})",
1936 test_name,
1937 val,
1938 bits,
1939 i,
1940 params.length.unwrap_or(14),
1941 param_idx
1942 );
1943 }
1944
1945 if bits == 0x22222222_22222222 {
1946 panic!(
1947 "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at index {} \
1948 in aroon_down output with params: length={} (param set {})",
1949 test_name,
1950 val,
1951 bits,
1952 i,
1953 params.length.unwrap_or(14),
1954 param_idx
1955 );
1956 }
1957
1958 if bits == 0x33333333_33333333 {
1959 panic!(
1960 "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at index {} \
1961 in aroon_down output with params: length={} (param set {})",
1962 test_name,
1963 val,
1964 bits,
1965 i,
1966 params.length.unwrap_or(14),
1967 param_idx
1968 );
1969 }
1970 }
1971 }
1972
1973 Ok(())
1974 }
1975
1976 #[cfg(not(debug_assertions))]
1977 fn check_aroon_no_poison(
1978 _test_name: &str,
1979 _kernel: Kernel,
1980 ) -> Result<(), Box<dyn std::error::Error>> {
1981 Ok(())
1982 }
1983
1984 #[cfg(feature = "proptest")]
1985 #[allow(clippy::float_cmp)]
1986 fn check_aroon_property(
1987 test_name: &str,
1988 kernel: Kernel,
1989 ) -> Result<(), Box<dyn std::error::Error>> {
1990 use proptest::prelude::*;
1991 skip_if_unsupported!(kernel, test_name);
1992
1993 let strat = (1usize..=100).prop_flat_map(|length| {
1994 (
1995 prop::collection::vec(
1996 (-1e6f64..1e6f64)
1997 .prop_filter("finite", |x| x.is_finite())
1998 .prop_flat_map(|base| {
1999 (0.0f64..0.3f64).prop_map(move |volatility| {
2000 let range = base.abs() * volatility + 0.01;
2001 let mid = base;
2002 let high = mid + range;
2003 let low = mid - range;
2004 (high, low)
2005 })
2006 }),
2007 length..400,
2008 ),
2009 Just(length),
2010 )
2011 });
2012
2013 proptest::test_runner::TestRunner::default()
2014 .run(&strat, |(bars, length)| {
2015 let (highs, lows): (Vec<f64>, Vec<f64>) = bars.into_iter().unzip();
2016
2017 let params = AroonParams {
2018 length: Some(length),
2019 };
2020 let input = AroonInput::from_slices_hl(&highs, &lows, params.clone());
2021
2022 let AroonOutput {
2023 aroon_up: out_up,
2024 aroon_down: out_down,
2025 } = aroon_with_kernel(&input, kernel).unwrap();
2026 let AroonOutput {
2027 aroon_up: ref_up,
2028 aroon_down: ref_down,
2029 } = aroon_with_kernel(&input, Kernel::Scalar).unwrap();
2030
2031 prop_assert_eq!(out_up.len(), highs.len());
2032 prop_assert_eq!(out_down.len(), lows.len());
2033
2034 for i in 0..length.min(out_up.len()) {
2035 prop_assert!(out_up[i].is_nan());
2036 prop_assert!(out_down[i].is_nan());
2037 }
2038
2039 for i in length..out_up.len() {
2040 prop_assert!(!out_up[i].is_nan());
2041 prop_assert!(!out_down[i].is_nan());
2042 }
2043
2044 for i in length..out_up.len() {
2045 prop_assert!(
2046 out_up[i] >= 0.0 && out_up[i] <= 100.0,
2047 "Aroon up at {} = {}, outside [0,100]",
2048 i,
2049 out_up[i]
2050 );
2051 prop_assert!(
2052 out_down[i] >= 0.0 && out_down[i] <= 100.0,
2053 "Aroon down at {} = {}, outside [0,100]",
2054 i,
2055 out_down[i]
2056 );
2057 }
2058
2059 for i in length..out_up.len().min(length + 5) {
2060 let window_start = i - length;
2061 let mut max_val = highs[window_start];
2062 let mut max_idx = window_start;
2063 let mut min_val = lows[window_start];
2064 let mut min_idx = window_start;
2065
2066 for j in (window_start + 1)..=i {
2067 if highs[j] > max_val {
2068 max_val = highs[j];
2069 max_idx = j;
2070 }
2071 if lows[j] < min_val {
2072 min_val = lows[j];
2073 min_idx = j;
2074 }
2075 }
2076
2077 let periods_since_high = i - max_idx;
2078 let periods_since_low = i - min_idx;
2079 let expected_up =
2080 ((length as f64 - periods_since_high as f64) / length as f64) * 100.0;
2081 let expected_down =
2082 ((length as f64 - periods_since_low as f64) / length as f64) * 100.0;
2083
2084 prop_assert!(
2085 (out_up[i] - expected_up).abs() < 1e-9,
2086 "Formula mismatch for aroon_up at {}: expected {}, got {}",
2087 i,
2088 expected_up,
2089 out_up[i]
2090 );
2091 prop_assert!(
2092 (out_down[i] - expected_down).abs() < 1e-9,
2093 "Formula mismatch for aroon_down at {}: expected {}, got {}",
2094 i,
2095 expected_down,
2096 out_down[i]
2097 );
2098 }
2099
2100 if length == 1 {
2101 for i in 1..out_up.len().min(10) {
2102 prop_assert!(
2103 out_up[i] == 0.0 || out_up[i] == 100.0,
2104 "With length=1, aroon_up must be exactly 0 or 100, got {} at {}",
2105 out_up[i],
2106 i
2107 );
2108 prop_assert!(
2109 out_down[i] == 0.0 || out_down[i] == 100.0,
2110 "With length=1, aroon_down must be exactly 0 or 100, got {} at {}",
2111 out_down[i],
2112 i
2113 );
2114
2115 if i > 0 && i < highs.len() {
2116 if highs[i] > highs[i - 1] {
2117 prop_assert_eq!(
2118 out_up[i],
2119 100.0,
2120 "When high[{}]={} > high[{}]={}, aroon_up should be 100",
2121 i,
2122 highs[i],
2123 i - 1,
2124 highs[i - 1]
2125 );
2126 }
2127
2128 if lows[i] < lows[i - 1] {
2129 prop_assert_eq!(
2130 out_down[i],
2131 100.0,
2132 "When low[{}]={} < low[{}]={}, aroon_down should be 100",
2133 i,
2134 lows[i],
2135 i - 1,
2136 lows[i - 1]
2137 );
2138 }
2139 }
2140 }
2141 }
2142
2143 let is_constant = highs.windows(2).all(|w| (w[0] - w[1]).abs() < 1e-10)
2144 && lows.windows(2).all(|w| (w[0] - w[1]).abs() < 1e-10);
2145
2146 if is_constant && length > 1 {
2147 for i in (length * 2).min(out_up.len())..(length * 3).min(out_up.len()) {
2148 prop_assert!(
2149 out_up[i] <= 100.0 / length as f64 + 1e-9,
2150 "With constant prices, aroon_up should approach 0, got {} at {}",
2151 out_up[i],
2152 i
2153 );
2154 prop_assert!(
2155 out_down[i] <= 100.0 / length as f64 + 1e-9,
2156 "With constant prices, aroon_down should approach 0, got {} at {}",
2157 out_down[i],
2158 i
2159 );
2160 }
2161 }
2162
2163 prop_assert_eq!(out_up.len(), ref_up.len());
2164 prop_assert_eq!(out_down.len(), ref_down.len());
2165
2166 for i in 0..out_up.len() {
2167 let y_up = out_up[i];
2168 let r_up = ref_up[i];
2169 let y_down = out_down[i];
2170 let r_down = ref_down[i];
2171
2172 if !y_up.is_finite() || !r_up.is_finite() {
2173 prop_assert_eq!(y_up.to_bits(), r_up.to_bits());
2174 } else {
2175 let ulp_diff = y_up.to_bits().abs_diff(r_up.to_bits());
2176 prop_assert!(
2177 (y_up - r_up).abs() <= 1e-9 || ulp_diff <= 4,
2178 "Kernel mismatch for aroon_up at {}: {} vs {} (ULP={})",
2179 i,
2180 y_up,
2181 r_up,
2182 ulp_diff
2183 );
2184 }
2185
2186 if !y_down.is_finite() || !r_down.is_finite() {
2187 prop_assert_eq!(y_down.to_bits(), r_down.to_bits());
2188 } else {
2189 let ulp_diff = y_down.to_bits().abs_diff(r_down.to_bits());
2190 prop_assert!(
2191 (y_down - r_down).abs() <= 1e-9 || ulp_diff <= 4,
2192 "Kernel mismatch for aroon_down at {}: {} vs {} (ULP={})",
2193 i,
2194 y_down,
2195 r_down,
2196 ulp_diff
2197 );
2198 }
2199 }
2200
2201 for i in (length + 10)..(out_up.len().min(length + 15)) {
2202 let window_start = i - length;
2203
2204 let mut max_idx = window_start;
2205 for j in (window_start + 1)..=i {
2206 if highs[j] > highs[max_idx] {
2207 max_idx = j;
2208 }
2209 }
2210
2211 if i + 1 < out_up.len() && max_idx < i {
2212 let next_window_start = i + 1 - length;
2213 let mut next_max_idx = next_window_start;
2214 for j in (next_window_start + 1)..=i + 1 {
2215 if j < highs.len() && highs[j] > highs[next_max_idx] {
2216 next_max_idx = j;
2217 }
2218 }
2219
2220 if next_max_idx == max_idx {
2221 prop_assert!(
2222 out_up[i + 1] <= out_up[i] + 1e-9,
2223 "Monotonicity: Aroon up should decrease as extreme ages: {} -> {}",
2224 out_up[i],
2225 out_up[i + 1]
2226 );
2227 }
2228 }
2229 }
2230
2231 for i in 0..highs.len() {
2232 prop_assert!(
2233 highs[i] >= lows[i],
2234 "Data integrity: High {} < Low {} at index {}",
2235 highs[i],
2236 lows[i],
2237 i
2238 );
2239 }
2240
2241 #[cfg(debug_assertions)]
2242 {
2243 for (i, &val) in out_up.iter().enumerate() {
2244 if val.is_finite() {
2245 let bits = val.to_bits();
2246 prop_assert!(
2247 bits != 0x11111111_11111111
2248 && bits != 0x22222222_22222222
2249 && bits != 0x33333333_33333333,
2250 "Found poison value {} (0x{:016X}) at {} in aroon_up",
2251 val,
2252 bits,
2253 i
2254 );
2255 }
2256 }
2257 for (i, &val) in out_down.iter().enumerate() {
2258 if val.is_finite() {
2259 let bits = val.to_bits();
2260 prop_assert!(
2261 bits != 0x11111111_11111111
2262 && bits != 0x22222222_22222222
2263 && bits != 0x33333333_33333333,
2264 "Found poison value {} (0x{:016X}) at {} in aroon_down",
2265 val,
2266 bits,
2267 i
2268 );
2269 }
2270 }
2271 }
2272
2273 Ok(())
2274 })
2275 .unwrap();
2276
2277 Ok(())
2278 }
2279
2280 macro_rules! generate_all_aroon_tests {
2281 ($($test_fn:ident),*) => {
2282 paste::paste! {
2283 $(
2284 #[test]
2285 fn [<$test_fn _scalar_f64>]() {
2286 let _ = $test_fn(stringify!([<$test_fn _scalar_f64>]), Kernel::Scalar);
2287 }
2288 )*
2289 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2290 $(
2291 #[test]
2292 fn [<$test_fn _avx2_f64>]() {
2293 let _ = $test_fn(stringify!([<$test_fn _avx2_f64>]), Kernel::Avx2);
2294 }
2295 #[test]
2296 fn [<$test_fn _avx512_f64>]() {
2297 let _ = $test_fn(stringify!([<$test_fn _avx512_f64>]), Kernel::Avx512);
2298 }
2299 )*
2300 }
2301 }
2302 }
2303
2304 generate_all_aroon_tests!(
2305 check_aroon_partial_params,
2306 check_aroon_accuracy,
2307 check_aroon_default_candles,
2308 check_aroon_zero_length,
2309 check_aroon_length_exceeds_data,
2310 check_aroon_very_small_data_set,
2311 check_aroon_reinput,
2312 check_aroon_nan_handling,
2313 check_aroon_streaming,
2314 check_aroon_no_poison,
2315 check_aroon_all_nan_error,
2316 check_aroon_leading_nan_warmup,
2317 check_aroon_nan_in_window,
2318 check_aroon_streaming_nan_window
2319 );
2320
2321 #[cfg(feature = "proptest")]
2322 generate_all_aroon_tests!(check_aroon_property);
2323
2324 fn check_aroon_all_nan_error(
2325 test_name: &str,
2326 kernel: Kernel,
2327 ) -> Result<(), Box<dyn std::error::Error>> {
2328 skip_if_unsupported!(kernel, test_name);
2329
2330 let high = vec![f64::NAN; 20];
2331 let low = vec![f64::NAN; 20];
2332 let params = AroonParams { length: Some(5) };
2333 let input = AroonInput::from_slices_hl(&high, &low, params);
2334
2335 let result = aroon_with_kernel(&input, kernel);
2336 assert!(
2337 matches!(result, Err(AroonError::AllValuesNaN)),
2338 "Expected AllValuesNaN error, got: {:?}",
2339 result
2340 );
2341
2342 Ok(())
2343 }
2344
2345 fn check_aroon_leading_nan_warmup(
2346 test_name: &str,
2347 kernel: Kernel,
2348 ) -> Result<(), Box<dyn std::error::Error>> {
2349 skip_if_unsupported!(kernel, test_name);
2350
2351 let mut high = vec![f64::NAN; 5];
2352 let mut low = vec![f64::NAN; 5];
2353 high.extend_from_slice(&[
2354 100.0, 110.0, 105.0, 115.0, 112.0, 120.0, 118.0, 125.0, 122.0, 130.0,
2355 ]);
2356 low.extend_from_slice(&[
2357 90.0, 95.0, 92.0, 98.0, 96.0, 100.0, 99.0, 105.0, 103.0, 108.0,
2358 ]);
2359
2360 let params = AroonParams { length: Some(3) };
2361 let input = AroonInput::from_slices_hl(&high, &low, params);
2362 let result = aroon_with_kernel(&input, kernel)?;
2363
2364 for i in 0..8 {
2365 assert!(
2366 result.aroon_up[i].is_nan(),
2367 "Expected NaN at index {} for aroon_up during warmup, got {}",
2368 i,
2369 result.aroon_up[i]
2370 );
2371 assert!(
2372 result.aroon_down[i].is_nan(),
2373 "Expected NaN at index {} for aroon_down during warmup, got {}",
2374 i,
2375 result.aroon_down[i]
2376 );
2377 }
2378
2379 for i in 8..high.len() {
2380 assert!(
2381 !result.aroon_up[i].is_nan(),
2382 "Unexpected NaN at index {} for aroon_up after warmup",
2383 i
2384 );
2385 assert!(
2386 !result.aroon_down[i].is_nan(),
2387 "Unexpected NaN at index {} for aroon_down after warmup",
2388 i
2389 );
2390 }
2391
2392 Ok(())
2393 }
2394
2395 fn check_aroon_nan_in_window(
2396 test_name: &str,
2397 kernel: Kernel,
2398 ) -> Result<(), Box<dyn std::error::Error>> {
2399 skip_if_unsupported!(kernel, test_name);
2400
2401 let mut high = vec![
2402 100.0,
2403 110.0,
2404 105.0,
2405 115.0,
2406 112.0,
2407 f64::NAN,
2408 118.0,
2409 125.0,
2410 122.0,
2411 130.0,
2412 ];
2413 let low = vec![
2414 90.0, 95.0, 92.0, 98.0, 96.0, 100.0, 99.0, 105.0, 103.0, 108.0,
2415 ];
2416
2417 let params = AroonParams { length: Some(3) };
2418 let input = AroonInput::from_slices_hl(&high, &low, params);
2419 let result = aroon_with_kernel(&input, kernel)?;
2420
2421 for i in 5..=8 {
2422 if i < result.aroon_up.len() {
2423 assert!(
2424 result.aroon_up[i].is_nan(),
2425 "Expected NaN at index {} for aroon_up when NaN is in window, got {}",
2426 i,
2427 result.aroon_up[i]
2428 );
2429 assert!(
2430 result.aroon_down[i].is_nan(),
2431 "Expected NaN at index {} for aroon_down when NaN is in window, got {}",
2432 i,
2433 result.aroon_down[i]
2434 );
2435 }
2436 }
2437
2438 if result.aroon_up.len() > 9 {
2439 assert!(
2440 !result.aroon_up[9].is_nan(),
2441 "Unexpected NaN at index 9 for aroon_up after NaN exits window"
2442 );
2443 assert!(
2444 !result.aroon_down[9].is_nan(),
2445 "Unexpected NaN at index 9 for aroon_down after NaN exits window"
2446 );
2447 }
2448
2449 Ok(())
2450 }
2451
2452 fn check_aroon_streaming_nan_window(
2453 test_name: &str,
2454 kernel: Kernel,
2455 ) -> Result<(), Box<dyn std::error::Error>> {
2456 skip_if_unsupported!(kernel, test_name);
2457
2458 let mut stream = AroonStream::try_new(AroonParams { length: Some(3) })?;
2459
2460 assert_eq!(stream.update(100.0, 90.0), None);
2461 assert_eq!(stream.update(110.0, 95.0), None);
2462 assert_eq!(stream.update(105.0, 92.0), None);
2463
2464 let result = stream.update(115.0, 98.0);
2465 assert!(result.is_some(), "Expected Some result after 4 values");
2466
2467 let result_with_nan = stream.update(f64::NAN, 100.0);
2468 assert_eq!(result_with_nan, None, "Expected None when NaN is in window");
2469
2470 assert_eq!(stream.update(120.0, 105.0), None);
2471 assert_eq!(stream.update(125.0, 108.0), None);
2472 assert_eq!(stream.update(130.0, 110.0), None);
2473
2474 let result_after_nan = stream.update(135.0, 112.0);
2475 assert!(
2476 result_after_nan.is_some(),
2477 "Expected Some result after NaN exits window"
2478 );
2479
2480 Ok(())
2481 }
2482
2483 fn check_batch_default_row(
2484 test: &str,
2485 kernel: Kernel,
2486 ) -> Result<(), Box<dyn std::error::Error>> {
2487 skip_if_unsupported!(kernel, test);
2488 let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2489 let c = read_candles_from_csv(file)?;
2490 let output = AroonBatchBuilder::new().kernel(kernel).apply_candles(&c)?;
2491
2492 let def = AroonParams::default();
2493 let row = output.up_for(&def).expect("default up row missing");
2494 assert_eq!(row.len(), c.close.len());
2495
2496 let expected = [21.43, 14.29, 7.14, 0.0, 0.0];
2497 let start = row.len() - 5;
2498 for (i, &v) in row[start..].iter().enumerate() {
2499 assert!(
2500 (v - expected[i]).abs() < 1e-2,
2501 "[{test}] default-row mismatch at idx {i}: {v} vs {expected:?}"
2502 );
2503 }
2504 Ok(())
2505 }
2506
2507 #[cfg(debug_assertions)]
2508 fn check_batch_no_poison(test: &str, kernel: Kernel) -> Result<(), Box<dyn std::error::Error>> {
2509 skip_if_unsupported!(kernel, test);
2510
2511 let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2512 let c = read_candles_from_csv(file)?;
2513
2514 let test_configs = vec![
2515 (1, 10, 1),
2516 (2, 20, 2),
2517 (5, 50, 5),
2518 (10, 100, 10),
2519 (14, 14, 0),
2520 (50, 200, 50),
2521 (1, 5, 1),
2522 (100, 200, 50),
2523 (3, 30, 3),
2524 ];
2525
2526 for (cfg_idx, &(l_start, l_end, l_step)) in test_configs.iter().enumerate() {
2527 let output = AroonBatchBuilder::new()
2528 .kernel(kernel)
2529 .length_range(l_start, l_end, l_step)
2530 .apply_candles(&c)?;
2531
2532 for (idx, &val) in output.up.iter().enumerate() {
2533 if val.is_nan() {
2534 continue;
2535 }
2536
2537 let bits = val.to_bits();
2538 let row = idx / output.cols;
2539 let col = idx % output.cols;
2540 let combo = &output.combos[row];
2541
2542 if bits == 0x11111111_11111111 {
2543 panic!(
2544 "[{}] Config {}: Found alloc_with_nan_prefix poison value {} (0x{:016X}) \
2545 at row {} col {} (flat index {}) in aroon_up output with params: length={}",
2546 test,
2547 cfg_idx,
2548 val,
2549 bits,
2550 row,
2551 col,
2552 idx,
2553 combo.length.unwrap_or(14)
2554 );
2555 }
2556
2557 if bits == 0x22222222_22222222 {
2558 panic!(
2559 "[{}] Config {}: Found init_matrix_prefixes poison value {} (0x{:016X}) \
2560 at row {} col {} (flat index {}) in aroon_up output with params: length={}",
2561 test,
2562 cfg_idx,
2563 val,
2564 bits,
2565 row,
2566 col,
2567 idx,
2568 combo.length.unwrap_or(14)
2569 );
2570 }
2571
2572 if bits == 0x33333333_33333333 {
2573 panic!(
2574 "[{}] Config {}: Found make_uninit_matrix poison value {} (0x{:016X}) \
2575 at row {} col {} (flat index {}) in aroon_up output with params: length={}",
2576 test,
2577 cfg_idx,
2578 val,
2579 bits,
2580 row,
2581 col,
2582 idx,
2583 combo.length.unwrap_or(14)
2584 );
2585 }
2586 }
2587
2588 for (idx, &val) in output.down.iter().enumerate() {
2589 if val.is_nan() {
2590 continue;
2591 }
2592
2593 let bits = val.to_bits();
2594 let row = idx / output.cols;
2595 let col = idx % output.cols;
2596 let combo = &output.combos[row];
2597
2598 if bits == 0x11111111_11111111 {
2599 panic!(
2600 "[{}] Config {}: Found alloc_with_nan_prefix poison value {} (0x{:016X}) \
2601 at row {} col {} (flat index {}) in aroon_down output with params: length={}",
2602 test,
2603 cfg_idx,
2604 val,
2605 bits,
2606 row,
2607 col,
2608 idx,
2609 combo.length.unwrap_or(14)
2610 );
2611 }
2612
2613 if bits == 0x22222222_22222222 {
2614 panic!(
2615 "[{}] Config {}: Found init_matrix_prefixes poison value {} (0x{:016X}) \
2616 at row {} col {} (flat index {}) in aroon_down output with params: length={}",
2617 test,
2618 cfg_idx,
2619 val,
2620 bits,
2621 row,
2622 col,
2623 idx,
2624 combo.length.unwrap_or(14)
2625 );
2626 }
2627
2628 if bits == 0x33333333_33333333 {
2629 panic!(
2630 "[{}] Config {}: Found make_uninit_matrix poison value {} (0x{:016X}) \
2631 at row {} col {} (flat index {}) in aroon_down output with params: length={}",
2632 test,
2633 cfg_idx,
2634 val,
2635 bits,
2636 row,
2637 col,
2638 idx,
2639 combo.length.unwrap_or(14)
2640 );
2641 }
2642 }
2643 }
2644
2645 Ok(())
2646 }
2647
2648 #[cfg(not(debug_assertions))]
2649 fn check_batch_no_poison(
2650 _test: &str,
2651 _kernel: Kernel,
2652 ) -> Result<(), Box<dyn std::error::Error>> {
2653 Ok(())
2654 }
2655
2656 macro_rules! gen_batch_tests {
2657 ($fn_name:ident) => {
2658 paste::paste! {
2659 #[test] fn [<$fn_name _scalar>]() {
2660 let _ = $fn_name(stringify!([<$fn_name _scalar>]), Kernel::ScalarBatch);
2661 }
2662 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2663 #[test] fn [<$fn_name _avx2>]() {
2664 let _ = $fn_name(stringify!([<$fn_name _avx2>]), Kernel::Avx2Batch);
2665 }
2666 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2667 #[test] fn [<$fn_name _avx512>]() {
2668 let _ = $fn_name(stringify!([<$fn_name _avx512>]), Kernel::Avx512Batch);
2669 }
2670 #[test] fn [<$fn_name _auto_detect>]() {
2671 let _ = $fn_name(stringify!([<$fn_name _auto_detect>]), Kernel::Auto);
2672 }
2673 }
2674 };
2675 }
2676 gen_batch_tests!(check_batch_default_row);
2677 gen_batch_tests!(check_batch_no_poison);
2678
2679 #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
2680 #[test]
2681 fn test_aroon_into_matches_api() -> Result<(), Box<dyn std::error::Error>> {
2682 let len = 256usize;
2683 let mut high = Vec::with_capacity(len);
2684 let mut low = Vec::with_capacity(len);
2685
2686 for _ in 0..8 {
2687 high.push(f64::NAN);
2688 low.push(f64::NAN);
2689 }
2690
2691 for i in 8..len {
2692 let base = 100.0 + (i as f64) * 0.017;
2693 high.push(base + 0.75 + (i as f64).sin() * 0.01);
2694 low.push(base - 0.80 + (i as f64).cos() * 0.01);
2695 }
2696
2697 let input = AroonInput::from_slices_hl(&high, &low, AroonParams::default());
2698
2699 let baseline = aroon(&input)?;
2700
2701 let mut up = vec![0.0; len];
2702 let mut down = vec![0.0; len];
2703 aroon_into(&input, &mut up, &mut down)?;
2704
2705 assert_eq!(baseline.aroon_up.len(), up.len());
2706 assert_eq!(baseline.aroon_down.len(), down.len());
2707
2708 #[inline]
2709 fn eq_or_both_nan(a: f64, b: f64) -> bool {
2710 (a.is_nan() && b.is_nan()) || (a == b)
2711 }
2712
2713 for i in 0..len {
2714 assert!(
2715 eq_or_both_nan(baseline.aroon_up[i], up[i]),
2716 "Mismatch at index {} (up): baseline={}, into={}",
2717 i,
2718 baseline.aroon_up[i],
2719 up[i]
2720 );
2721 assert!(
2722 eq_or_both_nan(baseline.aroon_down[i], down[i]),
2723 "Mismatch at index {} (down): baseline={}, into={}",
2724 i,
2725 baseline.aroon_down[i],
2726 down[i]
2727 );
2728 }
2729
2730 Ok(())
2731 }
2732}
2733
2734#[inline]
2735pub fn aroon_into_slice(
2736 dst_up: &mut [f64],
2737 dst_down: &mut [f64],
2738 input: &AroonInput,
2739 kern: Kernel,
2740) -> Result<(), AroonError> {
2741 let (high, low): (&[f64], &[f64]) = match &input.data {
2742 AroonData::Candles { candles } => {
2743 (source_type(candles, "high"), source_type(candles, "low"))
2744 }
2745 AroonData::SlicesHL { high, low } => (*high, *low),
2746 };
2747 if high.is_empty() || low.is_empty() {
2748 return Err(AroonError::EmptyInputData);
2749 }
2750 if high.len() != low.len() {
2751 return Err(AroonError::MismatchSliceLength {
2752 high_len: high.len(),
2753 low_len: low.len(),
2754 });
2755 }
2756 let len = high.len();
2757 let length = input.get_length();
2758 if length == 0 || length > len {
2759 return Err(AroonError::InvalidLength {
2760 length,
2761 data_len: len,
2762 });
2763 }
2764 if dst_up.len() != len || dst_down.len() != len {
2765 return Err(AroonError::OutputLengthMismatch {
2766 expected: len,
2767 got: dst_up.len().max(dst_down.len()),
2768 });
2769 }
2770
2771 let first = first_valid_pair(high, low).ok_or(AroonError::AllValuesNaN)?;
2772 let warm = first + length;
2773
2774 let chosen = match kern {
2775 Kernel::Auto => Kernel::Scalar,
2776 k => k,
2777 };
2778 unsafe {
2779 match chosen {
2780 Kernel::Scalar | Kernel::ScalarBatch => {
2781 aroon_scalar(high, low, length, dst_up, dst_down)
2782 }
2783 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2784 Kernel::Avx2 | Kernel::Avx2Batch => aroon_avx2(high, low, length, dst_up, dst_down),
2785 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2786 Kernel::Avx512 | Kernel::Avx512Batch => {
2787 aroon_avx512(high, low, length, dst_up, dst_down)
2788 }
2789 _ => unreachable!(),
2790 }
2791 }
2792 for v in &mut dst_up[..warm.min(len)] {
2793 *v = f64::NAN;
2794 }
2795 for v in &mut dst_down[..warm.min(len)] {
2796 *v = f64::NAN;
2797 }
2798 Ok(())
2799}
2800
2801#[cfg(feature = "python")]
2802#[pyfunction(name = "aroon")]
2803#[pyo3(signature = (high, low, length, kernel=None))]
2804pub fn aroon_py<'py>(
2805 py: Python<'py>,
2806 high: numpy::PyReadonlyArray1<'py, f64>,
2807 low: numpy::PyReadonlyArray1<'py, f64>,
2808 length: usize,
2809 kernel: Option<&str>,
2810) -> PyResult<(Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<f64>>)> {
2811 use numpy::{IntoPyArray, PyArray1, PyArrayMethods};
2812
2813 let h = high.as_slice()?;
2814 let l = low.as_slice()?;
2815 if h.len() != l.len() {
2816 return Err(PyValueError::new_err(format!(
2817 "High/low length mismatch: {} vs {}",
2818 h.len(),
2819 l.len()
2820 )));
2821 }
2822
2823 let kern = validate_kernel(kernel, false)?;
2824 let params = AroonParams {
2825 length: Some(length),
2826 };
2827 let input = AroonInput::from_slices_hl(h, l, params);
2828
2829 let out = py
2830 .allow_threads(|| aroon_with_kernel(&input, kern))
2831 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2832
2833 Ok((
2834 out.aroon_up.into_pyarray(py),
2835 out.aroon_down.into_pyarray(py),
2836 ))
2837}
2838
2839#[cfg(feature = "python")]
2840#[pyclass(name = "AroonStream")]
2841pub struct AroonStreamPy {
2842 stream: AroonStream,
2843}
2844
2845#[cfg(feature = "python")]
2846#[pymethods]
2847impl AroonStreamPy {
2848 #[new]
2849 fn new(length: usize) -> PyResult<Self> {
2850 let params = AroonParams {
2851 length: Some(length),
2852 };
2853 let stream =
2854 AroonStream::try_new(params).map_err(|e| PyValueError::new_err(e.to_string()))?;
2855 Ok(AroonStreamPy { stream })
2856 }
2857
2858 fn update(&mut self, high: f64, low: f64) -> Option<(f64, f64)> {
2859 self.stream.update(high, low)
2860 }
2861}
2862
2863#[cfg(feature = "python")]
2864#[pyfunction(name = "aroon_batch")]
2865#[pyo3(signature = (high, low, length_range, kernel=None))]
2866pub fn aroon_batch_py<'py>(
2867 py: Python<'py>,
2868 high: numpy::PyReadonlyArray1<'py, f64>,
2869 low: numpy::PyReadonlyArray1<'py, f64>,
2870 length_range: (usize, usize, usize),
2871 kernel: Option<&str>,
2872) -> PyResult<Bound<'py, pyo3::types::PyDict>> {
2873 use numpy::{IntoPyArray, PyArray1, PyArrayMethods};
2874 use pyo3::types::PyDict;
2875
2876 let h = high.as_slice()?;
2877 let l = low.as_slice()?;
2878 if h.len() != l.len() {
2879 return Err(PyValueError::new_err(format!(
2880 "High/low length mismatch: {} vs {}",
2881 h.len(),
2882 l.len()
2883 )));
2884 }
2885
2886 let sweep = AroonBatchRange {
2887 length: length_range,
2888 };
2889 let combos = expand_grid_aroon(&sweep).map_err(|e| PyValueError::new_err(e.to_string()))?;
2890 let rows = combos.len();
2891 let cols = h.len();
2892
2893 let total = rows
2894 .checked_mul(cols)
2895 .ok_or_else(|| PyValueError::new_err("rows * cols overflow"))?;
2896 let up_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
2897 let down_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
2898 let up_slice = unsafe { up_arr.as_slice_mut()? };
2899 let down_slice = unsafe { down_arr.as_slice_mut()? };
2900
2901 let kern = validate_kernel(kernel, true)?;
2902 py.allow_threads(|| {
2903 let batch = match kern {
2904 Kernel::Auto => detect_best_batch_kernel(),
2905 k => k,
2906 };
2907 let simd = match batch {
2908 Kernel::Avx512Batch => Kernel::Avx512,
2909 Kernel::Avx2Batch => Kernel::Avx2,
2910 Kernel::ScalarBatch => Kernel::Scalar,
2911 _ => unreachable!(),
2912 };
2913 aroon_batch_inner_into(h, l, &sweep, simd, true, up_slice, down_slice)
2914 })
2915 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2916
2917 let dict = PyDict::new(py);
2918 dict.set_item("up", up_arr.reshape((rows, cols))?)?;
2919 dict.set_item("down", down_arr.reshape((rows, cols))?)?;
2920 dict.set_item(
2921 "lengths",
2922 combos
2923 .iter()
2924 .map(|p| p.length.unwrap() as u64)
2925 .collect::<Vec<_>>()
2926 .into_pyarray(py),
2927 )?;
2928 dict.set_item("rows", rows)?;
2929 dict.set_item("cols", cols)?;
2930 Ok(dict)
2931}
2932
2933#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2934#[derive(Serialize, Deserialize)]
2935pub struct AroonJsOutput {
2936 pub values: Vec<f64>,
2937 pub rows: usize,
2938 pub cols: usize,
2939}
2940
2941#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2942#[wasm_bindgen(js_name = "aroon_js")]
2943pub fn aroon_js(high: &[f64], low: &[f64], length: usize) -> Result<JsValue, JsValue> {
2944 let params = AroonParams {
2945 length: Some(length),
2946 };
2947 let input = AroonInput::from_slices_hl(high, low, params);
2948
2949 let mut up = vec![0.0; high.len()];
2950 let mut down = vec![0.0; high.len()];
2951
2952 aroon_into_slice(&mut up, &mut down, &input, Kernel::Auto)
2953 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2954
2955 let obj = js_sys::Object::new();
2956 js_sys::Reflect::set(
2957 &obj,
2958 &JsValue::from_str("up"),
2959 &serde_wasm_bindgen::to_value(&up).unwrap(),
2960 )?;
2961 js_sys::Reflect::set(
2962 &obj,
2963 &JsValue::from_str("down"),
2964 &serde_wasm_bindgen::to_value(&down).unwrap(),
2965 )?;
2966
2967 Ok(obj.into())
2968}
2969
2970#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2971#[derive(Serialize, Deserialize)]
2972pub struct AroonBatchJsOutput {
2973 pub values: Vec<f64>,
2974 pub rows: usize,
2975 pub cols: usize,
2976 pub combos: Vec<AroonParams>,
2977}
2978
2979#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2980#[wasm_bindgen(js_name = "aroon_batch_js")]
2981pub fn aroon_batch_unified_js(
2982 high: &[f64],
2983 low: &[f64],
2984 length_start: usize,
2985 length_end: usize,
2986 length_step: usize,
2987) -> Result<JsValue, JsValue> {
2988 let sweep = AroonBatchRange {
2989 length: (length_start, length_end, length_step),
2990 };
2991 let combos = expand_grid(&sweep);
2992 let rows = combos.len();
2993 let cols = high.len();
2994 let total = rows
2995 .checked_mul(cols)
2996 .ok_or_else(|| JsValue::from_str("rows * cols overflow"))?;
2997
2998 let mut up = vec![0.0; total];
2999 let mut down = vec![0.0; total];
3000
3001 aroon_batch_inner_into(
3002 high,
3003 low,
3004 &sweep,
3005 detect_best_kernel(),
3006 false,
3007 &mut up,
3008 &mut down,
3009 )
3010 .map_err(|e| JsValue::from_str(&e.to_string()))?;
3011
3012 let obj = js_sys::Object::new();
3013 js_sys::Reflect::set(
3014 &obj,
3015 &JsValue::from_str("up"),
3016 &serde_wasm_bindgen::to_value(&up).unwrap(),
3017 )?;
3018 js_sys::Reflect::set(
3019 &obj,
3020 &JsValue::from_str("down"),
3021 &serde_wasm_bindgen::to_value(&down).unwrap(),
3022 )?;
3023 js_sys::Reflect::set(
3024 &obj,
3025 &JsValue::from_str("rows"),
3026 &JsValue::from_f64(rows as f64),
3027 )?;
3028 js_sys::Reflect::set(
3029 &obj,
3030 &JsValue::from_str("cols"),
3031 &JsValue::from_f64(cols as f64),
3032 )?;
3033 js_sys::Reflect::set(
3034 &obj,
3035 &JsValue::from_str("combos"),
3036 &serde_wasm_bindgen::to_value(&combos).unwrap(),
3037 )?;
3038
3039 Ok(obj.into())
3040}
3041
3042#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3043#[wasm_bindgen(js_name = "aroon_batch_metadata_js")]
3044pub fn aroon_batch_metadata_js(
3045 length_start: usize,
3046 length_end: usize,
3047 length_step: usize,
3048) -> Result<Vec<f64>, JsValue> {
3049 let sweep = AroonBatchRange {
3050 length: (length_start, length_end, length_step),
3051 };
3052
3053 let combos = expand_grid(&sweep);
3054 let metadata: Vec<f64> = combos
3055 .iter()
3056 .map(|c| c.length.unwrap_or(14) as f64)
3057 .collect();
3058
3059 Ok(metadata)
3060}
3061
3062#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3063#[derive(Serialize, Deserialize)]
3064pub struct AroonBatchConfig {
3065 pub length_range: Vec<usize>,
3066}
3067
3068#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3069#[wasm_bindgen(js_name = "aroon_batch")]
3070pub fn aroon_batch_config_js(
3071 high: &[f64],
3072 low: &[f64],
3073 config: JsValue,
3074) -> Result<JsValue, JsValue> {
3075 let config: AroonBatchConfig = serde_wasm_bindgen::from_value(config)
3076 .map_err(|e| JsValue::from_str(&format!("Invalid config: {}", e)))?;
3077
3078 if config.length_range.len() != 3 {
3079 return Err(JsValue::from_str(
3080 "Invalid config: length_range must have exactly 3 elements [start, end, step]",
3081 ));
3082 }
3083
3084 let sweep = AroonBatchRange {
3085 length: (
3086 config.length_range[0],
3087 config.length_range[1],
3088 config.length_range[2],
3089 ),
3090 };
3091
3092 let combos = expand_grid(&sweep);
3093 let rows = combos.len();
3094 let cols = high.len();
3095 let total = rows
3096 .checked_mul(cols)
3097 .ok_or_else(|| JsValue::from_str("rows * cols overflow"))?;
3098
3099 let mut up = vec![0.0; total];
3100 let mut down = vec![0.0; total];
3101
3102 aroon_batch_inner_into(
3103 high,
3104 low,
3105 &sweep,
3106 detect_best_kernel(),
3107 false,
3108 &mut up,
3109 &mut down,
3110 )
3111 .map_err(|e| JsValue::from_str(&e.to_string()))?;
3112
3113 let obj = js_sys::Object::new();
3114 js_sys::Reflect::set(
3115 &obj,
3116 &JsValue::from_str("up"),
3117 &serde_wasm_bindgen::to_value(&up).unwrap(),
3118 )?;
3119 js_sys::Reflect::set(
3120 &obj,
3121 &JsValue::from_str("down"),
3122 &serde_wasm_bindgen::to_value(&down).unwrap(),
3123 )?;
3124 js_sys::Reflect::set(
3125 &obj,
3126 &JsValue::from_str("rows"),
3127 &JsValue::from_f64(rows as f64),
3128 )?;
3129 js_sys::Reflect::set(
3130 &obj,
3131 &JsValue::from_str("cols"),
3132 &JsValue::from_f64(cols as f64),
3133 )?;
3134 js_sys::Reflect::set(
3135 &obj,
3136 &JsValue::from_str("combos"),
3137 &serde_wasm_bindgen::to_value(&combos).unwrap(),
3138 )?;
3139
3140 Ok(obj.into())
3141}
3142
3143#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3144#[wasm_bindgen]
3145pub fn aroon_alloc(len: usize) -> *mut f64 {
3146 let mut v = Vec::<f64>::with_capacity(2 * len);
3147 let p = v.as_mut_ptr();
3148 std::mem::forget(v);
3149 p
3150}
3151
3152#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3153#[wasm_bindgen]
3154pub fn aroon_free(ptr: *mut f64, len: usize) {
3155 unsafe {
3156 let _ = Vec::from_raw_parts(ptr, 2 * len, 2 * len);
3157 }
3158}
3159
3160#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3161#[wasm_bindgen]
3162pub fn aroon_into(
3163 high_ptr: *const f64,
3164 low_ptr: *const f64,
3165 out_ptr: *mut f64,
3166 len: usize,
3167 length: usize,
3168) -> Result<(), JsValue> {
3169 if high_ptr.is_null() || low_ptr.is_null() || out_ptr.is_null() {
3170 return Err(JsValue::from_str("null pointer passed to aroon_into"));
3171 }
3172 unsafe {
3173 let high = std::slice::from_raw_parts(high_ptr, len);
3174 let low = std::slice::from_raw_parts(low_ptr, len);
3175 let out = std::slice::from_raw_parts_mut(out_ptr, 2 * len);
3176
3177 let params = AroonParams {
3178 length: Some(length),
3179 };
3180 let input = AroonInput::from_slices_hl(high, low, params);
3181
3182 let (up, down) = out.split_at_mut(len);
3183 aroon_into_slice(up, down, &input, Kernel::Auto)
3184 .map_err(|e| JsValue::from_str(&e.to_string()))
3185 }
3186}