Skip to main content

vector_ta/indicators/
ultosc.rs

1#[cfg(feature = "python")]
2use crate::utilities::kernel_validation::validate_kernel;
3#[cfg(feature = "python")]
4use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
5#[cfg(feature = "python")]
6use pyo3::exceptions::PyValueError;
7#[cfg(feature = "python")]
8use pyo3::prelude::*;
9#[cfg(feature = "python")]
10use pyo3::types::PyDict;
11
12#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
13use serde::{Deserialize, Serialize};
14#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
15use wasm_bindgen::prelude::*;
16
17use crate::utilities::data_loader::{source_type, Candles};
18use crate::utilities::enums::Kernel;
19use crate::utilities::helpers::{
20    alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
21    make_uninit_matrix,
22};
23use aligned_vec::{AVec, CACHELINE_ALIGN};
24#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
25use core::arch::x86_64::*;
26#[cfg(not(target_arch = "wasm32"))]
27use rayon::prelude::*;
28use std::convert::AsRef;
29use thiserror::Error;
30
31#[cfg(all(feature = "python", feature = "cuda"))]
32mod ultosc_python_cuda_handle {
33    use crate::utilities::dlpack_cuda::export_f32_cuda_dlpack_2d;
34    use cust::context::Context;
35    use cust::memory::DeviceBuffer;
36    use pyo3::exceptions::PyValueError;
37    use pyo3::prelude::*;
38    use pyo3::types::PyDict;
39    use std::ffi::c_void;
40    use std::sync::Arc;
41
42    #[pyclass(
43        module = "ta_indicators.cuda",
44        unsendable,
45        name = "UltOscDeviceArrayF32Py"
46    )]
47    pub struct DeviceArrayF32Py {
48        pub(crate) buf: Option<DeviceBuffer<f32>>,
49        pub(crate) rows: usize,
50        pub(crate) cols: usize,
51        pub(crate) _ctx: Arc<Context>,
52        pub(crate) device_id: u32,
53    }
54
55    #[pymethods]
56    impl DeviceArrayF32Py {
57        #[getter]
58        fn __cuda_array_interface__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
59            let d = PyDict::new(py);
60            d.set_item("shape", (self.rows, self.cols))?;
61            d.set_item("typestr", "<f4")?;
62            d.set_item(
63                "strides",
64                (
65                    self.cols * std::mem::size_of::<f32>(),
66                    std::mem::size_of::<f32>(),
67                ),
68            )?;
69            let ptr = self
70                .buf
71                .as_ref()
72                .ok_or_else(|| PyValueError::new_err("buffer already exported via __dlpack__"))?
73                .as_device_ptr()
74                .as_raw() as usize;
75            d.set_item("data", (ptr, false))?;
76            d.set_item("version", 3)?;
77            Ok(d)
78        }
79
80        fn __dlpack_device__(&self) -> (i32, i32) {
81            (2, self.device_id as i32)
82        }
83
84        #[pyo3(signature = (stream=None, max_version=None, dl_device=None, copy=None))]
85        fn __dlpack__<'py>(
86            &mut self,
87            py: Python<'py>,
88            stream: Option<pyo3::PyObject>,
89            max_version: Option<pyo3::PyObject>,
90            dl_device: Option<pyo3::PyObject>,
91            copy: Option<pyo3::PyObject>,
92        ) -> PyResult<PyObject> {
93            let (exp_dev_ty, alloc_dev) = self.__dlpack_device__();
94            if let Some(dev_obj) = dl_device.as_ref() {
95                if let Ok((dev_ty, dev_id)) = dev_obj.extract::<(i32, i32)>(py) {
96                    if dev_ty != exp_dev_ty || dev_id != alloc_dev {
97                        let wants_copy = copy
98                            .as_ref()
99                            .and_then(|c| c.extract::<bool>(py).ok())
100                            .unwrap_or(false);
101                        if wants_copy {
102                            return Err(PyValueError::new_err(
103                                "device copy not implemented for __dlpack__",
104                            ));
105                        } else {
106                            return Err(PyValueError::new_err("dl_device mismatch for __dlpack__"));
107                        }
108                    }
109                }
110            }
111            let _ = stream;
112
113            let buf = self
114                .buf
115                .take()
116                .ok_or_else(|| PyValueError::new_err("__dlpack__ may only be called once"))?;
117
118            let max_version_bound = max_version.map(|obj| obj.into_bound(py));
119
120            export_f32_cuda_dlpack_2d(py, buf, self.rows, self.cols, alloc_dev, max_version_bound)
121        }
122    }
123
124    pub use DeviceArrayF32Py as UltOscDeviceArrayF32Py;
125}
126
127#[cfg(all(feature = "python", feature = "cuda"))]
128use self::ultosc_python_cuda_handle::UltOscDeviceArrayF32Py;
129
130#[derive(Debug, Clone)]
131pub enum UltOscData<'a> {
132    Candles {
133        candles: &'a Candles,
134        high_src: &'a str,
135        low_src: &'a str,
136        close_src: &'a str,
137    },
138    Slices {
139        high: &'a [f64],
140        low: &'a [f64],
141        close: &'a [f64],
142    },
143}
144
145#[derive(Debug, Clone)]
146pub struct UltOscOutput {
147    pub values: Vec<f64>,
148}
149
150#[derive(Debug, Clone, Copy)]
151#[cfg_attr(
152    all(target_arch = "wasm32", feature = "wasm"),
153    derive(Serialize, Deserialize)
154)]
155pub struct UltOscParams {
156    pub timeperiod1: Option<usize>,
157    pub timeperiod2: Option<usize>,
158    pub timeperiod3: Option<usize>,
159}
160
161impl Default for UltOscParams {
162    fn default() -> Self {
163        Self {
164            timeperiod1: Some(7),
165            timeperiod2: Some(14),
166            timeperiod3: Some(28),
167        }
168    }
169}
170
171#[derive(Debug, Clone)]
172pub struct UltOscInput<'a> {
173    pub data: UltOscData<'a>,
174    pub params: UltOscParams,
175}
176
177impl<'a> UltOscInput<'a> {
178    #[inline]
179    pub fn from_candles(
180        candles: &'a Candles,
181        high_src: &'a str,
182        low_src: &'a str,
183        close_src: &'a str,
184        params: UltOscParams,
185    ) -> Self {
186        Self {
187            data: UltOscData::Candles {
188                candles,
189                high_src,
190                low_src,
191                close_src,
192            },
193            params,
194        }
195    }
196    #[inline]
197    pub fn from_slices(
198        high: &'a [f64],
199        low: &'a [f64],
200        close: &'a [f64],
201        params: UltOscParams,
202    ) -> Self {
203        Self {
204            data: UltOscData::Slices { high, low, close },
205            params,
206        }
207    }
208    #[inline]
209    pub fn with_default_candles(candles: &'a Candles) -> Self {
210        Self {
211            data: UltOscData::Candles {
212                candles,
213                high_src: "high",
214                low_src: "low",
215                close_src: "close",
216            },
217            params: UltOscParams::default(),
218        }
219    }
220    #[inline]
221    pub fn get_timeperiod1(&self) -> usize {
222        self.params.timeperiod1.unwrap_or(7)
223    }
224    #[inline]
225    pub fn get_timeperiod2(&self) -> usize {
226        self.params.timeperiod2.unwrap_or(14)
227    }
228    #[inline]
229    pub fn get_timeperiod3(&self) -> usize {
230        self.params.timeperiod3.unwrap_or(28)
231    }
232}
233
234#[derive(Copy, Clone, Debug)]
235pub struct UltOscBuilder {
236    timeperiod1: Option<usize>,
237    timeperiod2: Option<usize>,
238    timeperiod3: Option<usize>,
239    kernel: Kernel,
240}
241
242impl Default for UltOscBuilder {
243    fn default() -> Self {
244        Self {
245            timeperiod1: None,
246            timeperiod2: None,
247            timeperiod3: None,
248            kernel: Kernel::Auto,
249        }
250    }
251}
252
253impl UltOscBuilder {
254    #[inline(always)]
255    pub fn new() -> Self {
256        Self::default()
257    }
258    #[inline(always)]
259    pub fn timeperiod1(mut self, p: usize) -> Self {
260        self.timeperiod1 = Some(p);
261        self
262    }
263    #[inline(always)]
264    pub fn timeperiod2(mut self, p: usize) -> Self {
265        self.timeperiod2 = Some(p);
266        self
267    }
268    #[inline(always)]
269    pub fn timeperiod3(mut self, p: usize) -> Self {
270        self.timeperiod3 = Some(p);
271        self
272    }
273    #[inline(always)]
274    pub fn kernel(mut self, k: Kernel) -> Self {
275        self.kernel = k;
276        self
277    }
278    #[inline(always)]
279    pub fn apply(self, candles: &Candles) -> Result<UltOscOutput, UltOscError> {
280        let params = UltOscParams {
281            timeperiod1: self.timeperiod1,
282            timeperiod2: self.timeperiod2,
283            timeperiod3: self.timeperiod3,
284        };
285        let input = UltOscInput::with_default_candles(candles);
286        ultosc_with_kernel(&UltOscInput { params, ..input }, self.kernel)
287    }
288    #[inline(always)]
289    pub fn apply_slices(
290        self,
291        high: &[f64],
292        low: &[f64],
293        close: &[f64],
294    ) -> Result<UltOscOutput, UltOscError> {
295        let params = UltOscParams {
296            timeperiod1: self.timeperiod1,
297            timeperiod2: self.timeperiod2,
298            timeperiod3: self.timeperiod3,
299        };
300        let input = UltOscInput::from_slices(high, low, close, params);
301        ultosc_with_kernel(&input, self.kernel)
302    }
303}
304
305#[derive(Debug, Error)]
306pub enum UltOscError {
307    #[error("ultosc: Input data slice is empty.")]
308    EmptyInputData,
309    #[error("ultosc: All values are NaN (or their preceding data is NaN).")]
310    AllValuesNaN,
311    #[error("ultosc: Invalid period: period = {period}, data length = {data_len}")]
312    InvalidPeriod { period: usize, data_len: usize },
313    #[error("ultosc: Not enough valid data: needed = {needed}, valid = {valid}")]
314    NotEnoughValidData { needed: usize, valid: usize },
315    #[error("ultosc: Output length mismatch: expected {expected}, got {got}")]
316    OutputLengthMismatch { expected: usize, got: usize },
317    #[error("ultosc: Inconsistent input lengths")]
318    InconsistentLengths,
319    #[error("ultosc: Invalid range: start={start}, end={end}, step={step}")]
320    InvalidRange {
321        start: String,
322        end: String,
323        step: String,
324    },
325    #[error("ultosc: Invalid kernel for batch: {0:?}")]
326    InvalidKernelForBatch(Kernel),
327}
328
329#[inline]
330fn ultosc_prepare<'a>(
331    input: &'a UltOscInput,
332    kernel: Kernel,
333) -> Result<
334    (
335        (&'a [f64], &'a [f64], &'a [f64]),
336        usize,
337        usize,
338        usize,
339        usize,
340        usize,
341        Kernel,
342    ),
343    UltOscError,
344> {
345    let (high, low, close) = match &input.data {
346        UltOscData::Candles {
347            candles,
348            high_src,
349            low_src,
350            close_src,
351        } => {
352            let high = source_type(candles, high_src);
353            let low = source_type(candles, low_src);
354            let close = source_type(candles, close_src);
355            (high, low, close)
356        }
357        UltOscData::Slices { high, low, close } => (*high, *low, *close),
358    };
359
360    let len = high.len();
361    if len == 0 || low.len() == 0 || close.len() == 0 {
362        return Err(UltOscError::EmptyInputData);
363    }
364    if low.len() != len || close.len() != len {
365        return Err(UltOscError::InconsistentLengths);
366    }
367
368    let p1 = input.get_timeperiod1();
369    let p2 = input.get_timeperiod2();
370    let p3 = input.get_timeperiod3();
371
372    if p1 == 0 || p2 == 0 || p3 == 0 || p1 > len || p2 > len || p3 > len {
373        let bad = if p1 == 0 || p1 > len {
374            p1
375        } else if p2 == 0 || p2 > len {
376            p2
377        } else {
378            p3
379        };
380        return Err(UltOscError::InvalidPeriod {
381            period: bad,
382            data_len: len,
383        });
384    }
385
386    let largest_period = p1.max(p2.max(p3));
387    let first_valid = match (1..len).find(|&i| {
388        !high[i - 1].is_nan()
389            && !low[i - 1].is_nan()
390            && !close[i - 1].is_nan()
391            && !high[i].is_nan()
392            && !low[i].is_nan()
393            && !close[i].is_nan()
394    }) {
395        Some(i) => i,
396        None => return Err(UltOscError::AllValuesNaN),
397    };
398
399    let start_idx = first_valid + (largest_period - 1);
400    if start_idx >= len {
401        return Err(UltOscError::NotEnoughValidData {
402            needed: largest_period,
403            valid: len.saturating_sub(first_valid),
404        });
405    }
406
407    let chosen = match kernel {
408        Kernel::Auto => Kernel::Scalar,
409        other => other,
410    };
411
412    Ok((
413        (high, low, close),
414        p1,
415        p2,
416        p3,
417        first_valid,
418        start_idx,
419        chosen,
420    ))
421}
422
423#[inline]
424pub fn ultosc(input: &UltOscInput) -> Result<UltOscOutput, UltOscError> {
425    ultosc_with_kernel(input, Kernel::Auto)
426}
427
428pub fn ultosc_with_kernel(
429    input: &UltOscInput,
430    kernel: Kernel,
431) -> Result<UltOscOutput, UltOscError> {
432    let ((high, low, close), p1, p2, p3, first_valid, start_idx, chosen) =
433        ultosc_prepare(input, kernel)?;
434    let len = high.len();
435    let mut out = alloc_with_nan_prefix(len, start_idx);
436
437    ultosc_compute_into(high, low, close, p1, p2, p3, first_valid, chosen, &mut out);
438
439    Ok(UltOscOutput { values: out })
440}
441
442#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
443pub fn ultosc_into(input: &UltOscInput, out: &mut [f64]) -> Result<(), UltOscError> {
444    let ((high, low, close), p1, p2, p3, first_valid, start_idx, chosen) =
445        ultosc_prepare(input, Kernel::Auto)?;
446
447    if out.len() != high.len() {
448        return Err(UltOscError::OutputLengthMismatch {
449            expected: high.len(),
450            got: out.len(),
451        });
452    }
453
454    let warm = start_idx.min(out.len());
455    let qnan = f64::from_bits(0x7ff8_0000_0000_0000);
456    for v in &mut out[..warm] {
457        *v = qnan;
458    }
459
460    ultosc_compute_into(high, low, close, p1, p2, p3, first_valid, chosen, out);
461
462    Ok(())
463}
464
465#[inline]
466fn ultosc_compute_into(
467    high: &[f64],
468    low: &[f64],
469    close: &[f64],
470    p1: usize,
471    p2: usize,
472    p3: usize,
473    first_valid: usize,
474    chosen: Kernel,
475    dst: &mut [f64],
476) {
477    unsafe {
478        match chosen {
479            Kernel::Scalar | Kernel::ScalarBatch => {
480                ultosc_scalar(high, low, close, p1, p2, p3, first_valid, dst)
481            }
482            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
483            Kernel::Avx2 | Kernel::Avx2Batch => {
484                ultosc_avx2(high, low, close, p1, p2, p3, first_valid, dst)
485            }
486            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
487            Kernel::Avx512 | Kernel::Avx512Batch => {
488                ultosc_avx512(high, low, close, p1, p2, p3, first_valid, dst)
489            }
490            _ => unreachable!(),
491        }
492    }
493}
494
495#[inline(always)]
496pub unsafe fn ultosc_scalar(
497    high: &[f64],
498    low: &[f64],
499    close: &[f64],
500    p1: usize,
501    p2: usize,
502    p3: usize,
503    first_valid: usize,
504    out: &mut [f64],
505) {
506    let len = high.len();
507    let max_period = p1.max(p2).max(p3);
508
509    const STACK_THRESHOLD: usize = 256;
510
511    if max_period <= STACK_THRESHOLD {
512        let mut cmtl_stack = [0.0_f64; STACK_THRESHOLD];
513        let mut tr_stack = [0.0_f64; STACK_THRESHOLD];
514        let cmtl_buf = &mut cmtl_stack[..max_period];
515        let tr_buf = &mut tr_stack[..max_period];
516
517        ultosc_scalar_impl(
518            high,
519            low,
520            close,
521            p1,
522            p2,
523            p3,
524            first_valid,
525            out,
526            cmtl_buf,
527            tr_buf,
528        );
529    } else {
530        let mut cmtl_vec = vec![0.0; max_period];
531        let mut tr_vec = vec![0.0; max_period];
532
533        ultosc_scalar_impl(
534            high,
535            low,
536            close,
537            p1,
538            p2,
539            p3,
540            first_valid,
541            out,
542            &mut cmtl_vec,
543            &mut tr_vec,
544        );
545    }
546}
547
548#[inline(always)]
549unsafe fn ultosc_scalar_impl(
550    high: &[f64],
551    low: &[f64],
552    close: &[f64],
553    p1: usize,
554    p2: usize,
555    p3: usize,
556    first_valid: usize,
557    out: &mut [f64],
558    cmtl_buf: &mut [f64],
559    tr_buf: &mut [f64],
560) {
561    let len = high.len();
562    if len == 0 {
563        return;
564    }
565
566    let max_p = p1.max(p2).max(p3);
567    debug_assert!(max_p > 0 && max_p <= len);
568
569    let start_idx = first_valid + max_p - 1;
570
571    let inv7_100: f64 = 100.0f64 / 7.0f64;
572    let w1: f64 = inv7_100 * 4.0f64;
573    let w2: f64 = inv7_100 * 2.0f64;
574    let w3: f64 = inv7_100 * 1.0f64;
575
576    let mut sum1_a = 0.0f64;
577    let mut sum1_b = 0.0f64;
578    let mut sum2_a = 0.0f64;
579    let mut sum2_b = 0.0f64;
580    let mut sum3_a = 0.0f64;
581    let mut sum3_b = 0.0f64;
582
583    let mut buf_idx: usize = 0;
584    let mut count: usize = 0;
585
586    let mut i = first_valid;
587    while i < len {
588        let hi = *high.get_unchecked(i);
589        let lo = *low.get_unchecked(i);
590        let ci = *close.get_unchecked(i);
591        let prev_c = *close.get_unchecked(i - 1);
592
593        let valid = !(hi.is_nan() | lo.is_nan() | ci.is_nan() | prev_c.is_nan());
594
595        let (c_new, t_new) = if valid {
596            let tl = if lo < prev_c { lo } else { prev_c };
597
598            let th = if hi > prev_c { hi } else { prev_c };
599            let tr = th - tl;
600            (ci - tl, tr)
601        } else {
602            (0.0, 0.0)
603        };
604
605        if count >= p1 {
606            let mut old_idx1 = buf_idx + max_p - p1;
607            if old_idx1 >= max_p {
608                old_idx1 -= max_p;
609            }
610            sum1_a -= *cmtl_buf.get_unchecked(old_idx1);
611            sum1_b -= *tr_buf.get_unchecked(old_idx1);
612        }
613        if count >= p2 {
614            let mut old_idx2 = buf_idx + max_p - p2;
615            if old_idx2 >= max_p {
616                old_idx2 -= max_p;
617            }
618            sum2_a -= *cmtl_buf.get_unchecked(old_idx2);
619            sum2_b -= *tr_buf.get_unchecked(old_idx2);
620        }
621        if count >= p3 {
622            let mut old_idx3 = buf_idx + max_p - p3;
623            if old_idx3 >= max_p {
624                old_idx3 -= max_p;
625            }
626            sum3_a -= *cmtl_buf.get_unchecked(old_idx3);
627            sum3_b -= *tr_buf.get_unchecked(old_idx3);
628        }
629
630        *cmtl_buf.get_unchecked_mut(buf_idx) = c_new;
631        *tr_buf.get_unchecked_mut(buf_idx) = t_new;
632
633        if valid {
634            sum1_a += c_new;
635            sum1_b += t_new;
636            sum2_a += c_new;
637            sum2_b += t_new;
638            sum3_a += c_new;
639            sum3_b += t_new;
640        }
641
642        count += 1;
643        if i >= start_idx {
644            let t1 = if sum1_b != 0.0 {
645                sum1_a * sum1_b.recip()
646            } else {
647                0.0
648            };
649            let t2 = if sum2_b != 0.0 {
650                sum2_a * sum2_b.recip()
651            } else {
652                0.0
653            };
654            let t3 = if sum3_b != 0.0 {
655                sum3_a * sum3_b.recip()
656            } else {
657                0.0
658            };
659
660            let acc = f64::mul_add(w2, t2, w3 * t3);
661            *out.get_unchecked_mut(i) = f64::mul_add(w1, t1, acc);
662        }
663
664        buf_idx += 1;
665        if buf_idx == max_p {
666            buf_idx = 0;
667        }
668
669        i += 1;
670    }
671}
672
673#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
674#[inline]
675pub unsafe fn ultosc_avx2(
676    high: &[f64],
677    low: &[f64],
678    close: &[f64],
679    p1: usize,
680    p2: usize,
681    p3: usize,
682    first_valid: usize,
683    out: &mut [f64],
684) {
685    ultosc_scalar(high, low, close, p1, p2, p3, first_valid, out)
686}
687
688#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
689#[inline]
690pub unsafe fn ultosc_avx512(
691    high: &[f64],
692    low: &[f64],
693    close: &[f64],
694    p1: usize,
695    p2: usize,
696    p3: usize,
697    first_valid: usize,
698    out: &mut [f64],
699) {
700    if p1.max(p2).max(p3) <= 32 {
701        ultosc_avx512_short(high, low, close, p1, p2, p3, first_valid, out)
702    } else {
703        ultosc_avx512_long(high, low, close, p1, p2, p3, first_valid, out)
704    }
705}
706#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
707#[inline]
708pub unsafe fn ultosc_avx512_short(
709    high: &[f64],
710    low: &[f64],
711    close: &[f64],
712    p1: usize,
713    p2: usize,
714    p3: usize,
715    first_valid: usize,
716    out: &mut [f64],
717) {
718    ultosc_scalar(high, low, close, p1, p2, p3, first_valid, out)
719}
720#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
721#[inline]
722pub unsafe fn ultosc_avx512_long(
723    high: &[f64],
724    low: &[f64],
725    close: &[f64],
726    p1: usize,
727    p2: usize,
728    p3: usize,
729    first_valid: usize,
730    out: &mut [f64],
731) {
732    ultosc_scalar(high, low, close, p1, p2, p3, first_valid, out)
733}
734
735#[inline(always)]
736pub fn ultosc_row_scalar(
737    high: &[f64],
738    low: &[f64],
739    close: &[f64],
740    p1: usize,
741    p2: usize,
742    p3: usize,
743    first_valid: usize,
744    out: &mut [f64],
745) {
746    unsafe { ultosc_scalar(high, low, close, p1, p2, p3, first_valid, out) }
747}
748#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
749#[inline(always)]
750pub fn ultosc_row_avx2(
751    high: &[f64],
752    low: &[f64],
753    close: &[f64],
754    p1: usize,
755    p2: usize,
756    p3: usize,
757    first_valid: usize,
758    out: &mut [f64],
759) {
760    unsafe { ultosc_avx2(high, low, close, p1, p2, p3, first_valid, out) }
761}
762#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
763#[inline(always)]
764pub fn ultosc_row_avx512(
765    high: &[f64],
766    low: &[f64],
767    close: &[f64],
768    p1: usize,
769    p2: usize,
770    p3: usize,
771    first_valid: usize,
772    out: &mut [f64],
773) {
774    unsafe { ultosc_avx512(high, low, close, p1, p2, p3, first_valid, out) }
775}
776#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
777#[inline(always)]
778pub fn ultosc_row_avx512_short(
779    high: &[f64],
780    low: &[f64],
781    close: &[f64],
782    p1: usize,
783    p2: usize,
784    p3: usize,
785    first_valid: usize,
786    out: &mut [f64],
787) {
788    unsafe { ultosc_avx512_short(high, low, close, p1, p2, p3, first_valid, out) }
789}
790#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
791#[inline(always)]
792pub fn ultosc_row_avx512_long(
793    high: &[f64],
794    low: &[f64],
795    close: &[f64],
796    p1: usize,
797    p2: usize,
798    p3: usize,
799    first_valid: usize,
800    out: &mut [f64],
801) {
802    unsafe { ultosc_avx512_long(high, low, close, p1, p2, p3, first_valid, out) }
803}
804
805#[derive(Clone, Debug)]
806pub struct UltOscBatchRange {
807    pub timeperiod1: (usize, usize, usize),
808    pub timeperiod2: (usize, usize, usize),
809    pub timeperiod3: (usize, usize, usize),
810}
811
812impl Default for UltOscBatchRange {
813    fn default() -> Self {
814        Self {
815            timeperiod1: (7, 7, 0),
816            timeperiod2: (14, 14, 0),
817            timeperiod3: (28, 277, 1),
818        }
819    }
820}
821
822#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
823#[derive(Serialize, Deserialize)]
824pub struct UltOscBatchConfig {
825    pub timeperiod1_range: (usize, usize, usize),
826    pub timeperiod2_range: (usize, usize, usize),
827    pub timeperiod3_range: (usize, usize, usize),
828}
829
830#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
831#[derive(Serialize, Deserialize)]
832pub struct UltOscBatchJsOutput {
833    pub values: Vec<f64>,
834    pub combos: Vec<UltOscParams>,
835    pub rows: usize,
836    pub cols: usize,
837}
838
839#[derive(Clone, Debug)]
840pub struct UltOscBatchBuilder {
841    kernel: Kernel,
842}
843
844impl Default for UltOscBatchBuilder {
845    fn default() -> Self {
846        Self {
847            kernel: Kernel::Auto,
848        }
849    }
850}
851
852impl UltOscBatchBuilder {
853    pub fn new() -> Self {
854        Self::default()
855    }
856
857    pub fn kernel(mut self, k: Kernel) -> Self {
858        self.kernel = k;
859        self
860    }
861
862    pub fn apply_slice(
863        self,
864        high: &[f64],
865        low: &[f64],
866        close: &[f64],
867        sweep: &UltOscBatchRange,
868    ) -> Result<UltOscBatchOutput, UltOscError> {
869        ultosc_batch_with_kernel(high, low, close, sweep, self.kernel)
870    }
871}
872
873#[derive(Clone, Debug)]
874pub struct UltOscBatchOutput {
875    pub values: Vec<f64>,
876    pub combos: Vec<UltOscParams>,
877    pub rows: usize,
878    pub cols: usize,
879}
880
881impl UltOscBatchOutput {
882    pub fn row_for_params(&self, p: &UltOscParams) -> Option<usize> {
883        self.combos.iter().position(|c| {
884            c.timeperiod1.unwrap_or(7) == p.timeperiod1.unwrap_or(7)
885                && c.timeperiod2.unwrap_or(14) == p.timeperiod2.unwrap_or(14)
886                && c.timeperiod3.unwrap_or(28) == p.timeperiod3.unwrap_or(28)
887        })
888    }
889
890    pub fn values_for(&self, p: &UltOscParams) -> Option<&[f64]> {
891        self.row_for_params(p).map(|row| {
892            let start = row * self.cols;
893            &self.values[start..start + self.cols]
894        })
895    }
896}
897
898#[inline(always)]
899fn expand_grid(r: &UltOscBatchRange) -> Result<Vec<UltOscParams>, UltOscError> {
900    fn axis_usize((start, end, step): (usize, usize, usize)) -> Result<Vec<usize>, UltOscError> {
901        if step == 0 || start == end {
902            return Ok(vec![start]);
903        }
904        let s = step.max(1);
905        let mut v = Vec::new();
906        if start <= end {
907            let mut cur = start;
908            loop {
909                v.push(cur);
910                if cur == end {
911                    break;
912                }
913                let next = cur
914                    .checked_add(s)
915                    .ok_or_else(|| UltOscError::InvalidRange {
916                        start: start.to_string(),
917                        end: end.to_string(),
918                        step: step.to_string(),
919                    })?;
920                if next <= cur || next > end {
921                    break;
922                }
923                cur = next;
924            }
925        } else {
926            let mut cur = start;
927            loop {
928                v.push(cur);
929                if cur == end {
930                    break;
931                }
932                let next = match cur.checked_sub(s) {
933                    Some(n) => n,
934                    None => break,
935                };
936                if next < end {
937                    break;
938                }
939                cur = next;
940            }
941        }
942        if v.is_empty() {
943            return Err(UltOscError::InvalidRange {
944                start: start.to_string(),
945                end: end.to_string(),
946                step: step.to_string(),
947            });
948        }
949        Ok(v)
950    }
951
952    let timeperiod1s = axis_usize(r.timeperiod1)?;
953    let timeperiod2s = axis_usize(r.timeperiod2)?;
954    let timeperiod3s = axis_usize(r.timeperiod3)?;
955
956    let cap = timeperiod1s
957        .len()
958        .checked_mul(timeperiod2s.len())
959        .and_then(|v| v.checked_mul(timeperiod3s.len()))
960        .ok_or_else(|| UltOscError::InvalidRange {
961            start: r.timeperiod1.0.to_string(),
962            end: r.timeperiod3.1.to_string(),
963            step: r.timeperiod1.2.to_string(),
964        })?;
965
966    let mut out = Vec::with_capacity(cap);
967    for &tp1 in &timeperiod1s {
968        for &tp2 in &timeperiod2s {
969            for &tp3 in &timeperiod3s {
970                out.push(UltOscParams {
971                    timeperiod1: Some(tp1),
972                    timeperiod2: Some(tp2),
973                    timeperiod3: Some(tp3),
974                });
975            }
976        }
977    }
978    Ok(out)
979}
980
981pub fn ultosc_batch_with_kernel(
982    high: &[f64],
983    low: &[f64],
984    close: &[f64],
985    sweep: &UltOscBatchRange,
986    k: Kernel,
987) -> Result<UltOscBatchOutput, UltOscError> {
988    let kernel = match k {
989        Kernel::Auto => detect_best_batch_kernel(),
990        other if other.is_batch() => other,
991        _ => return Err(UltOscError::InvalidKernelForBatch(k)),
992    };
993
994    let simd = match kernel {
995        Kernel::Avx512Batch => Kernel::Avx512,
996        Kernel::Avx2Batch => Kernel::Avx2,
997        Kernel::ScalarBatch => Kernel::Scalar,
998        _ => unreachable!(),
999    };
1000
1001    ultosc_batch_inner(high, low, close, sweep, simd, true)
1002}
1003
1004#[inline(always)]
1005fn ultosc_batch_inner(
1006    high: &[f64],
1007    low: &[f64],
1008    close: &[f64],
1009    sweep: &UltOscBatchRange,
1010    kern: Kernel,
1011    parallel: bool,
1012) -> Result<UltOscBatchOutput, UltOscError> {
1013    let combos = expand_grid(sweep)?;
1014    let cols = high.len();
1015    let rows = combos.len();
1016
1017    let expected = rows
1018        .checked_mul(cols)
1019        .ok_or_else(|| UltOscError::InvalidRange {
1020            start: rows.to_string(),
1021            end: cols.to_string(),
1022            step: "rows*cols".to_string(),
1023        })?;
1024
1025    if cols == 0 {
1026        return Err(UltOscError::EmptyInputData);
1027    }
1028    if low.len() != cols || close.len() != cols {
1029        return Err(UltOscError::InconsistentLengths);
1030    }
1031
1032    let mut buf_mu = make_uninit_matrix(rows, cols);
1033    if buf_mu.len() != expected {
1034        return Err(UltOscError::OutputLengthMismatch {
1035            expected,
1036            got: buf_mu.len(),
1037        });
1038    }
1039
1040    let mut buf_guard = core::mem::ManuallyDrop::new(buf_mu);
1041    let out: &mut [f64] = unsafe {
1042        core::slice::from_raw_parts_mut(buf_guard.as_mut_ptr() as *mut f64, buf_guard.len())
1043    };
1044
1045    ultosc_batch_inner_into(high, low, close, sweep, kern, parallel, out)?;
1046
1047    let values = unsafe {
1048        Vec::from_raw_parts(
1049            buf_guard.as_mut_ptr() as *mut f64,
1050            buf_guard.len(),
1051            buf_guard.capacity(),
1052        )
1053    };
1054
1055    Ok(UltOscBatchOutput {
1056        values,
1057        combos,
1058        rows,
1059        cols,
1060    })
1061}
1062
1063#[inline(always)]
1064pub fn ultosc_batch_inner_into(
1065    high: &[f64],
1066    low: &[f64],
1067    close: &[f64],
1068    sweep: &UltOscBatchRange,
1069    simd: Kernel,
1070    parallel: bool,
1071    out: &mut [f64],
1072) -> Result<Vec<UltOscParams>, UltOscError> {
1073    let combos = expand_grid(sweep)?;
1074
1075    let _ = simd;
1076
1077    let len = high.len();
1078    if len == 0 || low.is_empty() || close.is_empty() {
1079        return Err(UltOscError::EmptyInputData);
1080    }
1081    if low.len() != len || close.len() != len {
1082        return Err(UltOscError::InconsistentLengths);
1083    }
1084
1085    let first_valid_idx = (1..len)
1086        .find(|&i| {
1087            !high[i - 1].is_nan()
1088                && !low[i - 1].is_nan()
1089                && !close[i - 1].is_nan()
1090                && !high[i].is_nan()
1091                && !low[i].is_nan()
1092                && !close[i].is_nan()
1093        })
1094        .ok_or(UltOscError::AllValuesNaN)?;
1095
1096    let rows = combos.len();
1097    let cols = len;
1098
1099    let mut warm = Vec::with_capacity(rows);
1100    let mut max_p = 0usize;
1101    for c in &combos {
1102        let p1 = c.timeperiod1.unwrap_or(7);
1103        let p2 = c.timeperiod2.unwrap_or(14);
1104        let p3 = c.timeperiod3.unwrap_or(28);
1105        if p1 == 0 || p2 == 0 || p3 == 0 || p1 > len || p2 > len || p3 > len {
1106            let bad = if p1 == 0 || p1 > len {
1107                p1
1108            } else if p2 == 0 || p2 > len {
1109                p2
1110            } else {
1111                p3
1112            };
1113            return Err(UltOscError::InvalidPeriod {
1114                period: bad,
1115                data_len: len,
1116            });
1117        }
1118        let pmax = p1.max(p2).max(p3);
1119        if pmax > max_p {
1120            max_p = pmax;
1121        }
1122        warm.push(first_valid_idx + pmax - 1);
1123    }
1124
1125    if len - first_valid_idx < max_p {
1126        return Err(UltOscError::NotEnoughValidData {
1127            needed: max_p,
1128            valid: len - first_valid_idx,
1129        });
1130    }
1131
1132    let expected = rows
1133        .checked_mul(cols)
1134        .ok_or_else(|| UltOscError::InvalidRange {
1135            start: rows.to_string(),
1136            end: cols.to_string(),
1137            step: "rows*cols".to_string(),
1138        })?;
1139    if out.len() != expected {
1140        return Err(UltOscError::OutputLengthMismatch {
1141            expected,
1142            got: out.len(),
1143        });
1144    }
1145
1146    let out_uninit = unsafe {
1147        core::slice::from_raw_parts_mut(
1148            out.as_mut_ptr() as *mut core::mem::MaybeUninit<f64>,
1149            out.len(),
1150        )
1151    };
1152    init_matrix_prefixes(out_uninit, cols, &warm);
1153
1154    let mut pcmtl = vec![0.0f64; len + 1];
1155    let mut ptr = vec![0.0f64; len + 1];
1156    for i in 0..len {
1157        let (mut add_c, mut add_t) = (0.0f64, 0.0f64);
1158        if i >= first_valid_idx {
1159            let hi = high[i];
1160            let lo = low[i];
1161            let ci = close[i];
1162            let pc = close[i - 1];
1163            if hi.is_finite() && lo.is_finite() && ci.is_finite() && pc.is_finite() {
1164                let tl = if lo < pc { lo } else { pc };
1165                let mut trv = hi - lo;
1166                let d1 = (hi - pc).abs();
1167                if d1 > trv {
1168                    trv = d1;
1169                }
1170                let d2 = (lo - pc).abs();
1171                if d2 > trv {
1172                    trv = d2;
1173                }
1174                add_c = ci - tl;
1175                add_t = trv;
1176            }
1177        }
1178        pcmtl[i + 1] = pcmtl[i] + add_c;
1179        ptr[i + 1] = ptr[i] + add_t;
1180    }
1181
1182    let do_row = |row: usize, row_out: &mut [f64]| {
1183        let p1 = combos[row].timeperiod1.unwrap();
1184        let p2 = combos[row].timeperiod2.unwrap();
1185        let p3 = combos[row].timeperiod3.unwrap();
1186        let start = first_valid_idx + p1.max(p2).max(p3) - 1;
1187
1188        let inv7_100: f64 = 100.0f64 / 7.0f64;
1189        let w1: f64 = inv7_100 * 4.0f64;
1190        let w2: f64 = inv7_100 * 2.0f64;
1191        let w3: f64 = inv7_100 * 1.0f64;
1192
1193        for i in start..len {
1194            let s1a = pcmtl[i + 1] - pcmtl[i + 1 - p1];
1195            let s1b = ptr[i + 1] - ptr[i + 1 - p1];
1196            let s2a = pcmtl[i + 1] - pcmtl[i + 1 - p2];
1197            let s2b = ptr[i + 1] - ptr[i + 1 - p2];
1198            let s3a = pcmtl[i + 1] - pcmtl[i + 1 - p3];
1199            let s3b = ptr[i + 1] - ptr[i + 1 - p3];
1200
1201            let t1 = if s1b != 0.0 { s1a * s1b.recip() } else { 0.0 };
1202            let t2 = if s2b != 0.0 { s2a * s2b.recip() } else { 0.0 };
1203            let t3 = if s3b != 0.0 { s3a * s3b.recip() } else { 0.0 };
1204
1205            let acc = f64::mul_add(w2, t2, w3 * t3);
1206            row_out[i] = f64::mul_add(w1, t1, acc);
1207        }
1208    };
1209
1210    if parallel {
1211        #[cfg(not(target_arch = "wasm32"))]
1212        {
1213            use rayon::prelude::*;
1214            out_uninit
1215                .par_chunks_mut(cols)
1216                .enumerate()
1217                .for_each(|(row, row_mu)| {
1218                    let row_out = unsafe {
1219                        core::slice::from_raw_parts_mut(
1220                            row_mu.as_mut_ptr() as *mut f64,
1221                            row_mu.len(),
1222                        )
1223                    };
1224                    do_row(row, row_out)
1225                });
1226        }
1227        #[cfg(target_arch = "wasm32")]
1228        {
1229            out_uninit
1230                .chunks_mut(cols)
1231                .enumerate()
1232                .for_each(|(row, row_mu)| {
1233                    let row_out = unsafe {
1234                        core::slice::from_raw_parts_mut(
1235                            row_mu.as_mut_ptr() as *mut f64,
1236                            row_mu.len(),
1237                        )
1238                    };
1239                    do_row(row, row_out)
1240                });
1241        }
1242    } else {
1243        out_uninit
1244            .chunks_mut(cols)
1245            .enumerate()
1246            .for_each(|(row, row_mu)| {
1247                let row_out = unsafe {
1248                    core::slice::from_raw_parts_mut(row_mu.as_mut_ptr() as *mut f64, row_mu.len())
1249                };
1250                do_row(row, row_out)
1251            });
1252    }
1253
1254    Ok(combos)
1255}
1256
1257#[cfg(test)]
1258mod tests {
1259    use super::*;
1260    use crate::skip_if_unsupported;
1261    use crate::utilities::data_loader::read_candles_from_csv;
1262    #[cfg(feature = "proptest")]
1263    use proptest::prelude::*;
1264
1265    fn check_ultosc_partial_params(
1266        test_name: &str,
1267        kernel: Kernel,
1268    ) -> Result<(), Box<dyn std::error::Error>> {
1269        skip_if_unsupported!(kernel, test_name);
1270        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1271        let candles = read_candles_from_csv(file_path)?;
1272        let params = UltOscParams {
1273            timeperiod1: None,
1274            timeperiod2: None,
1275            timeperiod3: None,
1276        };
1277        let input = UltOscInput::from_candles(&candles, "high", "low", "close", params);
1278        let output = ultosc_with_kernel(&input, kernel)?;
1279        assert_eq!(output.values.len(), candles.close.len());
1280        Ok(())
1281    }
1282
1283    fn check_ultosc_accuracy(
1284        test_name: &str,
1285        kernel: Kernel,
1286    ) -> Result<(), Box<dyn std::error::Error>> {
1287        skip_if_unsupported!(kernel, test_name);
1288        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1289        let candles = read_candles_from_csv(file_path)?;
1290        let params = UltOscParams {
1291            timeperiod1: Some(7),
1292            timeperiod2: Some(14),
1293            timeperiod3: Some(28),
1294        };
1295        let input = UltOscInput::from_candles(&candles, "high", "low", "close", params);
1296        let result = ultosc_with_kernel(&input, kernel)?;
1297        let expected_last_five = [
1298            41.25546890298435,
1299            40.83865967175865,
1300            48.910324164909625,
1301            45.43113094857947,
1302            42.163165136766295,
1303        ];
1304        assert!(result.values.len() >= 5);
1305        let start_idx = result.values.len() - 5;
1306        for (i, &val) in result.values[start_idx..].iter().enumerate() {
1307            let exp = expected_last_five[i];
1308            assert!(
1309                (val - exp).abs() < 1e-8,
1310                "[{}] ULTOSC mismatch at last five index {}: expected {}, got {}",
1311                test_name,
1312                i,
1313                exp,
1314                val
1315            );
1316        }
1317        Ok(())
1318    }
1319
1320    fn check_ultosc_default_candles(
1321        test_name: &str,
1322        kernel: Kernel,
1323    ) -> Result<(), Box<dyn std::error::Error>> {
1324        skip_if_unsupported!(kernel, test_name);
1325        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1326        let candles = read_candles_from_csv(file_path)?;
1327        let input = UltOscInput::with_default_candles(&candles);
1328        let result = ultosc_with_kernel(&input, kernel)?;
1329        assert_eq!(result.values.len(), candles.close.len());
1330        Ok(())
1331    }
1332
1333    fn check_ultosc_zero_periods(
1334        test_name: &str,
1335        kernel: Kernel,
1336    ) -> Result<(), Box<dyn std::error::Error>> {
1337        skip_if_unsupported!(kernel, test_name);
1338        let input_high = [1.0, 2.0, 3.0];
1339        let input_low = [0.5, 1.5, 2.5];
1340        let input_close = [0.8, 1.8, 2.8];
1341        let params = UltOscParams {
1342            timeperiod1: Some(0),
1343            timeperiod2: Some(14),
1344            timeperiod3: Some(28),
1345        };
1346        let input = UltOscInput::from_slices(&input_high, &input_low, &input_close, params);
1347        let result = ultosc_with_kernel(&input, kernel);
1348        assert!(
1349            result.is_err(),
1350            "[{}] Expected error for zero period",
1351            test_name
1352        );
1353        Ok(())
1354    }
1355
1356    fn check_ultosc_period_exceeds_data_length(
1357        test_name: &str,
1358        kernel: Kernel,
1359    ) -> Result<(), Box<dyn std::error::Error>> {
1360        skip_if_unsupported!(kernel, test_name);
1361        let input_high = [1.0, 2.0, 3.0];
1362        let input_low = [0.5, 1.5, 2.5];
1363        let input_close = [0.8, 1.8, 2.8];
1364        let params = UltOscParams {
1365            timeperiod1: Some(7),
1366            timeperiod2: Some(14),
1367            timeperiod3: Some(28),
1368        };
1369        let input = UltOscInput::from_slices(&input_high, &input_low, &input_close, params);
1370        let result = ultosc_with_kernel(&input, kernel);
1371        assert!(
1372            result.is_err(),
1373            "[{}] Expected error for period exceeding data length",
1374            test_name
1375        );
1376        Ok(())
1377    }
1378
1379    #[cfg(debug_assertions)]
1380    fn check_ultosc_no_poison(
1381        test_name: &str,
1382        kernel: Kernel,
1383    ) -> Result<(), Box<dyn std::error::Error>> {
1384        skip_if_unsupported!(kernel, test_name);
1385
1386        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1387        let candles = read_candles_from_csv(file_path)?;
1388
1389        let test_params = vec![
1390            UltOscParams::default(),
1391            UltOscParams {
1392                timeperiod1: Some(1),
1393                timeperiod2: Some(2),
1394                timeperiod3: Some(3),
1395            },
1396            UltOscParams {
1397                timeperiod1: Some(2),
1398                timeperiod2: Some(4),
1399                timeperiod3: Some(8),
1400            },
1401            UltOscParams {
1402                timeperiod1: Some(5),
1403                timeperiod2: Some(10),
1404                timeperiod3: Some(20),
1405            },
1406            UltOscParams {
1407                timeperiod1: Some(7),
1408                timeperiod2: Some(14),
1409                timeperiod3: Some(28),
1410            },
1411            UltOscParams {
1412                timeperiod1: Some(10),
1413                timeperiod2: Some(20),
1414                timeperiod3: Some(40),
1415            },
1416            UltOscParams {
1417                timeperiod1: Some(14),
1418                timeperiod2: Some(28),
1419                timeperiod3: Some(56),
1420            },
1421            UltOscParams {
1422                timeperiod1: Some(20),
1423                timeperiod2: Some(40),
1424                timeperiod3: Some(80),
1425            },
1426            UltOscParams {
1427                timeperiod1: Some(5),
1428                timeperiod2: Some(6),
1429                timeperiod3: Some(7),
1430            },
1431            UltOscParams {
1432                timeperiod1: Some(3),
1433                timeperiod2: Some(10),
1434                timeperiod3: Some(50),
1435            },
1436            UltOscParams {
1437                timeperiod1: Some(14),
1438                timeperiod2: Some(14),
1439                timeperiod3: Some(14),
1440            },
1441            UltOscParams {
1442                timeperiod1: Some(28),
1443                timeperiod2: Some(14),
1444                timeperiod3: Some(7),
1445            },
1446        ];
1447
1448        for (param_idx, params) in test_params.iter().enumerate() {
1449            let input = UltOscInput::from_candles(&candles, "high", "low", "close", params.clone());
1450            let output = ultosc_with_kernel(&input, kernel)?;
1451
1452            for (i, &val) in output.values.iter().enumerate() {
1453                if val.is_nan() {
1454                    continue;
1455                }
1456
1457                let bits = val.to_bits();
1458
1459                if bits == 0x11111111_11111111 {
1460                    panic!(
1461                        "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at index {} \
1462						 with params: timeperiod1={}, timeperiod2={}, timeperiod3={} (param set {})",
1463                        test_name,
1464                        val,
1465                        bits,
1466                        i,
1467                        params.timeperiod1.unwrap_or(7),
1468                        params.timeperiod2.unwrap_or(14),
1469                        params.timeperiod3.unwrap_or(28),
1470                        param_idx
1471                    );
1472                }
1473
1474                if bits == 0x22222222_22222222 {
1475                    panic!(
1476                        "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at index {} \
1477						 with params: timeperiod1={}, timeperiod2={}, timeperiod3={} (param set {})",
1478                        test_name,
1479                        val,
1480                        bits,
1481                        i,
1482                        params.timeperiod1.unwrap_or(7),
1483                        params.timeperiod2.unwrap_or(14),
1484                        params.timeperiod3.unwrap_or(28),
1485                        param_idx
1486                    );
1487                }
1488
1489                if bits == 0x33333333_33333333 {
1490                    panic!(
1491                        "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at index {} \
1492						 with params: timeperiod1={}, timeperiod2={}, timeperiod3={} (param set {})",
1493                        test_name,
1494                        val,
1495                        bits,
1496                        i,
1497                        params.timeperiod1.unwrap_or(7),
1498                        params.timeperiod2.unwrap_or(14),
1499                        params.timeperiod3.unwrap_or(28),
1500                        param_idx
1501                    );
1502                }
1503            }
1504        }
1505
1506        Ok(())
1507    }
1508
1509    #[cfg(not(debug_assertions))]
1510    fn check_ultosc_no_poison(
1511        _test_name: &str,
1512        _kernel: Kernel,
1513    ) -> Result<(), Box<dyn std::error::Error>> {
1514        Ok(())
1515    }
1516
1517    #[cfg(feature = "proptest")]
1518    #[allow(clippy::float_cmp)]
1519    fn check_ultosc_property(
1520        test_name: &str,
1521        kernel: Kernel,
1522    ) -> Result<(), Box<dyn std::error::Error>> {
1523        use proptest::prelude::*;
1524        skip_if_unsupported!(kernel, test_name);
1525
1526        let strat = (1usize..=50, 1usize..=50, 1usize..=50).prop_flat_map(|(p1, p2, p3)| {
1527            let max_period = p1.max(p2).max(p3);
1528            (
1529                prop::collection::vec(
1530                    (0.1f64..10000.0f64).prop_filter("finite", |x| x.is_finite()),
1531                    (max_period + 1)..400,
1532                ),
1533                Just((p1, p2, p3)),
1534            )
1535        });
1536
1537        proptest::test_runner::TestRunner::default().run(
1538            &strat,
1539            |(base_prices, (p1, p2, p3))| {
1540                let mut high = Vec::with_capacity(base_prices.len());
1541                let mut low = Vec::with_capacity(base_prices.len());
1542                let mut close = Vec::with_capacity(base_prices.len());
1543
1544                let mut seed = p1 + p2 * 7 + p3 * 13;
1545                for &price in &base_prices {
1546                    seed = (seed * 1103515245 + 12345) % (1 << 31);
1547                    let spread_pct = 0.01 + (seed as f64 / (1u64 << 31) as f64) * 0.09;
1548                    let spread = price * spread_pct;
1549
1550                    seed = (seed * 1103515245 + 12345) % (1 << 31);
1551                    let close_position = seed as f64 / (1u64 << 31) as f64;
1552
1553                    let h = price + spread * 0.5;
1554                    let l = price - spread * 0.5;
1555                    let c = l + (h - l) * close_position;
1556
1557                    high.push(h);
1558                    low.push(l);
1559                    close.push(c);
1560                }
1561
1562                let params = UltOscParams {
1563                    timeperiod1: Some(p1),
1564                    timeperiod2: Some(p2),
1565                    timeperiod3: Some(p3),
1566                };
1567                let input = UltOscInput::from_slices(&high, &low, &close, params.clone());
1568
1569                let result = ultosc_with_kernel(&input, kernel).unwrap();
1570                let out = result.values;
1571
1572                let ref_result = ultosc_with_kernel(&input, Kernel::Scalar).unwrap();
1573                let ref_out = ref_result.values;
1574
1575                let max_period = p1.max(p2).max(p3);
1576
1577                let warmup = max_period;
1578
1579                for i in 0..warmup.min(out.len()) {
1580                    prop_assert!(
1581                        out[i].is_nan(),
1582                        "[{}] Expected NaN during warmup at index {}, got {}",
1583                        test_name,
1584                        i,
1585                        out[i]
1586                    );
1587                }
1588
1589                for (i, (&y, &r)) in out.iter().zip(ref_out.iter()).enumerate() {
1590                    if !y.is_finite() || !r.is_finite() {
1591                        prop_assert!(
1592                            y.to_bits() == r.to_bits(),
1593                            "[{}] NaN/inf mismatch at index {}: {} vs {}",
1594                            test_name,
1595                            i,
1596                            y,
1597                            r
1598                        );
1599                    } else {
1600                        let ulp_diff = y.to_bits().abs_diff(r.to_bits());
1601                        prop_assert!(
1602                            (y - r).abs() <= 1e-9 || ulp_diff <= 4,
1603                            "[{}] Value mismatch at index {}: {} vs {} (ULP diff: {})",
1604                            test_name,
1605                            i,
1606                            y,
1607                            r,
1608                            ulp_diff
1609                        );
1610                    }
1611                }
1612
1613                for (i, &val) in out.iter().enumerate() {
1614                    if !val.is_nan() {
1615                        prop_assert!(
1616                            val >= 0.0 && val <= 100.0,
1617                            "[{}] ULTOSC value {} at index {} is out of bounds [0, 100]",
1618                            test_name,
1619                            val,
1620                            i
1621                        );
1622                    }
1623                }
1624
1625                if high.windows(2).all(|w| (w[0] - w[1]).abs() < 1e-10)
1626                    && low.windows(2).all(|w| (w[0] - w[1]).abs() < 1e-10)
1627                    && close.windows(2).all(|w| (w[0] - w[1]).abs() < 1e-10)
1628                {
1629                    let stability_check_start = (warmup + p3.max(p2).max(p1)).min(out.len());
1630                    if stability_check_start < out.len() - 2 {
1631                        let stable_region = &out[stability_check_start..];
1632                        let first_valid = stable_region.iter().position(|&v| !v.is_nan());
1633
1634                        if let Some(idx) = first_valid {
1635                            let expected_stable = stable_region[idx];
1636
1637                            for (i, &val) in stable_region.iter().skip(idx + 1).enumerate() {
1638                                if !val.is_nan() {
1639                                    prop_assert!(
1640										(val - expected_stable).abs() < 1e-8,
1641										"[{}] Expected stable value {} for constant prices at index {}, got {}",
1642										test_name, expected_stable, stability_check_start + idx + 1 + i, val
1643									);
1644                                }
1645                            }
1646                        }
1647                    }
1648                }
1649
1650                let zero_range_high = vec![100.0; base_prices.len()];
1651                let zero_range_low = zero_range_high.clone();
1652                let zero_range_close = zero_range_high.clone();
1653
1654                let zero_input = UltOscInput::from_slices(
1655                    &zero_range_high,
1656                    &zero_range_low,
1657                    &zero_range_close,
1658                    params.clone(),
1659                );
1660                if let Ok(zero_result) = ultosc_with_kernel(&zero_input, kernel) {
1661                    for (i, &val) in zero_result.values.iter().enumerate().skip(warmup) {
1662                        if !val.is_nan() {
1663                            prop_assert!(
1664                                val.abs() < 1e-8,
1665                                "[{}] Expected 0 for zero range at index {}, got {}",
1666                                test_name,
1667                                i,
1668                                val
1669                            );
1670                        }
1671                    }
1672                }
1673
1674                if out.len() > warmup {
1675                    for i in warmup..out.len().min(warmup + 5) {
1676                        if !out[i].is_nan() {
1677                            prop_assert!(
1678                                out[i] >= 0.0 && out[i] <= 100.0,
1679                                "[{}] ULTOSC at {} should be in [0,100], got {}",
1680                                test_name,
1681                                i,
1682                                out[i]
1683                            );
1684                        }
1685                    }
1686                }
1687
1688                let reordered_params = UltOscParams {
1689                    timeperiod1: Some(p3),
1690                    timeperiod2: Some(p1),
1691                    timeperiod3: Some(p2),
1692                };
1693                let reordered_input =
1694                    UltOscInput::from_slices(&high, &low, &close, reordered_params);
1695
1696                prop_assert!(
1697                    ultosc_with_kernel(&reordered_input, kernel).is_ok(),
1698                    "[{}] ULTOSC should work with any period ordering",
1699                    test_name
1700                );
1701
1702                Ok(())
1703            },
1704        )?;
1705
1706        Ok(())
1707    }
1708
1709    macro_rules! generate_all_ultosc_tests {
1710        ($($test_fn:ident),*) => {
1711            paste::paste! {
1712                $(
1713                    #[test]
1714                    fn [<$test_fn _scalar_f64>]() {
1715                        let _ = $test_fn(stringify!([<$test_fn _scalar_f64>]), Kernel::Scalar);
1716                    }
1717                )*
1718                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1719                $(
1720                    #[test]
1721                    fn [<$test_fn _avx2_f64>]() {
1722                        let _ = $test_fn(stringify!([<$test_fn _avx2_f64>]), Kernel::Avx2);
1723                    }
1724                    #[test]
1725                    fn [<$test_fn _avx512_f64>]() {
1726                        let _ = $test_fn(stringify!([<$test_fn _avx512_f64>]), Kernel::Avx512);
1727                    }
1728                )*
1729            }
1730        }
1731    }
1732
1733    generate_all_ultosc_tests!(
1734        check_ultosc_partial_params,
1735        check_ultosc_accuracy,
1736        check_ultosc_default_candles,
1737        check_ultosc_zero_periods,
1738        check_ultosc_period_exceeds_data_length,
1739        check_ultosc_no_poison
1740    );
1741
1742    #[cfg(feature = "proptest")]
1743    generate_all_ultosc_tests!(check_ultosc_property);
1744    fn check_ultosc_batch_default(
1745        test_name: &str,
1746        kernel: Kernel,
1747    ) -> Result<(), Box<dyn std::error::Error>> {
1748        skip_if_unsupported!(kernel, test_name);
1749
1750        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1751        let candles = read_candles_from_csv(file_path)?;
1752
1753        let sweep = UltOscBatchRange {
1754            timeperiod1: (5, 9, 2),
1755            timeperiod2: (12, 16, 2),
1756            timeperiod3: (26, 30, 2),
1757        };
1758
1759        let batch_builder = UltOscBatchBuilder::new().kernel(kernel);
1760        let output =
1761            batch_builder.apply_slice(&candles.high, &candles.low, &candles.close, &sweep)?;
1762
1763        assert_eq!(output.rows, 3 * 3 * 3);
1764        assert_eq!(output.cols, candles.close.len());
1765        assert_eq!(output.values.len(), output.rows * output.cols);
1766        assert_eq!(output.combos.len(), output.rows);
1767
1768        let single_params = UltOscParams {
1769            timeperiod1: Some(7),
1770            timeperiod2: Some(14),
1771            timeperiod3: Some(28),
1772        };
1773        let single_input =
1774            UltOscInput::from_slices(&candles.high, &candles.low, &candles.close, single_params);
1775        let single_result = ultosc_with_kernel(&single_input, kernel)?;
1776
1777        if let Some(row_idx) = output.row_for_params(&single_params) {
1778            let batch_row = output.values_for(&single_params).unwrap();
1779
1780            let start = batch_row.len().saturating_sub(5);
1781            for i in 0..5 {
1782                let diff = (batch_row[start + i] - single_result.values[start + i]).abs();
1783                assert!(
1784                    diff < 1e-10,
1785                    "[{}] Batch vs single mismatch at idx {}: got {}, expected {}",
1786                    test_name,
1787                    i,
1788                    batch_row[start + i],
1789                    single_result.values[start + i]
1790                );
1791            }
1792        } else {
1793            panic!("[{}] Could not find row for params 7,14,28", test_name);
1794        }
1795
1796        Ok(())
1797    }
1798
1799    #[cfg(debug_assertions)]
1800    fn check_batch_no_poison(
1801        test_name: &str,
1802        kernel: Kernel,
1803    ) -> Result<(), Box<dyn std::error::Error>> {
1804        skip_if_unsupported!(kernel, test_name);
1805
1806        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1807        let candles = read_candles_from_csv(file_path)?;
1808
1809        let test_configs = vec![
1810            (2, 8, 2, 4, 16, 4, 8, 32, 8),
1811            (5, 7, 1, 10, 14, 2, 20, 28, 4),
1812            (7, 7, 0, 14, 14, 0, 14, 42, 7),
1813            (1, 5, 1, 10, 10, 0, 20, 20, 0),
1814            (10, 20, 5, 20, 40, 10, 40, 80, 20),
1815            (3, 9, 3, 6, 18, 6, 12, 36, 12),
1816            (5, 10, 1, 10, 20, 2, 20, 40, 4),
1817        ];
1818
1819        for (
1820            cfg_idx,
1821            &(
1822                tp1_start,
1823                tp1_end,
1824                tp1_step,
1825                tp2_start,
1826                tp2_end,
1827                tp2_step,
1828                tp3_start,
1829                tp3_end,
1830                tp3_step,
1831            ),
1832        ) in test_configs.iter().enumerate()
1833        {
1834            let sweep = UltOscBatchRange {
1835                timeperiod1: (tp1_start, tp1_end, tp1_step),
1836                timeperiod2: (tp2_start, tp2_end, tp2_step),
1837                timeperiod3: (tp3_start, tp3_end, tp3_step),
1838            };
1839
1840            let batch_builder = UltOscBatchBuilder::new().kernel(kernel);
1841            let output =
1842                batch_builder.apply_slice(&candles.high, &candles.low, &candles.close, &sweep)?;
1843
1844            for (idx, &val) in output.values.iter().enumerate() {
1845                if val.is_nan() {
1846                    continue;
1847                }
1848
1849                let bits = val.to_bits();
1850                let row = idx / output.cols;
1851                let col = idx % output.cols;
1852                let combo = &output.combos[row];
1853
1854                if bits == 0x11111111_11111111 {
1855                    panic!(
1856                        "[{}] Config {}: Found alloc_with_nan_prefix poison value {} (0x{:016X}) \
1857						at row {} col {} (flat index {}) with params: timeperiod1={}, timeperiod2={}, timeperiod3={}",
1858                        test_name,
1859                        cfg_idx,
1860                        val,
1861                        bits,
1862                        row,
1863                        col,
1864                        idx,
1865                        combo.timeperiod1.unwrap_or(7),
1866                        combo.timeperiod2.unwrap_or(14),
1867                        combo.timeperiod3.unwrap_or(28)
1868                    );
1869                }
1870
1871                if bits == 0x22222222_22222222 {
1872                    panic!(
1873                        "[{}] Config {}: Found init_matrix_prefixes poison value {} (0x{:016X}) \
1874						at row {} col {} (flat index {}) with params: timeperiod1={}, timeperiod2={}, timeperiod3={}",
1875                        test_name,
1876                        cfg_idx,
1877                        val,
1878                        bits,
1879                        row,
1880                        col,
1881                        idx,
1882                        combo.timeperiod1.unwrap_or(7),
1883                        combo.timeperiod2.unwrap_or(14),
1884                        combo.timeperiod3.unwrap_or(28)
1885                    );
1886                }
1887
1888                if bits == 0x33333333_33333333 {
1889                    panic!(
1890                        "[{}] Config {}: Found make_uninit_matrix poison value {} (0x{:016X}) \
1891						at row {} col {} (flat index {}) with params: timeperiod1={}, timeperiod2={}, timeperiod3={}",
1892                        test_name,
1893                        cfg_idx,
1894                        val,
1895                        bits,
1896                        row,
1897                        col,
1898                        idx,
1899                        combo.timeperiod1.unwrap_or(7),
1900                        combo.timeperiod2.unwrap_or(14),
1901                        combo.timeperiod3.unwrap_or(28)
1902                    );
1903                }
1904            }
1905        }
1906
1907        Ok(())
1908    }
1909
1910    #[cfg(not(debug_assertions))]
1911    fn check_batch_no_poison(
1912        _test_name: &str,
1913        _kernel: Kernel,
1914    ) -> Result<(), Box<dyn std::error::Error>> {
1915        Ok(())
1916    }
1917
1918    macro_rules! gen_batch_tests {
1919        ($fn_name:ident) => {
1920            paste::paste! {
1921                #[test]
1922                fn [<$fn_name _scalar>]() {
1923                    let _ = $fn_name(stringify!([<$fn_name _scalar>]), Kernel::ScalarBatch);
1924                }
1925                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1926                #[test]
1927                fn [<$fn_name _avx2>]() {
1928                    let _ = $fn_name(stringify!([<$fn_name _avx2>]), Kernel::Avx2Batch);
1929                }
1930                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1931                #[test]
1932                fn [<$fn_name _avx512>]() {
1933                    let _ = $fn_name(stringify!([<$fn_name _avx512>]), Kernel::Avx512Batch);
1934                }
1935            }
1936        };
1937    }
1938
1939    gen_batch_tests!(check_ultosc_batch_default);
1940    gen_batch_tests!(check_batch_no_poison);
1941
1942    #[test]
1943    fn test_ultosc_into_matches_api() -> Result<(), Box<dyn std::error::Error>> {
1944        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1945        let candles = read_candles_from_csv(file_path)?;
1946        let input =
1947            UltOscInput::from_candles(&candles, "high", "low", "close", UltOscParams::default());
1948
1949        let baseline = ultosc(&input)?;
1950
1951        let mut out = vec![0.0; candles.close.len()];
1952        #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
1953        {
1954            ultosc_into(&input, &mut out)?;
1955        }
1956        #[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1957        {
1958            ultosc_into_slice(&mut out, &input, Kernel::Auto)?;
1959        }
1960
1961        assert_eq!(baseline.values.len(), out.len());
1962
1963        fn eq_or_both_nan(a: f64, b: f64) -> bool {
1964            (a.is_nan() && b.is_nan()) || (a == b) || ((a - b).abs() <= 1e-12)
1965        }
1966
1967        for i in 0..out.len() {
1968            assert!(
1969                eq_or_both_nan(baseline.values[i], out[i]),
1970                "Mismatch at index {}: baseline={} out={}",
1971                i,
1972                baseline.values[i],
1973                out[i]
1974            );
1975        }
1976
1977        Ok(())
1978    }
1979}
1980
1981#[inline]
1982pub fn ultosc_into_slice(
1983    dst: &mut [f64],
1984    input: &UltOscInput,
1985    kern: Kernel,
1986) -> Result<(), UltOscError> {
1987    let ((high, low, close), p1, p2, p3, first_valid, start_idx, chosen) =
1988        ultosc_prepare(input, kern)?;
1989
1990    if dst.len() != high.len() {
1991        return Err(UltOscError::OutputLengthMismatch {
1992            expected: high.len(),
1993            got: dst.len(),
1994        });
1995    }
1996
1997    ultosc_compute_into(high, low, close, p1, p2, p3, first_valid, chosen, dst);
1998
1999    let qnan = f64::from_bits(0x7ff8_0000_0000_0000);
2000    dst[..start_idx].fill(qnan);
2001
2002    Ok(())
2003}
2004
2005#[derive(Debug, Clone)]
2006pub struct UltOscStream {
2007    params: UltOscParams,
2008
2009    cmtl_buf: Vec<f64>,
2010    tr_buf: Vec<f64>,
2011
2012    sum1_a: f64,
2013    sum1_b: f64,
2014    sum2_a: f64,
2015    sum2_b: f64,
2016    sum3_a: f64,
2017    sum3_b: f64,
2018
2019    buffer_idx: usize,
2020    count: usize,
2021
2022    max_period: usize,
2023    p1: usize,
2024    p2: usize,
2025    p3: usize,
2026
2027    w1: f64,
2028    w2: f64,
2029    w3: f64,
2030
2031    prev_close: Option<f64>,
2032}
2033
2034impl UltOscStream {
2035    #[inline]
2036    pub fn try_new(params: UltOscParams) -> Result<Self, UltOscError> {
2037        let p1 = params.timeperiod1.unwrap_or(7);
2038        let p2 = params.timeperiod2.unwrap_or(14);
2039        let p3 = params.timeperiod3.unwrap_or(28);
2040
2041        if p1 == 0 || p2 == 0 || p3 == 0 {
2042            let bad = if p1 == 0 {
2043                p1
2044            } else if p2 == 0 {
2045                p2
2046            } else {
2047                p3
2048            };
2049            return Err(UltOscError::InvalidPeriod {
2050                period: bad,
2051                data_len: 0,
2052            });
2053        }
2054
2055        let max_period = p1.max(p2).max(p3);
2056
2057        const INV7_100: f64 = 100.0 / 7.0;
2058        let w1 = INV7_100 * 4.0;
2059        let w2 = INV7_100 * 2.0;
2060        let w3 = INV7_100 * 1.0;
2061
2062        Ok(Self {
2063            params,
2064            cmtl_buf: vec![0.0; max_period],
2065            tr_buf: vec![0.0; max_period],
2066
2067            sum1_a: 0.0,
2068            sum1_b: 0.0,
2069            sum2_a: 0.0,
2070            sum2_b: 0.0,
2071            sum3_a: 0.0,
2072            sum3_b: 0.0,
2073
2074            buffer_idx: 0,
2075            count: 0,
2076
2077            max_period,
2078            p1,
2079            p2,
2080            p3,
2081
2082            w1,
2083            w2,
2084            w3,
2085            prev_close: None,
2086        })
2087    }
2088
2089    #[inline(always)]
2090    fn idx_minus(&self, k: usize) -> usize {
2091        let mut j = self.buffer_idx + self.max_period - k;
2092        if j >= self.max_period {
2093            j -= self.max_period;
2094        }
2095        j
2096    }
2097
2098    #[inline]
2099    pub fn update(&mut self, high: f64, low: f64, close: f64) -> Option<f64> {
2100        let prev_close = match self.prev_close {
2101            Some(pc) => pc,
2102            None => {
2103                self.prev_close = Some(close);
2104                return None;
2105            }
2106        };
2107
2108        let valid = !(high.is_nan() | low.is_nan() | close.is_nan() | prev_close.is_nan());
2109
2110        let (c_new, t_new) = if valid {
2111            let true_low = if low < prev_close { low } else { prev_close };
2112
2113            let base = high - low;
2114            let d1 = (high - prev_close).abs();
2115            let d2 = (low - prev_close).abs();
2116            let tr = if d1 > base {
2117                if d2 > d1 {
2118                    d2
2119                } else {
2120                    d1
2121                }
2122            } else {
2123                if d2 > base {
2124                    d2
2125                } else {
2126                    base
2127                }
2128            };
2129
2130            (close - true_low, tr)
2131        } else {
2132            (0.0, 0.0)
2133        };
2134
2135        if self.count >= self.p1 {
2136            let j = self.idx_minus(self.p1);
2137            self.sum1_a -= self.cmtl_buf[j];
2138            self.sum1_b -= self.tr_buf[j];
2139        }
2140        if self.count >= self.p2 {
2141            let j = self.idx_minus(self.p2);
2142            self.sum2_a -= self.cmtl_buf[j];
2143            self.sum2_b -= self.tr_buf[j];
2144        }
2145        if self.count >= self.p3 {
2146            let j = self.idx_minus(self.p3);
2147            self.sum3_a -= self.cmtl_buf[j];
2148            self.sum3_b -= self.tr_buf[j];
2149        }
2150
2151        self.cmtl_buf[self.buffer_idx] = c_new;
2152        self.tr_buf[self.buffer_idx] = t_new;
2153
2154        self.sum1_a += c_new;
2155        self.sum1_b += t_new;
2156        self.sum2_a += c_new;
2157        self.sum2_b += t_new;
2158        self.sum3_a += c_new;
2159        self.sum3_b += t_new;
2160
2161        self.buffer_idx += 1;
2162        if self.buffer_idx == self.max_period {
2163            self.buffer_idx = 0;
2164        }
2165        self.count += 1;
2166
2167        self.prev_close = Some(close);
2168
2169        if self.count < self.max_period {
2170            return None;
2171        }
2172
2173        let t1 = if self.sum1_b != 0.0 {
2174            self.sum1_a * self.sum1_b.recip()
2175        } else {
2176            0.0
2177        };
2178        let t2 = if self.sum2_b != 0.0 {
2179            self.sum2_a * self.sum2_b.recip()
2180        } else {
2181            0.0
2182        };
2183        let t3 = if self.sum3_b != 0.0 {
2184            self.sum3_a * self.sum3_b.recip()
2185        } else {
2186            0.0
2187        };
2188
2189        let acc = f64::mul_add(self.w2, t2, self.w3 * t3);
2190        Some(f64::mul_add(self.w1, t1, acc))
2191    }
2192}
2193
2194#[cfg(feature = "python")]
2195#[pyfunction(name = "ultosc")]
2196#[pyo3(signature = (high, low, close, timeperiod1=None, timeperiod2=None, timeperiod3=None, kernel=None))]
2197pub fn ultosc_py<'py>(
2198    py: Python<'py>,
2199    high: PyReadonlyArray1<'py, f64>,
2200    low: PyReadonlyArray1<'py, f64>,
2201    close: PyReadonlyArray1<'py, f64>,
2202    timeperiod1: Option<usize>,
2203    timeperiod2: Option<usize>,
2204    timeperiod3: Option<usize>,
2205    kernel: Option<&str>,
2206) -> PyResult<Bound<'py, PyArray1<f64>>> {
2207    let high_slice = high.as_slice()?;
2208    let low_slice = low.as_slice()?;
2209    let close_slice = close.as_slice()?;
2210    let kern = validate_kernel(kernel, false)?;
2211
2212    let params = UltOscParams {
2213        timeperiod1,
2214        timeperiod2,
2215        timeperiod3,
2216    };
2217    let input = UltOscInput::from_slices(high_slice, low_slice, close_slice, params);
2218
2219    let result_vec: Vec<f64> = py
2220        .allow_threads(|| ultosc_with_kernel(&input, kern).map(|o| o.values))
2221        .map_err(|e| PyValueError::new_err(e.to_string()))?;
2222
2223    Ok(result_vec.into_pyarray(py))
2224}
2225
2226#[cfg(feature = "python")]
2227#[pyfunction(name = "ultosc_batch")]
2228#[pyo3(signature = (high, low, close, timeperiod1_range, timeperiod2_range, timeperiod3_range, kernel=None))]
2229pub fn ultosc_batch_py<'py>(
2230    py: Python<'py>,
2231    high: PyReadonlyArray1<'py, f64>,
2232    low: PyReadonlyArray1<'py, f64>,
2233    close: PyReadonlyArray1<'py, f64>,
2234    timeperiod1_range: (usize, usize, usize),
2235    timeperiod2_range: (usize, usize, usize),
2236    timeperiod3_range: (usize, usize, usize),
2237    kernel: Option<&str>,
2238) -> PyResult<Bound<'py, PyDict>> {
2239    let high_slice = high.as_slice()?;
2240    let low_slice = low.as_slice()?;
2241    let close_slice = close.as_slice()?;
2242    let kern = validate_kernel(kernel, true)?;
2243
2244    let sweep = UltOscBatchRange {
2245        timeperiod1: timeperiod1_range,
2246        timeperiod2: timeperiod2_range,
2247        timeperiod3: timeperiod3_range,
2248    };
2249
2250    let combos = expand_grid(&sweep).map_err(|e| PyValueError::new_err(e.to_string()))?;
2251    let rows = combos.len();
2252    let cols = high_slice.len();
2253
2254    let total_elems = rows
2255        .checked_mul(cols)
2256        .ok_or_else(|| PyValueError::new_err("rows * cols overflow in ultosc_batch_py"))?;
2257
2258    let out_arr = unsafe { PyArray1::<f64>::new(py, [total_elems], false) };
2259    let slice_out = unsafe { out_arr.as_slice_mut()? };
2260
2261    let combos = py
2262        .allow_threads(|| {
2263            let kernel = match kern {
2264                Kernel::Auto => detect_best_batch_kernel(),
2265                k => k,
2266            };
2267            let simd = match kernel {
2268                Kernel::Avx512Batch => Kernel::Avx512,
2269                Kernel::Avx2Batch => Kernel::Avx2,
2270                Kernel::ScalarBatch => Kernel::Scalar,
2271                _ => kernel,
2272            };
2273            ultosc_batch_inner_into(
2274                high_slice,
2275                low_slice,
2276                close_slice,
2277                &sweep,
2278                simd,
2279                true,
2280                slice_out,
2281            )
2282        })
2283        .map_err(|e| PyValueError::new_err(e.to_string()))?;
2284
2285    let dict = PyDict::new(py);
2286    dict.set_item("values", out_arr.reshape((rows, cols))?)?;
2287    dict.set_item(
2288        "timeperiod1",
2289        combos
2290            .iter()
2291            .map(|p| p.timeperiod1.unwrap() as u64)
2292            .collect::<Vec<_>>()
2293            .into_pyarray(py),
2294    )?;
2295    dict.set_item(
2296        "timeperiod2",
2297        combos
2298            .iter()
2299            .map(|p| p.timeperiod2.unwrap() as u64)
2300            .collect::<Vec<_>>()
2301            .into_pyarray(py),
2302    )?;
2303    dict.set_item(
2304        "timeperiod3",
2305        combos
2306            .iter()
2307            .map(|p| p.timeperiod3.unwrap() as u64)
2308            .collect::<Vec<_>>()
2309            .into_pyarray(py),
2310    )?;
2311
2312    Ok(dict)
2313}
2314
2315#[cfg(all(feature = "python", feature = "cuda"))]
2316#[pyfunction(name = "ultosc_cuda_batch_dev")]
2317#[pyo3(signature = (high, low, close, timeperiod1_range, timeperiod2_range, timeperiod3_range, device_id=0))]
2318pub fn ultosc_cuda_batch_dev_py(
2319    py: Python<'_>,
2320    high: PyReadonlyArray1<'_, f32>,
2321    low: PyReadonlyArray1<'_, f32>,
2322    close: PyReadonlyArray1<'_, f32>,
2323    timeperiod1_range: (usize, usize, usize),
2324    timeperiod2_range: (usize, usize, usize),
2325    timeperiod3_range: (usize, usize, usize),
2326    device_id: usize,
2327) -> PyResult<UltOscDeviceArrayF32Py> {
2328    use crate::cuda::cuda_available;
2329    use crate::cuda::oscillators::CudaUltosc;
2330
2331    if !cuda_available() {
2332        return Err(PyValueError::new_err("CUDA not available"));
2333    }
2334    let high_slice = high.as_slice()?;
2335    let low_slice = low.as_slice()?;
2336    let close_slice = close.as_slice()?;
2337
2338    let sweep = UltOscBatchRange {
2339        timeperiod1: timeperiod1_range,
2340        timeperiod2: timeperiod2_range,
2341        timeperiod3: timeperiod3_range,
2342    };
2343
2344    let (buf, rows, cols, ctx_arc, dev_id) = py.allow_threads(|| -> PyResult<_> {
2345        let cuda = CudaUltosc::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
2346        let dev = cuda
2347            .ultosc_batch_dev(high_slice, low_slice, close_slice, &sweep)
2348            .map_err(|e| PyValueError::new_err(e.to_string()))?;
2349        let buf = dev.buf;
2350        let rows = dev.rows;
2351        let cols = dev.cols;
2352        let ctx = cuda.context_arc();
2353        let dev_id = cuda.device_id();
2354        Ok((buf, rows, cols, ctx, dev_id))
2355    })?;
2356    Ok(UltOscDeviceArrayF32Py {
2357        buf: Some(buf),
2358        rows,
2359        cols,
2360        _ctx: ctx_arc,
2361        device_id: dev_id as u32,
2362    })
2363}
2364
2365#[cfg(all(feature = "python", feature = "cuda"))]
2366#[pyfunction(name = "ultosc_cuda_many_series_one_param_dev")]
2367#[pyo3(signature = (high_tm, low_tm, close_tm, cols, rows, timeperiod1, timeperiod2, timeperiod3, device_id=0))]
2368pub fn ultosc_cuda_many_series_one_param_dev_py(
2369    py: Python<'_>,
2370    high_tm: PyReadonlyArray1<'_, f32>,
2371    low_tm: PyReadonlyArray1<'_, f32>,
2372    close_tm: PyReadonlyArray1<'_, f32>,
2373    cols: usize,
2374    rows: usize,
2375    timeperiod1: usize,
2376    timeperiod2: usize,
2377    timeperiod3: usize,
2378    device_id: usize,
2379) -> PyResult<UltOscDeviceArrayF32Py> {
2380    use crate::cuda::cuda_available;
2381    use crate::cuda::oscillators::CudaUltosc;
2382    if !cuda_available() {
2383        return Err(PyValueError::new_err("CUDA not available"));
2384    }
2385    let h = high_tm.as_slice()?;
2386    let l = low_tm.as_slice()?;
2387    let c = close_tm.as_slice()?;
2388    let (buf, rows_out, cols_out, ctx_arc, dev_id) = py.allow_threads(|| -> PyResult<_> {
2389        let cuda = CudaUltosc::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
2390        let dev = cuda
2391            .ultosc_many_series_one_param_time_major_dev(
2392                h,
2393                l,
2394                c,
2395                cols,
2396                rows,
2397                timeperiod1,
2398                timeperiod2,
2399                timeperiod3,
2400            )
2401            .map_err(|e| PyValueError::new_err(e.to_string()))?;
2402        let buf = dev.buf;
2403        let rows_out = dev.rows;
2404        let cols_out = dev.cols;
2405        let ctx = cuda.context_arc();
2406        let dev_id = cuda.device_id();
2407        Ok((buf, rows_out, cols_out, ctx, dev_id))
2408    })?;
2409    Ok(UltOscDeviceArrayF32Py {
2410        buf: Some(buf),
2411        rows: rows_out,
2412        cols: cols_out,
2413        _ctx: ctx_arc,
2414        device_id: dev_id as u32,
2415    })
2416}
2417
2418#[cfg(feature = "python")]
2419#[pyclass(name = "UltOscStream")]
2420pub struct UltOscStreamPy {
2421    stream: UltOscStream,
2422}
2423
2424#[cfg(feature = "python")]
2425#[pymethods]
2426impl UltOscStreamPy {
2427    #[new]
2428    #[pyo3(signature = (timeperiod1=None, timeperiod2=None, timeperiod3=None))]
2429    fn new(
2430        timeperiod1: Option<usize>,
2431        timeperiod2: Option<usize>,
2432        timeperiod3: Option<usize>,
2433    ) -> PyResult<Self> {
2434        let params = UltOscParams {
2435            timeperiod1,
2436            timeperiod2,
2437            timeperiod3,
2438        };
2439        let stream =
2440            UltOscStream::try_new(params).map_err(|e| PyValueError::new_err(e.to_string()))?;
2441        Ok(UltOscStreamPy { stream })
2442    }
2443
2444    fn update(&mut self, high: f64, low: f64, close: f64) -> Option<f64> {
2445        self.stream.update(high, low, close)
2446    }
2447}
2448
2449#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2450#[wasm_bindgen]
2451pub fn ultosc_js(
2452    high: &[f64],
2453    low: &[f64],
2454    close: &[f64],
2455    timeperiod1: usize,
2456    timeperiod2: usize,
2457    timeperiod3: usize,
2458) -> Result<Vec<f64>, JsValue> {
2459    if high.is_empty() || low.is_empty() || close.is_empty() {
2460        return Err(JsValue::from_str("Empty data"));
2461    }
2462    if timeperiod1 == 0 || timeperiod2 == 0 || timeperiod3 == 0 {
2463        return Err(JsValue::from_str("Invalid period"));
2464    }
2465    let len = high.len();
2466    if timeperiod1 > len || timeperiod2 > len || timeperiod3 > len {
2467        return Err(JsValue::from_str("Period exceeds data length"));
2468    }
2469
2470    let params = UltOscParams {
2471        timeperiod1: Some(timeperiod1),
2472        timeperiod2: Some(timeperiod2),
2473        timeperiod3: Some(timeperiod3),
2474    };
2475    let input = UltOscInput::from_slices(high, low, close, params);
2476
2477    let mut output = vec![0.0; len];
2478    ultosc_into_slice(&mut output, &input, Kernel::Auto)
2479        .map_err(|e| JsValue::from_str(&e.to_string()))?;
2480
2481    Ok(output)
2482}
2483
2484#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2485#[wasm_bindgen]
2486pub fn ultosc_into(
2487    high_ptr: *const f64,
2488    low_ptr: *const f64,
2489    close_ptr: *const f64,
2490    out_ptr: *mut f64,
2491    len: usize,
2492    timeperiod1: usize,
2493    timeperiod2: usize,
2494    timeperiod3: usize,
2495) -> Result<(), JsValue> {
2496    if high_ptr.is_null() || low_ptr.is_null() || close_ptr.is_null() || out_ptr.is_null() {
2497        return Err(JsValue::from_str("null pointer passed to ultosc_into"));
2498    }
2499
2500    unsafe {
2501        let high = std::slice::from_raw_parts(high_ptr, len);
2502        let low = std::slice::from_raw_parts(low_ptr, len);
2503        let close = std::slice::from_raw_parts(close_ptr, len);
2504
2505        let params = UltOscParams {
2506            timeperiod1: Some(timeperiod1),
2507            timeperiod2: Some(timeperiod2),
2508            timeperiod3: Some(timeperiod3),
2509        };
2510        let input = UltOscInput::from_slices(high, low, close, params);
2511
2512        if high_ptr == out_ptr as *const f64
2513            || low_ptr == out_ptr as *const f64
2514            || close_ptr == out_ptr as *const f64
2515        {
2516            let mut temp = vec![0.0; len];
2517            ultosc_into_slice(&mut temp, &input, Kernel::Auto)
2518                .map_err(|e| JsValue::from_str(&e.to_string()))?;
2519            let out = std::slice::from_raw_parts_mut(out_ptr, len);
2520            out.copy_from_slice(&temp);
2521        } else {
2522            let out = std::slice::from_raw_parts_mut(out_ptr, len);
2523            ultosc_into_slice(out, &input, Kernel::Auto)
2524                .map_err(|e| JsValue::from_str(&e.to_string()))?;
2525        }
2526
2527        Ok(())
2528    }
2529}
2530
2531#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2532#[wasm_bindgen]
2533pub fn ultosc_alloc(len: usize) -> *mut f64 {
2534    let mut vec = Vec::<f64>::with_capacity(len);
2535    let ptr = vec.as_mut_ptr();
2536    std::mem::forget(vec);
2537    ptr
2538}
2539
2540#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2541#[wasm_bindgen]
2542pub fn ultosc_free(ptr: *mut f64, len: usize) {
2543    if !ptr.is_null() {
2544        unsafe {
2545            let _ = Vec::from_raw_parts(ptr, len, len);
2546        }
2547    }
2548}
2549
2550#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2551#[wasm_bindgen(js_name = ultosc_batch)]
2552pub fn ultosc_batch_js(
2553    high: &[f64],
2554    low: &[f64],
2555    close: &[f64],
2556    config: JsValue,
2557) -> Result<JsValue, JsValue> {
2558    let config: UltOscBatchConfig = serde_wasm_bindgen::from_value(config)
2559        .map_err(|e| JsValue::from_str(&format!("Invalid config: {}", e)))?;
2560
2561    let sweep = UltOscBatchRange {
2562        timeperiod1: config.timeperiod1_range,
2563        timeperiod2: config.timeperiod2_range,
2564        timeperiod3: config.timeperiod3_range,
2565    };
2566
2567    let batch_output = ultosc_batch_with_kernel(high, low, close, &sweep, Kernel::Auto)
2568        .map_err(|e| JsValue::from_str(&e.to_string()))?;
2569
2570    let rows = batch_output.combos.len();
2571    let cols = high.len();
2572
2573    let result = UltOscBatchJsOutput {
2574        values: batch_output.values,
2575        combos: batch_output.combos,
2576        rows,
2577        cols,
2578    };
2579
2580    serde_wasm_bindgen::to_value(&result)
2581        .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
2582}