Skip to main content

vector_ta/indicators/
aroon.rs

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}