Skip to main content

vector_ta/indicators/
aroonosc.rs

1use crate::utilities::data_loader::Candles;
2use crate::utilities::enums::Kernel;
3use crate::utilities::helpers::{
4    alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
5    make_uninit_matrix,
6};
7#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
8use core::arch::x86_64::*;
9use paste::paste;
10#[cfg(not(target_arch = "wasm32"))]
11use rayon::prelude::*;
12use std::mem::ManuallyDrop;
13use thiserror::Error;
14
15#[inline(always)]
16fn first_valid_hilo(high: &[f64], low: &[f64]) -> Option<usize> {
17    high.iter()
18        .zip(low)
19        .position(|(h, l)| h.is_finite() && l.is_finite())
20}
21
22#[derive(Debug, Clone)]
23pub enum AroonOscData<'a> {
24    Candles { candles: &'a Candles },
25    SlicesHL { high: &'a [f64], low: &'a [f64] },
26}
27
28#[derive(Debug, Clone)]
29#[cfg_attr(
30    all(target_arch = "wasm32", feature = "wasm"),
31    derive(serde::Serialize, serde::Deserialize)
32)]
33pub struct AroonOscParams {
34    pub length: Option<usize>,
35}
36
37impl Default for AroonOscParams {
38    fn default() -> Self {
39        Self { length: Some(14) }
40    }
41}
42
43#[derive(Debug, Clone)]
44pub struct AroonOscInput<'a> {
45    pub data: AroonOscData<'a>,
46    pub params: AroonOscParams,
47}
48
49impl<'a> AroonOscInput<'a> {
50    #[inline]
51    pub fn from_candles(candles: &'a Candles, params: AroonOscParams) -> Self {
52        Self {
53            data: AroonOscData::Candles { candles },
54            params,
55        }
56    }
57    #[inline]
58    pub fn from_slices_hl(high: &'a [f64], low: &'a [f64], params: AroonOscParams) -> Self {
59        Self {
60            data: AroonOscData::SlicesHL { high, low },
61            params,
62        }
63    }
64    #[inline]
65    pub fn with_default_candles(candles: &'a Candles) -> Self {
66        Self {
67            data: AroonOscData::Candles { candles },
68            params: AroonOscParams::default(),
69        }
70    }
71    #[inline]
72    pub fn get_length(&self) -> usize {
73        self.params.length.unwrap_or(14)
74    }
75
76    #[inline]
77    pub fn data_len(&self) -> usize {
78        match &self.data {
79            AroonOscData::Candles { candles } => candles.close.len(),
80            AroonOscData::SlicesHL { high, .. } => high.len(),
81        }
82    }
83
84    #[inline]
85    pub fn get_high(&self) -> &'a [f64] {
86        match &self.data {
87            AroonOscData::Candles { candles } => &candles.high,
88            AroonOscData::SlicesHL { high, .. } => high,
89        }
90    }
91
92    #[inline]
93    pub fn get_low(&self) -> &'a [f64] {
94        match &self.data {
95            AroonOscData::Candles { candles } => &candles.low,
96            AroonOscData::SlicesHL { low, .. } => low,
97        }
98    }
99}
100
101#[derive(Debug, Clone)]
102pub struct AroonOscOutput {
103    pub values: Vec<f64>,
104}
105
106#[derive(Copy, Clone, Debug)]
107pub struct AroonOscBuilder {
108    length: Option<usize>,
109    kernel: Kernel,
110}
111
112impl Default for AroonOscBuilder {
113    fn default() -> Self {
114        Self {
115            length: None,
116            kernel: Kernel::Auto,
117        }
118    }
119}
120
121impl AroonOscBuilder {
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<AroonOscOutput, AroonOscError> {
138        let p = AroonOscParams {
139            length: self.length,
140        };
141        let i = AroonOscInput::from_candles(c, p);
142        aroon_osc_with_kernel(&i, self.kernel)
143    }
144    #[inline(always)]
145    pub fn apply_slice(self, high: &[f64], low: &[f64]) -> Result<AroonOscOutput, AroonOscError> {
146        let p = AroonOscParams {
147            length: self.length,
148        };
149        let i = AroonOscInput::from_slices_hl(high, low, p);
150        aroon_osc_with_kernel(&i, self.kernel)
151    }
152    #[inline(always)]
153    pub fn into_stream(self) -> Result<AroonOscStream, AroonOscError> {
154        let p = AroonOscParams {
155            length: self.length,
156        };
157        AroonOscStream::try_new(p)
158    }
159}
160
161#[derive(Debug, Error)]
162pub enum AroonOscError {
163    #[error("aroonosc: Input data slice is empty.")]
164    EmptyInputData,
165    #[error("aroonosc: All values are NaN.")]
166    AllValuesNaN,
167    #[error("aroonosc: Invalid length: length = {period}, data length = {data_len}")]
168    InvalidPeriod { period: usize, data_len: usize },
169    #[error("aroonosc: Not enough data: needed = {needed}, valid = {valid}")]
170    NotEnoughValidData { needed: usize, valid: usize },
171    #[error("aroonosc: Mismatch in high/low slice length: high_len={high_len}, low_len={low_len}")]
172    MismatchSliceLength { high_len: usize, low_len: usize },
173    #[error("aroonosc: Output length mismatch: expected={expected}, got={got}")]
174    OutputLengthMismatch { expected: usize, got: usize },
175
176    #[error("aroonosc: Invalid range: start={start}, end={end}, step={step}")]
177    InvalidRange {
178        start: usize,
179        end: usize,
180        step: usize,
181    },
182    #[error("aroonosc: Invalid kernel for batch: {0:?}")]
183    InvalidKernelForBatch(crate::utilities::enums::Kernel),
184}
185
186#[inline]
187pub fn aroon_osc(input: &AroonOscInput) -> Result<AroonOscOutput, AroonOscError> {
188    aroon_osc_with_kernel(input, Kernel::Auto)
189}
190
191#[inline(always)]
192fn aroon_osc_prepare<'a>(
193    input: &'a AroonOscInput,
194    kernel: Kernel,
195) -> Result<(&'a [f64], &'a [f64], usize, usize, Kernel), AroonOscError> {
196    let length = input.get_length();
197    let high = input.get_high();
198    let low = input.get_low();
199    let len = low.len();
200    if length == 0 {
201        return Err(AroonOscError::InvalidPeriod {
202            period: length,
203            data_len: len,
204        });
205    }
206
207    if high.is_empty() || low.is_empty() {
208        return Err(AroonOscError::EmptyInputData);
209    }
210    if high.len() != low.len() {
211        return Err(AroonOscError::MismatchSliceLength {
212            high_len: high.len(),
213            low_len: low.len(),
214        });
215    }
216
217    let first = first_valid_hilo(high, low).ok_or(AroonOscError::AllValuesNaN)?;
218
219    let window = length.checked_add(1).ok_or(AroonOscError::InvalidPeriod {
220        period: length,
221        data_len: len,
222    })?;
223    let available = len.checked_sub(first).ok_or(AroonOscError::InvalidPeriod {
224        period: length,
225        data_len: len,
226    })?;
227    if available < window {
228        return Err(AroonOscError::NotEnoughValidData {
229            needed: window,
230            valid: available,
231        });
232    }
233
234    let chosen = match kernel {
235        Kernel::Auto => detect_best_kernel(),
236        k => k,
237    };
238    Ok((high, low, length, first, chosen))
239}
240
241pub fn aroon_osc_with_kernel(
242    input: &AroonOscInput,
243    kernel: Kernel,
244) -> Result<AroonOscOutput, AroonOscError> {
245    let (high, low, length, first, chosen) = aroon_osc_prepare(input, kernel)?;
246    let warm_end = first
247        .checked_add(length)
248        .ok_or(AroonOscError::InvalidPeriod {
249            period: length,
250            data_len: high.len(),
251        })?;
252    let mut out = alloc_with_nan_prefix(high.len(), warm_end);
253
254    match chosen {
255        Kernel::Scalar | Kernel::ScalarBatch => {
256            aroon_osc_scalar_highlow_into(high, low, length, first, &mut out)
257        }
258        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
259        Kernel::Avx2 | Kernel::Avx2Batch | Kernel::Avx512 | Kernel::Avx512Batch => {
260            aroon_osc_scalar_highlow_into(high, low, length, first, &mut out)
261        }
262        _ => unreachable!(),
263    }
264    Ok(AroonOscOutput { values: out })
265}
266
267#[inline]
268pub fn aroon_osc_into(input: &AroonOscInput, out: &mut [f64]) -> Result<(), AroonOscError> {
269    let (high, low, length, first, chosen) = aroon_osc_prepare(input, Kernel::Auto)?;
270
271    if out.len() != high.len() {
272        return Err(AroonOscError::OutputLengthMismatch {
273            expected: high.len(),
274            got: out.len(),
275        });
276    }
277
278    let warm_end = first
279        .checked_add(length)
280        .ok_or(AroonOscError::InvalidPeriod {
281            period: length,
282            data_len: high.len(),
283        })?;
284    let warm = warm_end.min(out.len());
285    let qnan = f64::from_bits(0x7ff8_0000_0000_0000);
286    for v in &mut out[..warm] {
287        *v = qnan;
288    }
289
290    match chosen {
291        Kernel::Scalar | Kernel::ScalarBatch => {
292            aroon_osc_scalar_highlow_into(high, low, length, first, out)
293        }
294        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
295        Kernel::Avx2 | Kernel::Avx2Batch | Kernel::Avx512 | Kernel::Avx512Batch => {
296            aroon_osc_scalar_highlow_into(high, low, length, first, out)
297        }
298        _ => unreachable!(),
299    }
300
301    Ok(())
302}
303
304#[inline]
305pub fn aroon_osc_scalar_highlow_into(
306    high: &[f64],
307    low: &[f64],
308    length: usize,
309    first: usize,
310    out: &mut [f64],
311) {
312    let len = low.len();
313    let window = length + 1;
314    let start_i = first + length;
315    if start_i >= len {
316        return;
317    }
318
319    if length <= 64 {
320        let scale = 100.0 / length as f64;
321        unsafe {
322            let h_ptr = high.as_ptr();
323            let l_ptr = low.as_ptr();
324            let out_ptr = out.as_mut_ptr();
325
326            let mut maxi = first;
327            let mut mini = first;
328            let mut max = *h_ptr.add(first);
329            let mut min = *l_ptr.add(first);
330            let mut j = first + 1;
331            while j <= start_i {
332                let hv = *h_ptr.add(j);
333                if hv > max {
334                    max = hv;
335                    maxi = j;
336                }
337                let lv = *l_ptr.add(j);
338                if lv < min {
339                    min = lv;
340                    mini = j;
341                }
342                j += 1;
343            }
344
345            let mut i = start_i;
346            while i < len {
347                let start = i - length;
348
349                let bar_h = *h_ptr.add(i);
350                if maxi < start {
351                    maxi = start;
352                    max = *h_ptr.add(maxi);
353                    let mut k = start + 1;
354                    while k <= i {
355                        let hv = *h_ptr.add(k);
356                        if hv > max {
357                            max = hv;
358                            maxi = k;
359                        }
360                        k += 1;
361                    }
362                } else if bar_h > max {
363                    maxi = i;
364                    max = bar_h;
365                }
366
367                let bar_l = *l_ptr.add(i);
368                if mini < start {
369                    mini = start;
370                    min = *l_ptr.add(mini);
371                    let mut k = start + 1;
372                    while k <= i {
373                        let lv = *l_ptr.add(k);
374                        if lv < min {
375                            min = lv;
376                            mini = k;
377                        }
378                        k += 1;
379                    }
380                } else if bar_l < min {
381                    mini = i;
382                    min = bar_l;
383                }
384
385                let v = (maxi as f64 - mini as f64) * scale;
386                *out_ptr.add(i) = v.max(-100.0).min(100.0);
387                i += 1;
388            }
389        }
390        return;
391    }
392
393    let cap = window;
394
395    let mut dq_hi = vec![0usize; cap];
396    let mut hi_head = 0usize;
397    let mut hi_tail = 0usize;
398    let mut hi_len = 0usize;
399
400    let mut dq_lo = vec![0usize; cap];
401    let mut lo_head = 0usize;
402    let mut lo_tail = 0usize;
403    let mut lo_len = 0usize;
404
405    #[inline(always)]
406    fn dec_wrap(x: usize, cap: usize) -> usize {
407        if x == 0 {
408            cap - 1
409        } else {
410            x - 1
411        }
412    }
413    #[inline(always)]
414    fn inc_wrap(x: &mut usize, cap: usize) {
415        *x += 1;
416        if *x == cap {
417            *x = 0;
418        }
419    }
420
421    for i in first..start_i {
422        let v_hi = high[i];
423        while hi_len > 0 {
424            let last = dec_wrap(hi_tail, cap);
425            let last_idx = dq_hi[last];
426            let last_val = high[last_idx];
427            if last_val < v_hi {
428                hi_tail = last;
429                hi_len -= 1;
430            } else {
431                break;
432            }
433        }
434        dq_hi[hi_tail] = i;
435        inc_wrap(&mut hi_tail, cap);
436        hi_len += 1;
437
438        let v_lo = low[i];
439        while lo_len > 0 {
440            let last = dec_wrap(lo_tail, cap);
441            let last_idx = dq_lo[last];
442            let last_val = low[last_idx];
443            if last_val > v_lo {
444                lo_tail = last;
445                lo_len -= 1;
446            } else {
447                break;
448            }
449        }
450        dq_lo[lo_tail] = i;
451        inc_wrap(&mut lo_tail, cap);
452        lo_len += 1;
453    }
454
455    let scale = 100.0 / length as f64;
456    for i in start_i..len {
457        let start = i - length;
458
459        while hi_len > 0 && dq_hi[hi_head] < start {
460            inc_wrap(&mut hi_head, cap);
461            hi_len -= 1;
462        }
463        while lo_len > 0 && dq_lo[lo_head] < start {
464            inc_wrap(&mut lo_head, cap);
465            lo_len -= 1;
466        }
467
468        let v_hi = high[i];
469        while hi_len > 0 {
470            let last = dec_wrap(hi_tail, cap);
471            let last_idx = dq_hi[last];
472            let last_val = high[last_idx];
473            if last_val < v_hi {
474                hi_tail = last;
475                hi_len -= 1;
476            } else {
477                break;
478            }
479        }
480        dq_hi[hi_tail] = i;
481        inc_wrap(&mut hi_tail, cap);
482        hi_len += 1;
483
484        let v_lo = low[i];
485        while lo_len > 0 {
486            let last = dec_wrap(lo_tail, cap);
487            let last_idx = dq_lo[last];
488            let last_val = low[last_idx];
489            if last_val > v_lo {
490                lo_tail = last;
491                lo_len -= 1;
492            } else {
493                break;
494            }
495        }
496        dq_lo[lo_tail] = i;
497        inc_wrap(&mut lo_tail, cap);
498        lo_len += 1;
499
500        let hi_idx = dq_hi[hi_head];
501        let lo_idx = dq_lo[lo_head];
502        let v = (hi_idx as f64 - lo_idx as f64) * scale;
503        out[i] = v.max(-100.0).min(100.0);
504    }
505}
506
507#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
508#[inline]
509pub fn aroon_osc_avx512(high: &[f64], low: &[f64], length: usize, first: usize, out: &mut [f64]) {
510    aroon_osc_scalar_highlow_into(high, low, length, first, out)
511}
512#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
513#[inline]
514pub fn aroon_osc_avx2(high: &[f64], low: &[f64], length: usize, first: usize, out: &mut [f64]) {
515    aroon_osc_scalar_highlow_into(high, low, length, first, out)
516}
517#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
518#[inline]
519pub unsafe fn aroon_osc_avx512_short(
520    high: &[f64],
521    low: &[f64],
522    length: usize,
523    first: usize,
524    out: &mut [f64],
525) {
526    aroon_osc_scalar_highlow_into(high, low, length, first, out)
527}
528#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
529#[inline]
530pub unsafe fn aroon_osc_avx512_long(
531    high: &[f64],
532    low: &[f64],
533    length: usize,
534    first: usize,
535    out: &mut [f64],
536) {
537    aroon_osc_scalar_highlow_into(high, low, length, first, out)
538}
539
540#[inline]
541pub fn aroon_osc_into_slice(
542    dst: &mut [f64],
543    input: &AroonOscInput,
544    kern: Kernel,
545) -> Result<(), AroonOscError> {
546    let (high, low, length, first, chosen) = aroon_osc_prepare(input, kern)?;
547    if dst.len() != high.len() {
548        return Err(AroonOscError::OutputLengthMismatch {
549            expected: high.len(),
550            got: dst.len(),
551        });
552    }
553
554    match chosen {
555        Kernel::Scalar | Kernel::ScalarBatch => {
556            aroon_osc_scalar_highlow_into(high, low, length, first, dst)
557        }
558        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
559        Kernel::Avx2 | Kernel::Avx2Batch | Kernel::Avx512 | Kernel::Avx512Batch => {
560            aroon_osc_scalar_highlow_into(high, low, length, first, dst)
561        }
562        _ => unreachable!(),
563    }
564
565    let warm_end = first
566        .checked_add(length)
567        .ok_or(AroonOscError::InvalidPeriod {
568            period: length,
569            data_len: high.len(),
570        })?;
571    let warm = warm_end.min(dst.len());
572    for v in &mut dst[..warm] {
573        *v = f64::NAN;
574    }
575    Ok(())
576}
577
578#[inline(always)]
579pub fn aroon_osc_batch_with_kernel(
580    high: &[f64],
581    low: &[f64],
582    sweep: &AroonOscBatchRange,
583    k: Kernel,
584) -> Result<AroonOscBatchOutput, AroonOscError> {
585    let kernel = match k {
586        Kernel::Auto => detect_best_batch_kernel(),
587        other if other.is_batch() => other,
588        _ => {
589            return Err(AroonOscError::InvalidKernelForBatch(k));
590        }
591    };
592    let simd = match kernel {
593        Kernel::Avx512Batch => Kernel::Avx512,
594        Kernel::Avx2Batch => Kernel::Avx2,
595        Kernel::ScalarBatch => Kernel::Scalar,
596        _ => unreachable!(),
597    };
598    aroon_osc_batch_par_slice(high, low, sweep, simd)
599}
600
601#[derive(Clone, Debug)]
602pub struct AroonOscBatchRange {
603    pub length: (usize, usize, usize),
604}
605
606impl Default for AroonOscBatchRange {
607    fn default() -> Self {
608        Self {
609            length: (14, 263, 1),
610        }
611    }
612}
613
614#[derive(Clone, Debug)]
615pub struct AroonOscBatchOutput {
616    pub values: Vec<f64>,
617    pub combos: Vec<AroonOscParams>,
618    pub rows: usize,
619    pub cols: usize,
620}
621impl AroonOscBatchOutput {
622    pub fn row_for_params(&self, p: &AroonOscParams) -> Option<usize> {
623        self.combos
624            .iter()
625            .position(|c| c.length.unwrap_or(14) == p.length.unwrap_or(14))
626    }
627    pub fn values_for(&self, p: &AroonOscParams) -> Option<&[f64]> {
628        self.row_for_params(p).map(|row| {
629            let start = row * self.cols;
630            &self.values[start..start + self.cols]
631        })
632    }
633}
634
635#[derive(Clone, Debug, Default)]
636pub struct AroonOscBatchBuilder {
637    range: AroonOscBatchRange,
638    kernel: Kernel,
639}
640impl AroonOscBatchBuilder {
641    pub fn new() -> Self {
642        Self::default()
643    }
644    pub fn kernel(mut self, k: Kernel) -> Self {
645        self.kernel = k;
646        self
647    }
648    #[inline]
649    pub fn length_range(mut self, start: usize, end: usize, step: usize) -> Self {
650        self.range.length = (start, end, step);
651        self
652    }
653    #[inline]
654    pub fn length_static(mut self, l: usize) -> Self {
655        self.range.length = (l, l, 0);
656        self
657    }
658    pub fn apply_slices(
659        self,
660        high: &[f64],
661        low: &[f64],
662    ) -> Result<AroonOscBatchOutput, AroonOscError> {
663        aroon_osc_batch_with_kernel(high, low, &self.range, self.kernel)
664    }
665    pub fn with_default_slices(
666        high: &[f64],
667        low: &[f64],
668        k: Kernel,
669    ) -> Result<AroonOscBatchOutput, AroonOscError> {
670        AroonOscBatchBuilder::new()
671            .kernel(k)
672            .apply_slices(high, low)
673    }
674    pub fn apply_candles(self, c: &Candles) -> Result<AroonOscBatchOutput, AroonOscError> {
675        let high = c
676            .select_candle_field("high")
677            .map_err(|_| AroonOscError::EmptyInputData)?;
678        let low = c
679            .select_candle_field("low")
680            .map_err(|_| AroonOscError::EmptyInputData)?;
681        self.apply_slices(high, low)
682    }
683    pub fn with_default_candles(c: &Candles) -> Result<AroonOscBatchOutput, AroonOscError> {
684        AroonOscBatchBuilder::new()
685            .kernel(Kernel::Auto)
686            .apply_candles(c)
687    }
688}
689
690#[inline(always)]
691fn expand_grid(r: &AroonOscBatchRange) -> Result<Vec<AroonOscParams>, AroonOscError> {
692    fn axis_usize((start, end, step): (usize, usize, usize)) -> Result<Vec<usize>, AroonOscError> {
693        if step == 0 || start == end {
694            return Ok(vec![start]);
695        }
696        if start < end {
697            let v: Vec<usize> = (start..=end).step_by(step).collect();
698            if v.is_empty() {
699                return Err(AroonOscError::InvalidRange { start, end, step });
700            }
701            Ok(v)
702        } else {
703            let mut v = Vec::new();
704            let mut cur = start;
705            while cur >= end {
706                v.push(cur);
707                let next = cur.saturating_sub(step);
708                if next == cur {
709                    break;
710                }
711                cur = next;
712            }
713            if v.is_empty() {
714                return Err(AroonOscError::InvalidRange { start, end, step });
715            }
716            Ok(v)
717        }
718    }
719    let lengths = axis_usize(r.length)?;
720    Ok(lengths
721        .into_iter()
722        .map(|l| AroonOscParams { length: Some(l) })
723        .collect())
724}
725
726#[inline(always)]
727pub fn aroon_osc_batch_slice(
728    high: &[f64],
729    low: &[f64],
730    sweep: &AroonOscBatchRange,
731    kern: Kernel,
732) -> Result<AroonOscBatchOutput, AroonOscError> {
733    aroon_osc_batch_inner(high, low, sweep, kern, false)
734}
735#[inline(always)]
736pub fn aroon_osc_batch_par_slice(
737    high: &[f64],
738    low: &[f64],
739    sweep: &AroonOscBatchRange,
740    kern: Kernel,
741) -> Result<AroonOscBatchOutput, AroonOscError> {
742    aroon_osc_batch_inner(high, low, sweep, kern, true)
743}
744
745#[inline(always)]
746fn aroon_osc_batch_inner(
747    high: &[f64],
748    low: &[f64],
749    sweep: &AroonOscBatchRange,
750    kern: Kernel,
751    parallel: bool,
752) -> Result<AroonOscBatchOutput, AroonOscError> {
753    let combos = expand_grid(sweep)?;
754    if combos.is_empty() {
755        return Err(AroonOscError::InvalidRange {
756            start: sweep.length.0,
757            end: sweep.length.1,
758            step: sweep.length.2,
759        });
760    }
761    if high.len() != low.len() {
762        return Err(AroonOscError::MismatchSliceLength {
763            high_len: high.len(),
764            low_len: low.len(),
765        });
766    }
767
768    let len = high.len();
769    let first = first_valid_hilo(high, low).ok_or(AroonOscError::AllValuesNaN)?;
770
771    let max_len = combos.iter().map(|c| c.length.unwrap()).max().unwrap();
772    let needed = max_len.checked_add(1).ok_or(AroonOscError::InvalidRange {
773        start: sweep.length.0,
774        end: sweep.length.1,
775        step: sweep.length.2,
776    })?;
777    let available = len.checked_sub(first).ok_or(AroonOscError::InvalidRange {
778        start: sweep.length.0,
779        end: sweep.length.1,
780        step: sweep.length.2,
781    })?;
782    if available < needed {
783        return Err(AroonOscError::NotEnoughValidData {
784            needed,
785            valid: available,
786        });
787    }
788
789    let rows = combos.len();
790    let cols = len;
791
792    rows.checked_mul(cols).ok_or(AroonOscError::InvalidRange {
793        start: sweep.length.0,
794        end: sweep.length.1,
795        step: sweep.length.2,
796    })?;
797
798    let mut buf_mu = make_uninit_matrix(rows, cols);
799
800    let warmup_periods: Vec<usize> = combos
801        .iter()
802        .map(|c| {
803            first
804                .checked_add(c.length.unwrap())
805                .ok_or(AroonOscError::InvalidRange {
806                    start: sweep.length.0,
807                    end: sweep.length.1,
808                    step: sweep.length.2,
809                })
810        })
811        .collect::<Result<_, _>>()?;
812    init_matrix_prefixes(&mut buf_mu, cols, &warmup_periods);
813
814    let mut buf_guard = ManuallyDrop::new(buf_mu);
815    let values: &mut [f64] = unsafe {
816        core::slice::from_raw_parts_mut(buf_guard.as_mut_ptr() as *mut f64, buf_guard.len())
817    };
818
819    let do_row = |row: usize, out_row: &mut [f64]| {
820        let length = combos[row].length.unwrap();
821        aroon_osc_scalar_highlow_into(high, low, length, first, out_row);
822    };
823
824    if parallel {
825        #[cfg(not(target_arch = "wasm32"))]
826        {
827            values
828                .par_chunks_mut(cols)
829                .enumerate()
830                .for_each(|(row, slice)| do_row(row, slice));
831        }
832
833        #[cfg(target_arch = "wasm32")]
834        {
835            for (row, slice) in values.chunks_mut(cols).enumerate() {
836                do_row(row, slice);
837            }
838        }
839    } else {
840        for (row, slice) in values.chunks_mut(cols).enumerate() {
841            do_row(row, slice);
842        }
843    }
844
845    let values = unsafe {
846        Vec::from_raw_parts(
847            buf_guard.as_mut_ptr() as *mut f64,
848            buf_guard.len(),
849            buf_guard.capacity(),
850        )
851    };
852
853    Ok(AroonOscBatchOutput {
854        values,
855        combos,
856        rows,
857        cols,
858    })
859}
860
861#[inline(always)]
862fn aroon_osc_batch_inner_into(
863    high: &[f64],
864    low: &[f64],
865    sweep: &AroonOscBatchRange,
866    kern: Kernel,
867    parallel: bool,
868    out: &mut [f64],
869) -> Result<Vec<AroonOscParams>, AroonOscError> {
870    let combos = expand_grid(sweep)?;
871    if combos.is_empty() {
872        return Err(AroonOscError::InvalidRange {
873            start: sweep.length.0,
874            end: sweep.length.1,
875            step: sweep.length.2,
876        });
877    }
878    if high.len() != low.len() {
879        return Err(AroonOscError::MismatchSliceLength {
880            high_len: high.len(),
881            low_len: low.len(),
882        });
883    }
884
885    let len = high.len();
886    let first = first_valid_hilo(high, low).ok_or(AroonOscError::AllValuesNaN)?;
887    let max_len = combos.iter().map(|c| c.length.unwrap()).max().unwrap();
888    let needed = max_len.checked_add(1).ok_or(AroonOscError::InvalidRange {
889        start: sweep.length.0,
890        end: sweep.length.1,
891        step: sweep.length.2,
892    })?;
893    let available = len.checked_sub(first).ok_or(AroonOscError::InvalidRange {
894        start: sweep.length.0,
895        end: sweep.length.1,
896        step: sweep.length.2,
897    })?;
898    if available < needed {
899        return Err(AroonOscError::NotEnoughValidData {
900            needed,
901            valid: available,
902        });
903    }
904
905    let rows = combos.len();
906    let cols = len;
907    let warmup_periods: Vec<usize> = combos
908        .iter()
909        .map(|c| {
910            first
911                .checked_add(c.length.unwrap())
912                .ok_or(AroonOscError::InvalidRange {
913                    start: sweep.length.0,
914                    end: sweep.length.1,
915                    step: sweep.length.2,
916                })
917        })
918        .collect::<Result<_, _>>()?;
919
920    let mut out_uninit = unsafe {
921        Vec::from_raw_parts(
922            out.as_mut_ptr() as *mut std::mem::MaybeUninit<f64>,
923            out.len(),
924            out.len(),
925        )
926    };
927    init_matrix_prefixes(&mut out_uninit, cols, &warmup_periods);
928    std::mem::forget(out_uninit);
929
930    let out_mu = unsafe {
931        std::slice::from_raw_parts_mut(
932            out.as_mut_ptr() as *mut std::mem::MaybeUninit<f64>,
933            out.len(),
934        )
935    };
936
937    let do_row = |row: usize, row_mu: &mut [std::mem::MaybeUninit<f64>]| {
938        let dst = unsafe {
939            core::slice::from_raw_parts_mut(row_mu.as_mut_ptr() as *mut f64, row_mu.len())
940        };
941        let length = combos[row].length.unwrap();
942        aroon_osc_scalar_highlow_into(high, low, length, first, dst);
943    };
944
945    if parallel {
946        #[cfg(not(target_arch = "wasm32"))]
947        out_mu
948            .par_chunks_mut(cols)
949            .enumerate()
950            .for_each(|(r, s)| do_row(r, s));
951        #[cfg(target_arch = "wasm32")]
952        for (r, s) in out_mu.chunks_mut(cols).enumerate() {
953            do_row(r, s);
954        }
955    } else {
956        for (r, s) in out_mu.chunks_mut(cols).enumerate() {
957            do_row(r, s);
958        }
959    }
960
961    Ok(combos)
962}
963
964#[inline(always)]
965pub unsafe fn aroon_osc_row_scalar(
966    high: &[f64],
967    low: &[f64],
968    length: usize,
969    first: usize,
970    out: &mut [f64],
971) {
972    aroon_osc_scalar_highlow_into(high, low, length, first, out)
973}
974#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
975#[inline(always)]
976pub unsafe fn aroon_osc_row_avx2(
977    high: &[f64],
978    low: &[f64],
979    length: usize,
980    first: usize,
981    out: &mut [f64],
982) {
983    aroon_osc_scalar_highlow_into(high, low, length, first, out)
984}
985#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
986#[inline(always)]
987pub unsafe fn aroon_osc_row_avx512(
988    high: &[f64],
989    low: &[f64],
990    length: usize,
991    first: usize,
992    out: &mut [f64],
993) {
994    if length <= 32 {
995        aroon_osc_avx512_short(high, low, length, first, out);
996    } else {
997        aroon_osc_avx512_long(high, low, length, first, out);
998    }
999}
1000#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1001#[inline(always)]
1002pub unsafe fn aroon_osc_row_avx512_short(
1003    high: &[f64],
1004    low: &[f64],
1005    length: usize,
1006    first: usize,
1007    out: &mut [f64],
1008) {
1009    aroon_osc_scalar_highlow_into(high, low, length, first, out)
1010}
1011#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1012#[inline(always)]
1013pub unsafe fn aroon_osc_row_avx512_long(
1014    high: &[f64],
1015    low: &[f64],
1016    length: usize,
1017    first: usize,
1018    out: &mut [f64],
1019) {
1020    aroon_osc_scalar_highlow_into(high, low, length, first, out)
1021}
1022
1023#[inline]
1024pub fn aroon_osc_batch_into_slice(
1025    high: &[f64],
1026    low: &[f64],
1027    sweep: &AroonOscBatchRange,
1028    kern: Kernel,
1029    parallel: bool,
1030    out: &mut [f64],
1031) -> Result<Vec<AroonOscParams>, AroonOscError> {
1032    let combos = expand_grid(sweep)?;
1033    if combos.is_empty() {
1034        return Err(AroonOscError::InvalidRange {
1035            start: sweep.length.0,
1036            end: sweep.length.1,
1037            step: sweep.length.2,
1038        });
1039    }
1040
1041    let len = high.len();
1042    if high.len() != low.len() {
1043        return Err(AroonOscError::MismatchSliceLength {
1044            high_len: high.len(),
1045            low_len: low.len(),
1046        });
1047    }
1048
1049    let expected_len = combos
1050        .len()
1051        .checked_mul(len)
1052        .ok_or(AroonOscError::InvalidRange {
1053            start: sweep.length.0,
1054            end: sweep.length.1,
1055            step: sweep.length.2,
1056        })?;
1057    if out.len() != expected_len {
1058        return Err(AroonOscError::OutputLengthMismatch {
1059            expected: expected_len,
1060            got: out.len(),
1061        });
1062    }
1063
1064    aroon_osc_batch_inner_into(high, low, sweep, kern, parallel, out)
1065}
1066
1067#[derive(Debug, Clone)]
1068pub struct AroonOscStream {
1069    length: usize,
1070    scale: f64,
1071    cap: usize,
1072    t: usize,
1073
1074    hi_idx: Vec<usize>,
1075    hi_val: Vec<f64>,
1076    hi_head: usize,
1077    hi_tail: usize,
1078    hi_len: usize,
1079
1080    lo_idx: Vec<usize>,
1081    lo_val: Vec<f64>,
1082    lo_head: usize,
1083    lo_tail: usize,
1084    lo_len: usize,
1085}
1086
1087impl AroonOscStream {
1088    #[inline(always)]
1089    pub fn try_new(params: AroonOscParams) -> Result<Self, AroonOscError> {
1090        let length = params.length.unwrap_or(14);
1091        if length == 0 {
1092            return Err(AroonOscError::InvalidPeriod {
1093                period: length,
1094                data_len: 0,
1095            });
1096        }
1097        let cap = length.checked_add(1).ok_or(AroonOscError::InvalidPeriod {
1098            period: length,
1099            data_len: 0,
1100        })?;
1101        Ok(Self {
1102            length,
1103            scale: 100.0 / length as f64,
1104            cap,
1105            t: 0,
1106            hi_idx: vec![0; cap],
1107            hi_val: vec![0.0; cap],
1108            hi_head: 0,
1109            hi_tail: 0,
1110            hi_len: 0,
1111            lo_idx: vec![0; cap],
1112            lo_val: vec![0.0; cap],
1113            lo_head: 0,
1114            lo_tail: 0,
1115            lo_len: 0,
1116        })
1117    }
1118
1119    #[inline(always)]
1120    pub fn update(&mut self, high: f64, low: f64) -> Option<f64> {
1121        let idx = self.t;
1122        let min_idx_in_window = idx.saturating_sub(self.length);
1123
1124        while self.hi_len > 0 && self.hi_idx[self.hi_head] < min_idx_in_window {
1125            self.hi_head = self.inc_wrap(self.hi_head);
1126            self.hi_len -= 1;
1127        }
1128        while self.lo_len > 0 && self.lo_idx[self.lo_head] < min_idx_in_window {
1129            self.lo_head = self.inc_wrap(self.lo_head);
1130            self.lo_len -= 1;
1131        }
1132
1133        let h = if high.is_finite() {
1134            high
1135        } else {
1136            f64::NEG_INFINITY
1137        };
1138        let l = if low.is_finite() { low } else { f64::INFINITY };
1139
1140        while self.hi_len > 0 {
1141            let last = self.dec_wrap(self.hi_tail);
1142            if self.hi_val[last] < h {
1143                self.hi_tail = last;
1144                self.hi_len -= 1;
1145            } else {
1146                break;
1147            }
1148        }
1149        self.hi_idx[self.hi_tail] = idx;
1150        self.hi_val[self.hi_tail] = h;
1151        self.hi_tail = self.inc_wrap(self.hi_tail);
1152        self.hi_len += 1;
1153
1154        while self.lo_len > 0 {
1155            let last = self.dec_wrap(self.lo_tail);
1156            if self.lo_val[last] > l {
1157                self.lo_tail = last;
1158                self.lo_len -= 1;
1159            } else {
1160                break;
1161            }
1162        }
1163        self.lo_idx[self.lo_tail] = idx;
1164        self.lo_val[self.lo_tail] = l;
1165        self.lo_tail = self.inc_wrap(self.lo_tail);
1166        self.lo_len += 1;
1167
1168        self.t = idx.wrapping_add(1);
1169
1170        if idx < self.length {
1171            return None;
1172        }
1173        debug_assert!(self.hi_len > 0 && self.lo_len > 0);
1174
1175        let hi_i = self.hi_idx[self.hi_head] as i64;
1176        let lo_i = self.lo_idx[self.lo_head] as i64;
1177        let v = (hi_i - lo_i) as f64 * self.scale;
1178
1179        Some(v.max(-100.0).min(100.0))
1180    }
1181
1182    #[inline(always)]
1183    fn inc_wrap(&self, x: usize) -> usize {
1184        let y = x + 1;
1185        if y == self.cap {
1186            0
1187        } else {
1188            y
1189        }
1190    }
1191    #[inline(always)]
1192    fn dec_wrap(&self, x: usize) -> usize {
1193        if x == 0 {
1194            self.cap - 1
1195        } else {
1196            x - 1
1197        }
1198    }
1199}
1200
1201#[cfg(test)]
1202mod tests {
1203    use super::*;
1204    use crate::skip_if_unsupported;
1205    use crate::utilities::data_loader::read_candles_from_csv;
1206
1207    #[test]
1208    fn test_aroonosc_into_matches_api() -> Result<(), Box<dyn std::error::Error>> {
1209        let n = 256usize;
1210        let timestamp: Vec<i64> = (0..n as i64).collect();
1211        let mut open = Vec::with_capacity(n);
1212        let mut high = Vec::with_capacity(n);
1213        let mut low = Vec::with_capacity(n);
1214        let mut close = Vec::with_capacity(n);
1215        let mut volume = Vec::with_capacity(n);
1216
1217        for i in 0..n {
1218            let ib = i as f64;
1219            let base = 1000.0 + (ib * 0.05).sin() * 10.0 + (i % 7) as f64;
1220            let spread = 5.0 + (i % 11) as f64 * 0.1;
1221            let h = base + spread;
1222            let l = base - spread;
1223            let o = base;
1224            let c = base + ((i % 3) as f64 - 1.0) * 0.5;
1225            let v = 1000.0 + (i % 5) as f64 * 10.0;
1226            open.push(o);
1227            high.push(h);
1228            low.push(l);
1229            close.push(c);
1230            volume.push(v);
1231        }
1232
1233        let candles = Candles::new(timestamp, open, high.clone(), low.clone(), close, volume);
1234        let input = AroonOscInput::with_default_candles(&candles);
1235
1236        let baseline = aroon_osc(&input)?.values;
1237
1238        let mut out = vec![0.0; n];
1239        aroon_osc_into(&input, &mut out)?;
1240
1241        assert_eq!(baseline.len(), out.len());
1242
1243        fn eq_or_both_nan(a: f64, b: f64) -> bool {
1244            (a.is_nan() && b.is_nan()) || (a == b)
1245        }
1246
1247        for (i, (&a, &b)) in baseline.iter().zip(out.iter()).enumerate() {
1248            assert!(
1249                eq_or_both_nan(a, b),
1250                "Mismatch at index {}: baseline={}, into={}",
1251                i,
1252                a,
1253                b
1254            );
1255        }
1256
1257        Ok(())
1258    }
1259    fn check_aroonosc_partial_params(
1260        test_name: &str,
1261        kernel: Kernel,
1262    ) -> Result<(), Box<dyn std::error::Error>> {
1263        skip_if_unsupported!(kernel, test_name);
1264        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1265        let candles = read_candles_from_csv(file_path)?;
1266        let partial_params = AroonOscParams { length: Some(20) };
1267        let input = AroonOscInput::from_candles(&candles, partial_params);
1268        let result = aroon_osc_with_kernel(&input, kernel)?;
1269        assert_eq!(result.values.len(), candles.close.len());
1270        Ok(())
1271    }
1272    fn check_aroonosc_accuracy(
1273        test_name: &str,
1274        kernel: Kernel,
1275    ) -> Result<(), Box<dyn std::error::Error>> {
1276        skip_if_unsupported!(kernel, test_name);
1277        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1278        let candles = read_candles_from_csv(file_path)?;
1279        let input = AroonOscInput::with_default_candles(&candles);
1280        let result = aroon_osc_with_kernel(&input, kernel)?;
1281        let expected_last_five = [-50.0, -50.0, -50.0, -50.0, -42.8571];
1282        assert!(result.values.len() >= 5, "Not enough Aroon Osc values");
1283        assert_eq!(result.values.len(), candles.close.len());
1284        let start_index = result.values.len().saturating_sub(5);
1285        let last_five = &result.values[start_index..];
1286        for (i, &value) in last_five.iter().enumerate() {
1287            assert!(
1288                (value - expected_last_five[i]).abs() < 1e-2,
1289                "Aroon Osc mismatch at index {}: expected {}, got {}",
1290                i,
1291                expected_last_five[i],
1292                value
1293            );
1294        }
1295        let length = 14;
1296        for val in result.values.iter().skip(length) {
1297            if !val.is_nan() {
1298                assert!(
1299                    val.is_finite(),
1300                    "Aroon Osc should be finite after enough data"
1301                );
1302            }
1303        }
1304        Ok(())
1305    }
1306    fn check_aroonosc_default_candles(
1307        test_name: &str,
1308        kernel: Kernel,
1309    ) -> Result<(), Box<dyn std::error::Error>> {
1310        skip_if_unsupported!(kernel, test_name);
1311        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1312        let candles = read_candles_from_csv(file_path)?;
1313        let input = AroonOscInput::with_default_candles(&candles);
1314        match input.data {
1315            AroonOscData::Candles { .. } => {}
1316            _ => panic!("Expected AroonOscData::Candles variant"),
1317        }
1318        assert!(input.params.length.is_some());
1319        Ok(())
1320    }
1321    fn check_aroonosc_with_slices_data_reinput(
1322        test_name: &str,
1323        kernel: Kernel,
1324    ) -> Result<(), Box<dyn std::error::Error>> {
1325        skip_if_unsupported!(kernel, test_name);
1326        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1327        let candles = read_candles_from_csv(file_path)?;
1328        let first_params = AroonOscParams { length: Some(10) };
1329        let first_input = AroonOscInput::from_candles(&candles, first_params);
1330        let first_result = aroon_osc_with_kernel(&first_input, kernel)?;
1331        let second_params = AroonOscParams { length: Some(5) };
1332        let second_input = AroonOscInput::from_slices_hl(
1333            &first_result.values,
1334            &first_result.values,
1335            second_params,
1336        );
1337        let second_result = aroon_osc_with_kernel(&second_input, kernel)?;
1338        assert_eq!(second_result.values.len(), first_result.values.len());
1339        for i in 20..second_result.values.len() {
1340            assert!(!second_result.values[i].is_nan());
1341        }
1342        Ok(())
1343    }
1344    fn check_aroonosc_nan_handling(
1345        test_name: &str,
1346        kernel: Kernel,
1347    ) -> Result<(), Box<dyn std::error::Error>> {
1348        skip_if_unsupported!(kernel, test_name);
1349        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1350        let candles = read_candles_from_csv(file_path)?;
1351        let input = AroonOscInput::with_default_candles(&candles);
1352        let result = aroon_osc_with_kernel(&input, kernel)?;
1353        if result.values.len() > 50 {
1354            for i in 50..result.values.len() {
1355                assert!(
1356                    !result.values[i].is_nan(),
1357                    "Expected no NaN after index {}, but found NaN",
1358                    i
1359                );
1360            }
1361        }
1362        Ok(())
1363    }
1364
1365    #[cfg(debug_assertions)]
1366    fn check_aroonosc_no_poison(
1367        test_name: &str,
1368        kernel: Kernel,
1369    ) -> Result<(), Box<dyn std::error::Error>> {
1370        skip_if_unsupported!(kernel, test_name);
1371
1372        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1373        let candles = read_candles_from_csv(file_path)?;
1374
1375        let test_lengths = vec![5, 14, 25, 50, 100, 200];
1376
1377        for length in test_lengths {
1378            let params = AroonOscParams {
1379                length: Some(length),
1380            };
1381            let input = AroonOscInput::from_candles(&candles, params);
1382
1383            if candles.close.len() < length {
1384                continue;
1385            }
1386
1387            let output = aroon_osc_with_kernel(&input, kernel)?;
1388
1389            for (i, &val) in output.values.iter().enumerate() {
1390                if val.is_nan() {
1391                    continue;
1392                }
1393
1394                let bits = val.to_bits();
1395
1396                if bits == 0x11111111_11111111 {
1397                    panic!(
1398						"[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at index {} with length {}",
1399						test_name, val, bits, i, length
1400					);
1401                }
1402
1403                if bits == 0x22222222_22222222 {
1404                    panic!(
1405						"[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at index {} with length {}",
1406						test_name, val, bits, i, length
1407					);
1408                }
1409
1410                if bits == 0x33333333_33333333 {
1411                    panic!(
1412						"[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at index {} with length {}",
1413						test_name, val, bits, i, length
1414					);
1415                }
1416            }
1417        }
1418
1419        Ok(())
1420    }
1421
1422    #[cfg(not(debug_assertions))]
1423    fn check_aroonosc_no_poison(
1424        _test_name: &str,
1425        _kernel: Kernel,
1426    ) -> Result<(), Box<dyn std::error::Error>> {
1427        Ok(())
1428    }
1429
1430    #[cfg(feature = "proptest")]
1431    #[allow(clippy::float_cmp)]
1432    fn check_aroonosc_property(
1433        test_name: &str,
1434        kernel: Kernel,
1435    ) -> Result<(), Box<dyn std::error::Error>> {
1436        use proptest::prelude::*;
1437        skip_if_unsupported!(kernel, test_name);
1438
1439        let strat = (2usize..=100)
1440            .prop_flat_map(|length| {
1441                let min_size = (length * 2).max(length + 20);
1442                let max_size = 400;
1443                (
1444                    10.0f64..1000.0f64,
1445                    0.0f64..0.1f64,
1446                    -0.02f64..0.02f64,
1447                    min_size..max_size,
1448                    Just(length),
1449                    0u8..6,
1450                )
1451            })
1452            .prop_map(
1453                |(base_price, volatility, trend, size, length, market_type)| {
1454                    let mut high = Vec::with_capacity(size);
1455                    let mut low = Vec::with_capacity(size);
1456
1457                    for i in 0..size {
1458                        let time_factor = i as f64 / size as f64;
1459
1460                        let (h, l) = match market_type {
1461                            0 => {
1462                                let cycle = (time_factor * 4.0 * std::f64::consts::PI).sin();
1463                                let price = base_price * (1.0 + cycle * volatility);
1464                                let spread = price * volatility * 0.5;
1465                                (price + spread, price - spread)
1466                            }
1467                            1 => {
1468                                let price = base_price * (1.0 + trend.abs() * i as f64);
1469                                let noise = ((i * 17 + 13) % 100) as f64 / 100.0 - 0.5;
1470                                let variation = price * volatility * noise * 0.3;
1471                                let spread = price * volatility * 0.2;
1472                                (price + variation + spread, price + variation - spread)
1473                            }
1474                            2 => {
1475                                let price = base_price * (1.0 - trend.abs() * i as f64).max(1.0);
1476                                let noise = ((i * 23 + 7) % 100) as f64 / 100.0 - 0.5;
1477                                let variation = price * volatility * noise * 0.3;
1478                                let spread = price * volatility * 0.2;
1479                                (price + variation + spread, price + variation - spread)
1480                            }
1481                            3 => {
1482                                let price = base_price;
1483                                (price, price)
1484                            }
1485                            4 => {
1486                                let price = base_price + (i as f64 * base_price * 0.01);
1487                                let spread = price * 0.001;
1488                                (price + spread, price - spread)
1489                            }
1490                            _ => {
1491                                let price = base_price
1492                                    - (i as f64 * base_price * 0.005).min(base_price * 0.9);
1493                                let spread = price * 0.001;
1494                                (price + spread, price - spread)
1495                            }
1496                        };
1497
1498                        high.push(h.max(l));
1499                        low.push(h.min(l));
1500                    }
1501
1502                    (high, low, length, market_type)
1503                },
1504            );
1505
1506        proptest::test_runner::TestRunner::default()
1507            .run(&strat, |(high, low, length, market_type)| {
1508                let params = AroonOscParams {
1509                    length: Some(length),
1510                };
1511                let input = AroonOscInput::from_slices_hl(&high, &low, params);
1512
1513                let result = aroon_osc_with_kernel(&input, kernel)?;
1514
1515                let reference = aroon_osc_with_kernel(&input, Kernel::Scalar)?;
1516
1517                prop_assert_eq!(result.values.len(), high.len(), "Output length mismatch");
1518
1519                for i in 0..length {
1520                    prop_assert!(
1521                        result.values[i].is_nan(),
1522                        "Expected NaN at index {} during warmup (length={})",
1523                        i,
1524                        length
1525                    );
1526                }
1527
1528                for i in length..result.values.len() {
1529                    let val = result.values[i];
1530                    let ref_val = reference.values[i];
1531
1532                    prop_assert!(
1533                        val >= -100.0 && val <= 100.0,
1534                        "AroonOsc value {} at index {} out of range [-100, 100]",
1535                        val,
1536                        i
1537                    );
1538
1539                    if val.is_finite() && ref_val.is_finite() {
1540                        let diff = (val - ref_val).abs();
1541                        prop_assert!(
1542                            diff <= 1e-9,
1543                            "Kernel mismatch at index {}: {} vs {} (diff={})",
1544                            i,
1545                            val,
1546                            ref_val,
1547                            diff
1548                        );
1549                    } else {
1550                        prop_assert_eq!(
1551                            val.is_nan(),
1552                            ref_val.is_nan(),
1553                            "NaN mismatch at index {}: {} vs {}",
1554                            i,
1555                            val,
1556                            ref_val
1557                        );
1558                    }
1559
1560                    let window_start = i.saturating_sub(length);
1561                    let window_high = &high[window_start..=i];
1562                    let window_low = &low[window_start..=i];
1563
1564                    if window_high
1565                        .iter()
1566                        .all(|&h| (h - window_high[0]).abs() < f64::EPSILON)
1567                        && window_low
1568                            .iter()
1569                            .all(|&l| (l - window_low[0]).abs() < f64::EPSILON)
1570                    {
1571                        prop_assert!(
1572							val.abs() < 1e-9,
1573							"Completely flat window should produce AroonOsc = 0, got {} at index {}",
1574							val,
1575							i
1576						);
1577                    }
1578
1579                    let highest_idx = window_high
1580                        .iter()
1581                        .enumerate()
1582                        .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
1583                        .map(|(idx, _)| idx)
1584                        .unwrap_or(0);
1585
1586                    let lowest_idx = window_low
1587                        .iter()
1588                        .enumerate()
1589                        .min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
1590                        .map(|(idx, _)| idx)
1591                        .unwrap_or(0);
1592
1593                    if highest_idx == window_high.len() - 1 {
1594                        prop_assert!(
1595							val >= -100.0 && val <= 100.0,
1596							"When highest high is most recent, AroonOsc {} should be valid at index {}",
1597							val,
1598							i
1599						);
1600                    }
1601
1602                    if lowest_idx == window_low.len() - 1 {
1603                        prop_assert!(
1604							val >= -100.0 && val <= 100.0,
1605							"When lowest low is most recent, AroonOsc {} should be valid at index {}",
1606							val,
1607							i
1608						);
1609                    }
1610
1611                    if market_type == 4 {
1612                        prop_assert!(
1613							val >= -100.0,
1614							"Monotonic increasing should not produce very negative AroonOsc, got {} at index {}",
1615							val,
1616							i
1617						);
1618                    } else if market_type == 5 {
1619                        prop_assert!(
1620							val <= 100.0,
1621							"Monotonic decreasing should not produce very positive AroonOsc, got {} at index {}",
1622							val,
1623							i
1624						);
1625                    }
1626
1627                    let is_flat_window = window_high
1628                        .iter()
1629                        .all(|&h| (h - window_high[0]).abs() < f64::EPSILON)
1630                        && window_low
1631                            .iter()
1632                            .all(|&l| (l - window_low[0]).abs() < f64::EPSILON);
1633
1634                    if !is_flat_window {
1635                        if highest_idx == window_high.len() - 1 && lowest_idx == 0 {
1636                            prop_assert!(
1637								val >= 50.0,
1638								"When highest is recent and lowest is old, AroonOsc {} should be positive at index {}",
1639								val,
1640								i
1641							);
1642                        } else if lowest_idx == window_low.len() - 1 && highest_idx == 0 {
1643                            prop_assert!(
1644								val <= -50.0,
1645								"When lowest is recent and highest is old, AroonOsc {} should be negative at index {}",
1646								val,
1647								i
1648							);
1649                        }
1650                    }
1651                }
1652
1653                #[cfg(debug_assertions)]
1654                for &val in &result.values {
1655                    if !val.is_nan() {
1656                        let bits = val.to_bits();
1657                        prop_assert_ne!(
1658                            bits,
1659                            0x11111111_11111111,
1660                            "Found poison value from alloc_with_nan_prefix"
1661                        );
1662                        prop_assert_ne!(
1663                            bits,
1664                            0x22222222_22222222,
1665                            "Found poison value from init_matrix_prefixes"
1666                        );
1667                        prop_assert_ne!(
1668                            bits,
1669                            0x33333333_33333333,
1670                            "Found poison value from make_uninit_matrix"
1671                        );
1672                    }
1673                }
1674
1675                Ok(())
1676            })
1677            .unwrap();
1678
1679        Ok(())
1680    }
1681
1682    macro_rules! generate_all_aroonosc_tests {
1683        ($($test_fn:ident),*) => {
1684            paste! {
1685                $(
1686                    #[test]
1687                    fn [<$test_fn _scalar_f64>]() {
1688                        let _ = $test_fn(stringify!([<$test_fn _scalar_f64>]), Kernel::Scalar);
1689                    }
1690                )*
1691                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1692                $(
1693                    #[test]
1694                    fn [<$test_fn _avx2_f64>]() {
1695                        let _ = $test_fn(stringify!([<$test_fn _avx2_f64>]), Kernel::Avx2);
1696                    }
1697                    #[test]
1698                    fn [<$test_fn _avx512_f64>]() {
1699                        let _ = $test_fn(stringify!([<$test_fn _avx512_f64>]), Kernel::Avx512);
1700                    }
1701                )*
1702            }
1703        }
1704    }
1705    generate_all_aroonosc_tests!(
1706        check_aroonosc_partial_params,
1707        check_aroonosc_accuracy,
1708        check_aroonosc_default_candles,
1709        check_aroonosc_with_slices_data_reinput,
1710        check_aroonosc_nan_handling,
1711        check_aroonosc_no_poison
1712    );
1713
1714    #[cfg(feature = "proptest")]
1715    generate_all_aroonosc_tests!(check_aroonosc_property);
1716    fn check_batch_default_row(
1717        test: &str,
1718        kernel: Kernel,
1719    ) -> Result<(), Box<dyn std::error::Error>> {
1720        skip_if_unsupported!(kernel, test);
1721        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1722        let c = read_candles_from_csv(file)?;
1723        let output = AroonOscBatchBuilder::new()
1724            .kernel(kernel)
1725            .apply_candles(&c)?;
1726        let def = AroonOscParams::default();
1727        let row = output.values_for(&def).expect("default row missing");
1728        assert_eq!(row.len(), c.close.len());
1729        Ok(())
1730    }
1731
1732    #[cfg(debug_assertions)]
1733    fn check_batch_no_poison(test: &str, kernel: Kernel) -> Result<(), Box<dyn std::error::Error>> {
1734        skip_if_unsupported!(kernel, test);
1735
1736        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1737        let c = read_candles_from_csv(file)?;
1738
1739        let test_configs = vec![
1740            (2, 10, 2),
1741            (5, 25, 5),
1742            (10, 100, 10),
1743            (50, 200, 50),
1744            (14, 14, 0),
1745            (1, 5, 1),
1746        ];
1747
1748        for (start, end, step) in test_configs {
1749            if c.close.len() < end {
1750                continue;
1751            }
1752
1753            let output = AroonOscBatchBuilder::new()
1754                .kernel(kernel)
1755                .length_range(start, end, step)
1756                .apply_candles(&c)?;
1757
1758            for (idx, &val) in output.values.iter().enumerate() {
1759                if val.is_nan() {
1760                    continue;
1761                }
1762
1763                let bits = val.to_bits();
1764                let row = idx / output.cols;
1765                let col = idx % output.cols;
1766                let length = output.combos[row].length.unwrap_or(14);
1767
1768                if bits == 0x11111111_11111111 {
1769                    panic!(
1770                        "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at row {} col {} (flat index {}) with length {} in range ({}, {}, {})",
1771                        test, val, bits, row, col, idx, length, start, end, step
1772                    );
1773                }
1774
1775                if bits == 0x22222222_22222222 {
1776                    panic!(
1777                        "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at row {} col {} (flat index {}) with length {} in range ({}, {}, {})",
1778                        test, val, bits, row, col, idx, length, start, end, step
1779                    );
1780                }
1781
1782                if bits == 0x33333333_33333333 {
1783                    panic!(
1784                        "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at row {} col {} (flat index {}) with length {} in range ({}, {}, {})",
1785                        test, val, bits, row, col, idx, length, start, end, step
1786                    );
1787                }
1788            }
1789        }
1790
1791        Ok(())
1792    }
1793
1794    #[cfg(not(debug_assertions))]
1795    fn check_batch_no_poison(
1796        _test: &str,
1797        _kernel: Kernel,
1798    ) -> Result<(), Box<dyn std::error::Error>> {
1799        Ok(())
1800    }
1801
1802    macro_rules! gen_batch_tests {
1803        ($fn_name:ident) => {
1804            paste! {
1805                #[test] fn [<$fn_name _scalar>]() { let _ = $fn_name(stringify!([<$fn_name _scalar>]), Kernel::ScalarBatch); }
1806                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1807                #[test] fn [<$fn_name _avx2>]()   { let _ = $fn_name(stringify!([<$fn_name _avx2>]), Kernel::Avx2Batch); }
1808                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1809                #[test] fn [<$fn_name _avx512>]() { let _ = $fn_name(stringify!([<$fn_name _avx512>]), Kernel::Avx512Batch); }
1810            }
1811        };
1812    }
1813    gen_batch_tests!(check_batch_default_row);
1814    gen_batch_tests!(check_batch_no_poison);
1815}
1816
1817#[cfg(all(feature = "python", feature = "cuda"))]
1818use crate::cuda::oscillators::{CudaAroonOsc, DeviceArrayF32Aroonosc};
1819#[cfg(feature = "python")]
1820use numpy::{IntoPyArray, PyArray1};
1821#[cfg(feature = "python")]
1822use pyo3::exceptions::PyValueError;
1823#[cfg(feature = "python")]
1824use pyo3::prelude::*;
1825#[cfg(feature = "python")]
1826use pyo3::types::{PyDict, PyList};
1827
1828#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1829use serde::{Deserialize, Serialize};
1830#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1831use wasm_bindgen::prelude::*;
1832
1833#[cfg(feature = "python")]
1834use crate::utilities::kernel_validation::validate_kernel;
1835
1836#[cfg(feature = "python")]
1837#[pyfunction(name = "aroonosc")]
1838#[pyo3(signature = (high, low, length=14, kernel=None))]
1839pub fn aroon_osc_py<'py>(
1840    py: Python<'py>,
1841    high: numpy::PyReadonlyArray1<'py, f64>,
1842    low: numpy::PyReadonlyArray1<'py, f64>,
1843    length: usize,
1844    kernel: Option<&str>,
1845) -> PyResult<Bound<'py, numpy::PyArray1<f64>>> {
1846    use numpy::{IntoPyArray, PyArrayMethods};
1847
1848    let high_slice = high.as_slice()?;
1849    let low_slice = low.as_slice()?;
1850
1851    if high_slice.len() != low_slice.len() {
1852        return Err(PyValueError::new_err(format!(
1853            "High and low arrays must have same length. Got high: {}, low: {}",
1854            high_slice.len(),
1855            low_slice.len()
1856        )));
1857    }
1858
1859    if length == 0 {
1860        return Err(PyValueError::new_err(
1861            "Invalid length: length must be greater than 0",
1862        ));
1863    }
1864
1865    let kern = validate_kernel(kernel, false)?;
1866
1867    let params = AroonOscParams {
1868        length: Some(length),
1869    };
1870    let aroon_in = AroonOscInput::from_slices_hl(high_slice, low_slice, params);
1871
1872    let result_vec: Vec<f64> = py
1873        .allow_threads(|| aroon_osc_with_kernel(&aroon_in, kern).map(|o| o.values))
1874        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1875
1876    Ok(result_vec.into_pyarray(py))
1877}
1878
1879#[cfg(feature = "python")]
1880#[pyclass(name = "AroonOscStream")]
1881pub struct AroonOscStreamPy {
1882    stream: AroonOscStream,
1883}
1884
1885#[cfg(feature = "python")]
1886#[pymethods]
1887impl AroonOscStreamPy {
1888    #[new]
1889    fn new(length: usize) -> PyResult<Self> {
1890        let params = AroonOscParams {
1891            length: Some(length),
1892        };
1893        let stream =
1894            AroonOscStream::try_new(params).map_err(|e| PyValueError::new_err(e.to_string()))?;
1895        Ok(AroonOscStreamPy { stream })
1896    }
1897
1898    fn update(&mut self, high: f64, low: f64) -> Option<f64> {
1899        self.stream.update(high, low)
1900    }
1901}
1902
1903#[cfg(feature = "python")]
1904#[pyfunction(name = "aroonosc_batch")]
1905#[pyo3(signature = (high, low, length_range, kernel=None))]
1906pub fn aroon_osc_batch_py<'py>(
1907    py: Python<'py>,
1908    high: numpy::PyReadonlyArray1<'py, f64>,
1909    low: numpy::PyReadonlyArray1<'py, f64>,
1910    length_range: (usize, usize, usize),
1911    kernel: Option<&str>,
1912) -> PyResult<Bound<'py, pyo3::types::PyDict>> {
1913    use numpy::{IntoPyArray, PyArray1, PyArrayMethods};
1914    use pyo3::types::PyDict;
1915
1916    let high_slice = high.as_slice()?;
1917    let low_slice = low.as_slice()?;
1918
1919    if high_slice.len() != low_slice.len() {
1920        return Err(PyValueError::new_err(format!(
1921            "High and low arrays must have same length. Got high: {}, low: {}",
1922            high_slice.len(),
1923            low_slice.len()
1924        )));
1925    }
1926
1927    let kern = validate_kernel(kernel, true)?;
1928
1929    let sweep = AroonOscBatchRange {
1930        length: length_range,
1931    };
1932
1933    let combos = expand_grid(&sweep).map_err(|e| PyValueError::new_err(e.to_string()))?;
1934    let rows = combos.len();
1935    let cols = high_slice.len();
1936
1937    let out_arr = unsafe { PyArray1::<f64>::new(py, [rows * cols], false) };
1938    let slice_out = unsafe { out_arr.as_slice_mut()? };
1939
1940    let combos = py
1941        .allow_threads(|| -> Result<Vec<AroonOscParams>, AroonOscError> {
1942            let kernel = match kern {
1943                Kernel::Auto => detect_best_batch_kernel(),
1944                k => k,
1945            };
1946            let simd = match kernel {
1947                Kernel::Avx512Batch => Kernel::Avx512,
1948                Kernel::Avx2Batch => Kernel::Avx2,
1949                Kernel::ScalarBatch => Kernel::Scalar,
1950                _ => unreachable!(),
1951            };
1952
1953            aroon_osc_batch_inner_into(high_slice, low_slice, &sweep, simd, true, slice_out)
1954        })
1955        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1956
1957    let dict = PyDict::new(py);
1958    dict.set_item("values", out_arr.reshape((rows, cols))?)?;
1959    dict.set_item(
1960        "lengths",
1961        combos
1962            .iter()
1963            .map(|p| p.length.unwrap() as u64)
1964            .collect::<Vec<_>>()
1965            .into_pyarray(py),
1966    )?;
1967
1968    Ok(dict)
1969}
1970
1971#[cfg(all(feature = "python", feature = "cuda"))]
1972pub struct PrimaryCtxGuard {
1973    dev: i32,
1974    ctx: cust::sys::CUcontext,
1975}
1976
1977#[cfg(all(feature = "python", feature = "cuda"))]
1978impl PrimaryCtxGuard {
1979    fn new(device_id: u32) -> Result<Self, cust::error::CudaError> {
1980        unsafe {
1981            let mut ctx: cust::sys::CUcontext = core::ptr::null_mut();
1982            let dev = device_id as i32;
1983            let res = cust::sys::cuDevicePrimaryCtxRetain(&mut ctx as *mut _, dev);
1984            if res != cust::sys::CUresult::CUDA_SUCCESS {
1985                return Err(cust::error::CudaError::UnknownError);
1986            }
1987            Ok(PrimaryCtxGuard { dev, ctx })
1988        }
1989    }
1990    #[inline]
1991    unsafe fn push_current(&self) {
1992        let _ = cust::sys::cuCtxSetCurrent(self.ctx);
1993    }
1994}
1995
1996#[cfg(all(feature = "python", feature = "cuda"))]
1997impl Drop for PrimaryCtxGuard {
1998    fn drop(&mut self) {
1999        unsafe {
2000            let _ = cust::sys::cuDevicePrimaryCtxRelease_v2(self.dev);
2001        }
2002    }
2003}
2004
2005#[cfg(all(feature = "python", feature = "cuda"))]
2006#[pyclass(module = "ta_indicators.cuda", unsendable)]
2007pub struct AroonOscDeviceArrayF32Py {
2008    inner: Option<DeviceArrayF32Aroonosc>,
2009    device_id: u32,
2010    pc_guard: PrimaryCtxGuard,
2011}
2012#[cfg(all(feature = "python", feature = "cuda"))]
2013#[pymethods]
2014impl AroonOscDeviceArrayF32Py {
2015    #[getter]
2016    fn __cuda_array_interface__<'py>(
2017        &self,
2018        py: Python<'py>,
2019    ) -> PyResult<Bound<'py, pyo3::types::PyDict>> {
2020        use pyo3::types::PyDict;
2021        let inner = self
2022            .inner
2023            .as_ref()
2024            .ok_or_else(|| PyValueError::new_err("buffer already exported"))?;
2025        let d = PyDict::new(py);
2026        d.set_item("shape", (inner.rows, inner.cols))?;
2027        d.set_item("typestr", "<f4")?;
2028        d.set_item(
2029            "strides",
2030            (
2031                inner.cols * std::mem::size_of::<f32>(),
2032                std::mem::size_of::<f32>(),
2033            ),
2034        )?;
2035        let ptr_val: usize = if inner.rows == 0 || inner.cols == 0 {
2036            0
2037        } else {
2038            inner.device_ptr() as usize
2039        };
2040        d.set_item("data", (ptr_val, false))?;
2041        d.set_item("version", 3)?;
2042        Ok(d)
2043    }
2044
2045    fn __dlpack_device__(&self) -> (i32, i32) {
2046        (2, self.device_id as i32)
2047    }
2048
2049    #[pyo3(signature=(stream=None, max_version=None, dl_device=None, copy=None))]
2050    fn __dlpack__<'py>(
2051        &mut self,
2052        py: Python<'py>,
2053        stream: Option<pyo3::PyObject>,
2054        max_version: Option<pyo3::PyObject>,
2055        dl_device: Option<pyo3::PyObject>,
2056        copy: Option<pyo3::PyObject>,
2057    ) -> PyResult<PyObject> {
2058        use crate::utilities::dlpack_cuda::export_f32_cuda_dlpack_2d;
2059
2060        let (kdl, alloc_dev) = self.__dlpack_device__();
2061        if let Some(dev_obj) = dl_device.as_ref() {
2062            if let Ok((dev_ty, dev_id)) = dev_obj.extract::<(i32, i32)>(py) {
2063                if dev_ty != kdl || dev_id != alloc_dev {
2064                    let wants_copy = copy
2065                        .as_ref()
2066                        .and_then(|c| c.extract::<bool>(py).ok())
2067                        .unwrap_or(false);
2068                    if wants_copy {
2069                        return Err(PyValueError::new_err(
2070                            "device copy not implemented for __dlpack__",
2071                        ));
2072                    } else {
2073                        return Err(PyValueError::new_err("dl_device mismatch for __dlpack__"));
2074                    }
2075                }
2076            }
2077        }
2078        let _ = stream;
2079
2080        let inner = self
2081            .inner
2082            .take()
2083            .ok_or_else(|| PyValueError::new_err("buffer already exported via __dlpack__"))?;
2084        let rows = inner.rows;
2085        let cols = inner.cols;
2086        let buf = inner.buf;
2087
2088        let max_version_bound = max_version.map(|obj| obj.into_bound(py));
2089
2090        export_f32_cuda_dlpack_2d(py, buf, rows, cols, alloc_dev, max_version_bound)
2091    }
2092}
2093
2094#[cfg(all(feature = "python", feature = "cuda"))]
2095impl Drop for AroonOscDeviceArrayF32Py {
2096    fn drop(&mut self) {
2097        unsafe {
2098            self.pc_guard.push_current();
2099        }
2100    }
2101}
2102
2103#[cfg(all(feature = "python", feature = "cuda"))]
2104#[pyfunction(name = "aroonosc_cuda_batch_dev")]
2105#[pyo3(signature = (high_f32, low_f32, length_range, device_id=0))]
2106pub fn aroonosc_cuda_batch_dev_py(
2107    py: Python<'_>,
2108    high_f32: numpy::PyReadonlyArray1<'_, f32>,
2109    low_f32: numpy::PyReadonlyArray1<'_, f32>,
2110    length_range: (usize, usize, usize),
2111    device_id: usize,
2112) -> PyResult<AroonOscDeviceArrayF32Py> {
2113    use crate::cuda::cuda_available;
2114    use pyo3::exceptions::PyValueError;
2115
2116    if !cuda_available() {
2117        return Err(PyValueError::new_err("CUDA not available"));
2118    }
2119
2120    let high = high_f32.as_slice()?;
2121    let low = low_f32.as_slice()?;
2122    if high.len() != low.len() {
2123        return Err(PyValueError::new_err("mismatched input lengths"));
2124    }
2125
2126    let sweep = AroonOscBatchRange {
2127        length: length_range,
2128    };
2129    let inner = py.allow_threads(|| {
2130        let cuda =
2131            CudaAroonOsc::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
2132        cuda.aroonosc_batch_dev(high, low, &sweep)
2133            .map_err(|e| PyValueError::new_err(e.to_string()))
2134    })?;
2135
2136    let guard =
2137        PrimaryCtxGuard::new(device_id as u32).map_err(|e| PyValueError::new_err(e.to_string()))?;
2138    Ok(AroonOscDeviceArrayF32Py {
2139        inner: Some(inner),
2140        device_id: device_id as u32,
2141        pc_guard: guard,
2142    })
2143}
2144
2145#[cfg(all(feature = "python", feature = "cuda"))]
2146#[pyfunction(name = "aroonosc_cuda_many_series_one_param_dev")]
2147#[pyo3(signature = (high_tm_f32, low_tm_f32, length, device_id=0))]
2148pub fn aroonosc_cuda_many_series_one_param_dev_py(
2149    py: Python<'_>,
2150    high_tm_f32: numpy::PyReadonlyArray2<'_, f32>,
2151    low_tm_f32: numpy::PyReadonlyArray2<'_, f32>,
2152    length: usize,
2153    device_id: usize,
2154) -> PyResult<AroonOscDeviceArrayF32Py> {
2155    use crate::cuda::cuda_available;
2156    use numpy::PyUntypedArrayMethods;
2157    use pyo3::exceptions::PyValueError;
2158
2159    if !cuda_available() {
2160        return Err(PyValueError::new_err("CUDA not available"));
2161    }
2162    let shape_h = high_tm_f32.shape();
2163    let shape_l = low_tm_f32.shape();
2164    if shape_h != shape_l || shape_h.len() != 2 {
2165        return Err(PyValueError::new_err("high/low must be same 2D shape"));
2166    }
2167    let rows = shape_h[0];
2168    let cols = shape_h[1];
2169    let h = high_tm_f32.as_slice()?;
2170    let l = low_tm_f32.as_slice()?;
2171    let inner = py.allow_threads(|| {
2172        let cuda =
2173            CudaAroonOsc::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
2174        cuda.aroonosc_many_series_one_param_time_major_dev(h, l, cols, rows, length)
2175            .map_err(|e| PyValueError::new_err(e.to_string()))
2176    })?;
2177    let guard =
2178        PrimaryCtxGuard::new(device_id as u32).map_err(|e| PyValueError::new_err(e.to_string()))?;
2179    Ok(AroonOscDeviceArrayF32Py {
2180        inner: Some(inner),
2181        device_id: device_id as u32,
2182        pc_guard: guard,
2183    })
2184}
2185
2186#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2187#[wasm_bindgen]
2188pub fn aroonosc_js(high: &[f64], low: &[f64], length: usize) -> Result<Vec<f64>, JsValue> {
2189    if high.len() != low.len() {
2190        return Err(JsValue::from_str(&format!(
2191            "High and low arrays must have same length. Got high: {}, low: {}",
2192            high.len(),
2193            low.len()
2194        )));
2195    }
2196
2197    let params = AroonOscParams {
2198        length: Some(length),
2199    };
2200    let input = AroonOscInput::from_slices_hl(high, low, params);
2201
2202    aroon_osc_with_kernel(&input, Kernel::Auto)
2203        .map(|output| output.values)
2204        .map_err(|e| JsValue::from_str(&e.to_string()))
2205}
2206
2207#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2208#[wasm_bindgen]
2209pub fn aroonosc_batch_js(
2210    high: &[f64],
2211    low: &[f64],
2212    length_start: usize,
2213    length_end: usize,
2214    length_step: usize,
2215) -> Result<Vec<f64>, JsValue> {
2216    if high.len() != low.len() {
2217        return Err(JsValue::from_str(&format!(
2218            "High and low arrays must have same length. Got high: {}, low: {}",
2219            high.len(),
2220            low.len()
2221        )));
2222    }
2223
2224    let sweep = AroonOscBatchRange {
2225        length: (length_start, length_end, length_step),
2226    };
2227
2228    aroon_osc_batch_slice(high, low, &sweep, Kernel::Auto)
2229        .map(|output| output.values)
2230        .map_err(|e| JsValue::from_str(&e.to_string()))
2231}
2232
2233#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2234#[wasm_bindgen]
2235pub fn aroonosc_batch_metadata_js(
2236    length_start: usize,
2237    length_end: usize,
2238    length_step: usize,
2239) -> Result<Vec<f64>, JsValue> {
2240    let sweep = AroonOscBatchRange {
2241        length: (length_start, length_end, length_step),
2242    };
2243
2244    let combos = expand_grid(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
2245    let mut metadata = Vec::with_capacity(combos.len());
2246
2247    for combo in combos {
2248        metadata.push(combo.length.unwrap() as f64);
2249    }
2250
2251    Ok(metadata)
2252}
2253
2254#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2255#[derive(Serialize, Deserialize)]
2256pub struct AroonOscBatchConfig {
2257    pub length_range: (usize, usize, usize),
2258}
2259
2260#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2261#[derive(Serialize, Deserialize)]
2262pub struct AroonOscBatchJsOutput {
2263    pub values: Vec<f64>,
2264    pub combos: Vec<AroonOscParams>,
2265    pub rows: usize,
2266    pub cols: usize,
2267}
2268
2269#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2270#[wasm_bindgen(js_name = aroonosc_batch)]
2271pub fn aroon_osc_batch_unified_js(
2272    high: &[f64],
2273    low: &[f64],
2274    config: JsValue,
2275) -> Result<JsValue, JsValue> {
2276    if high.len() != low.len() {
2277        return Err(JsValue::from_str(&format!(
2278            "High and low arrays must have same length. Got high: {}, low: {}",
2279            high.len(),
2280            low.len()
2281        )));
2282    }
2283
2284    let config: AroonOscBatchConfig = serde_wasm_bindgen::from_value(config)
2285        .map_err(|e| JsValue::from_str(&format!("Invalid config: {}", e)))?;
2286
2287    let sweep = AroonOscBatchRange {
2288        length: config.length_range,
2289    };
2290
2291    let output = aroon_osc_batch_slice(high, low, &sweep, Kernel::Auto)
2292        .map_err(|e| JsValue::from_str(&e.to_string()))?;
2293
2294    let js_output = AroonOscBatchJsOutput {
2295        values: output.values,
2296        combos: output.combos,
2297        rows: output.rows,
2298        cols: output.cols,
2299    };
2300
2301    serde_wasm_bindgen::to_value(&js_output)
2302        .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
2303}
2304
2305#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2306#[wasm_bindgen]
2307pub fn aroonosc_alloc(len: usize) -> *mut f64 {
2308    let mut vec = Vec::<f64>::with_capacity(len);
2309    let ptr = vec.as_mut_ptr();
2310    std::mem::forget(vec);
2311    ptr
2312}
2313
2314#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2315#[wasm_bindgen]
2316pub fn aroonosc_free(ptr: *mut f64, len: usize) {
2317    if !ptr.is_null() {
2318        unsafe {
2319            let _ = Vec::from_raw_parts(ptr, len, len);
2320        }
2321    }
2322}
2323
2324#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2325#[wasm_bindgen]
2326pub fn aroonosc_into(
2327    high_ptr: *const f64,
2328    low_ptr: *const f64,
2329    out_ptr: *mut f64,
2330    len: usize,
2331    length: usize,
2332) -> Result<(), JsValue> {
2333    if high_ptr.is_null() || low_ptr.is_null() || out_ptr.is_null() {
2334        return Err(JsValue::from_str("Null pointer provided"));
2335    }
2336
2337    unsafe {
2338        let high = std::slice::from_raw_parts(high_ptr, len);
2339        let low = std::slice::from_raw_parts(low_ptr, len);
2340
2341        let params = AroonOscParams {
2342            length: Some(length),
2343        };
2344        let input = AroonOscInput::from_slices_hl(high, low, params);
2345
2346        if high_ptr == out_ptr || low_ptr == out_ptr {
2347            let mut temp = vec![0.0; len];
2348            aroon_osc_into_slice(&mut temp, &input, Kernel::Auto)
2349                .map_err(|e| JsValue::from_str(&e.to_string()))?;
2350            let out = std::slice::from_raw_parts_mut(out_ptr, len);
2351            out.copy_from_slice(&temp);
2352        } else {
2353            let out = std::slice::from_raw_parts_mut(out_ptr, len);
2354            aroon_osc_into_slice(out, &input, Kernel::Auto)
2355                .map_err(|e| JsValue::from_str(&e.to_string()))?;
2356        }
2357
2358        Ok(())
2359    }
2360}
2361
2362#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2363#[wasm_bindgen]
2364pub fn aroonosc_batch_into(
2365    high_ptr: *const f64,
2366    low_ptr: *const f64,
2367    out_ptr: *mut f64,
2368    len: usize,
2369    length_start: usize,
2370    length_end: usize,
2371    length_step: usize,
2372) -> Result<usize, JsValue> {
2373    if high_ptr.is_null() || low_ptr.is_null() || out_ptr.is_null() {
2374        return Err(JsValue::from_str("Null pointer provided"));
2375    }
2376
2377    unsafe {
2378        let high = std::slice::from_raw_parts(high_ptr, len);
2379        let low = std::slice::from_raw_parts(low_ptr, len);
2380
2381        let sweep = AroonOscBatchRange {
2382            length: (length_start, length_end, length_step),
2383        };
2384
2385        let combos = expand_grid(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
2386        let rows = combos.len();
2387        let expected_len = rows
2388            .checked_mul(len)
2389            .ok_or_else(|| JsValue::from_str("aroonosc: length range too large"))?;
2390        let out = std::slice::from_raw_parts_mut(out_ptr, expected_len);
2391
2392        let high_overlaps = (high_ptr as usize) < (out_ptr as usize + expected_len * 8)
2393            && (high_ptr as usize + len * 8) > (out_ptr as usize);
2394        let low_overlaps = (low_ptr as usize) < (out_ptr as usize + expected_len * 8)
2395            && (low_ptr as usize + len * 8) > (out_ptr as usize);
2396
2397        if high_overlaps || low_overlaps {
2398            let mut temp = vec![0.0; expected_len];
2399            aroon_osc_batch_into_slice(high, low, &sweep, Kernel::Auto, false, &mut temp)
2400                .map_err(|e| JsValue::from_str(&e.to_string()))?;
2401            out.copy_from_slice(&temp);
2402        } else {
2403            aroon_osc_batch_into_slice(high, low, &sweep, Kernel::Auto, false, out)
2404                .map_err(|e| JsValue::from_str(&e.to_string()))?;
2405        }
2406
2407        Ok(rows)
2408    }
2409}