Skip to main content

vector_ta/indicators/
vosc.rs

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