Skip to main content

vector_ta/indicators/
fosc.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};
7use aligned_vec::{AVec, CACHELINE_ALIGN};
8#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
9use core::arch::x86_64::*;
10#[cfg(not(target_arch = "wasm32"))]
11use rayon::prelude::*;
12use std::convert::AsRef;
13use std::mem::MaybeUninit;
14use thiserror::Error;
15
16#[cfg(all(feature = "python", feature = "cuda"))]
17use crate::utilities::dlpack_cuda::{make_device_array_py, DeviceArrayF32Py};
18#[cfg(feature = "python")]
19use crate::utilities::kernel_validation::validate_kernel;
20#[cfg(feature = "python")]
21use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
22#[cfg(feature = "python")]
23use pyo3::{
24    exceptions::PyValueError,
25    pyclass, pyfunction, pymethods,
26    types::{PyDict, PyDictMethods},
27    Bound, PyErr, PyResult, Python,
28};
29#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
30use serde::{Deserialize, Serialize};
31#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
32use wasm_bindgen::prelude::*;
33
34impl<'a> AsRef<[f64]> for FoscInput<'a> {
35    #[inline(always)]
36    fn as_ref(&self) -> &[f64] {
37        match &self.data {
38            FoscData::Slice(slice) => slice,
39            FoscData::Candles { candles, source } => source_type(candles, source),
40        }
41    }
42}
43
44#[derive(Debug, Clone)]
45pub enum FoscData<'a> {
46    Candles {
47        candles: &'a Candles,
48        source: &'a str,
49    },
50    Slice(&'a [f64]),
51}
52
53#[derive(Debug, Clone)]
54pub struct FoscOutput {
55    pub values: Vec<f64>,
56}
57
58#[derive(Debug, Clone)]
59#[cfg_attr(
60    all(target_arch = "wasm32", feature = "wasm"),
61    derive(Serialize, Deserialize)
62)]
63pub struct FoscParams {
64    pub period: Option<usize>,
65}
66
67impl Default for FoscParams {
68    fn default() -> Self {
69        Self { period: Some(5) }
70    }
71}
72
73#[derive(Debug, Clone)]
74pub struct FoscInput<'a> {
75    pub data: FoscData<'a>,
76    pub params: FoscParams,
77}
78
79impl<'a> FoscInput<'a> {
80    #[inline]
81    pub fn from_candles(c: &'a Candles, s: &'a str, p: FoscParams) -> Self {
82        Self {
83            data: FoscData::Candles {
84                candles: c,
85                source: s,
86            },
87            params: p,
88        }
89    }
90    #[inline]
91    pub fn from_slice(sl: &'a [f64], p: FoscParams) -> Self {
92        Self {
93            data: FoscData::Slice(sl),
94            params: p,
95        }
96    }
97    #[inline]
98    pub fn with_default_candles(c: &'a Candles) -> Self {
99        Self::from_candles(c, "close", FoscParams::default())
100    }
101    #[inline]
102    pub fn get_period(&self) -> usize {
103        self.params.period.unwrap_or(5)
104    }
105}
106
107#[derive(Copy, Clone, Debug)]
108pub struct FoscBuilder {
109    period: Option<usize>,
110    kernel: Kernel,
111}
112
113impl Default for FoscBuilder {
114    fn default() -> Self {
115        Self {
116            period: None,
117            kernel: Kernel::Auto,
118        }
119    }
120}
121
122impl FoscBuilder {
123    #[inline(always)]
124    pub fn new() -> Self {
125        Self::default()
126    }
127    #[inline(always)]
128    pub fn period(mut self, n: usize) -> Self {
129        self.period = Some(n);
130        self
131    }
132    #[inline(always)]
133    pub fn kernel(mut self, k: Kernel) -> Self {
134        self.kernel = k;
135        self
136    }
137    #[inline(always)]
138    pub fn apply(self, c: &Candles) -> Result<FoscOutput, FoscError> {
139        let p = FoscParams {
140            period: self.period,
141        };
142        let i = FoscInput::from_candles(c, "close", p);
143        fosc_with_kernel(&i, self.kernel)
144    }
145    #[inline(always)]
146    pub fn apply_slice(self, d: &[f64]) -> Result<FoscOutput, FoscError> {
147        let p = FoscParams {
148            period: self.period,
149        };
150        let i = FoscInput::from_slice(d, p);
151        fosc_with_kernel(&i, self.kernel)
152    }
153    #[inline(always)]
154    pub fn into_stream(self) -> Result<FoscStream, FoscError> {
155        let p = FoscParams {
156            period: self.period,
157        };
158        FoscStream::try_new(p)
159    }
160}
161
162#[derive(Debug, Error)]
163pub enum FoscError {
164    #[error("fosc: Empty input data provided")]
165    EmptyInputData,
166    #[error("fosc: All values are NaN.")]
167    AllValuesNaN,
168    #[error("fosc: Invalid period: period = {period}, data length = {data_len}")]
169    InvalidPeriod { period: usize, data_len: usize },
170    #[error("fosc: Not enough valid data: needed = {needed}, valid = {valid}")]
171    NotEnoughValidData { needed: usize, valid: usize },
172    #[error("fosc: Output length mismatch: expected = {expected}, got = {got}")]
173    OutputLengthMismatch { expected: usize, got: usize },
174    #[error("fosc: Invalid kernel for batch operation: {0:?}")]
175    InvalidKernelForBatch(crate::utilities::enums::Kernel),
176    #[error("fosc: Invalid range expansion: start={start}, end={end}, step={step}")]
177    InvalidRange {
178        start: usize,
179        end: usize,
180        step: usize,
181    },
182}
183
184#[inline]
185pub fn fosc(input: &FoscInput) -> Result<FoscOutput, FoscError> {
186    fosc_with_kernel(input, Kernel::Auto)
187}
188
189pub fn fosc_with_kernel(input: &FoscInput, kernel: Kernel) -> Result<FoscOutput, FoscError> {
190    let data: &[f64] = input.as_ref();
191    let len = data.len();
192    if len == 0 {
193        return Err(FoscError::EmptyInputData);
194    }
195    let period = input.get_period();
196    let first = data
197        .iter()
198        .position(|x| !x.is_nan())
199        .ok_or(FoscError::AllValuesNaN)?;
200    if period == 0 || period > len {
201        return Err(FoscError::InvalidPeriod {
202            period,
203            data_len: len,
204        });
205    }
206    if (len - first) < period {
207        return Err(FoscError::NotEnoughValidData {
208            needed: period,
209            valid: len - first,
210        });
211    }
212    let chosen = match kernel {
213        Kernel::Auto => detect_best_kernel(),
214        other => other,
215    };
216
217    let mut out = alloc_with_nan_prefix(len, first + period - 1);
218    unsafe {
219        match chosen {
220            Kernel::Scalar | Kernel::ScalarBatch => fosc_scalar(data, period, first, &mut out),
221            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
222            Kernel::Avx2 | Kernel::Avx2Batch => fosc_avx2(data, period, first, &mut out),
223            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
224            Kernel::Avx512 | Kernel::Avx512Batch => fosc_avx512(data, period, first, &mut out),
225            _ => unreachable!(),
226        }
227    }
228    Ok(FoscOutput { values: out })
229}
230
231#[inline]
232pub fn fosc_into_slice(dst: &mut [f64], input: &FoscInput, kern: Kernel) -> Result<(), FoscError> {
233    let data: &[f64] = input.as_ref();
234    let len = data.len();
235    let period = input.get_period();
236
237    if len == 0 {
238        return Err(FoscError::EmptyInputData);
239    }
240
241    if dst.len() != len {
242        return Err(FoscError::OutputLengthMismatch {
243            expected: len,
244            got: dst.len(),
245        });
246    }
247
248    let first = data
249        .iter()
250        .position(|x| !x.is_nan())
251        .ok_or(FoscError::AllValuesNaN)?;
252    if period == 0 || period > len {
253        return Err(FoscError::InvalidPeriod {
254            period,
255            data_len: len,
256        });
257    }
258    if (len - first) < period {
259        return Err(FoscError::NotEnoughValidData {
260            needed: period,
261            valid: len - first,
262        });
263    }
264
265    let chosen = match kern {
266        Kernel::Auto => detect_best_kernel(),
267        other => other,
268    };
269
270    unsafe {
271        match chosen {
272            Kernel::Scalar | Kernel::ScalarBatch => fosc_scalar(data, period, first, dst),
273            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
274            Kernel::Avx2 | Kernel::Avx2Batch => fosc_avx2(data, period, first, dst),
275            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
276            Kernel::Avx512 | Kernel::Avx512Batch => fosc_avx512(data, period, first, dst),
277            _ => unreachable!(),
278        }
279    }
280
281    let warmup_end = first + period - 1;
282    for v in &mut dst[..warmup_end] {
283        *v = f64::NAN;
284    }
285
286    Ok(())
287}
288
289#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
290#[inline]
291pub fn fosc_into(input: &FoscInput, out: &mut [f64]) -> Result<(), FoscError> {
292    fosc_into_slice(out, input, Kernel::Auto)
293}
294
295#[inline(always)]
296unsafe fn fosc_core(data: &[f64], period: usize, first: usize, out: &mut [f64]) {
297    let n = data.len();
298    if n == 0 || period == 0 || n < period {
299        return;
300    }
301
302    let begin = first + period - 1;
303    if begin >= n {
304        return;
305    }
306
307    let p = period as f64;
308    let x = 0.5 * p * (p + 1.0);
309    let x2 = (p * (p + 1.0) * (2.0 * p + 1.0)) / 6.0;
310    let denom = p * x2 - x * x;
311    let bd = if denom.abs() < f64::EPSILON {
312        0.0
313    } else {
314        1.0 / denom
315    };
316    let inv_p = 1.0 / p;
317
318    let p_bd = p * bd;
319    let x_bd = x * bd;
320
321    let tsf_coeff = 0.5 * (p + 1.0);
322
323    let mut y = 0.0f64;
324    let mut xy = 0.0f64;
325    let base = data.as_ptr().add(first);
326    let limit = period - 1;
327    let mut k = 0usize;
328    while k + 4 <= limit {
329        let d0 = *base.add(k + 0);
330        let d1 = *base.add(k + 1);
331        let d2 = *base.add(k + 2);
332        let d3 = *base.add(k + 3);
333
334        y += d0 + d1 + d2 + d3;
335        xy += d0 * (k + 1) as f64;
336        xy += d1 * (k + 2) as f64;
337        xy += d2 * (k + 3) as f64;
338        xy += d3 * (k + 4) as f64;
339        k += 4;
340    }
341    while k < limit {
342        let d = *base.add(k);
343        y += d;
344        xy += d * (k + 1) as f64;
345        k += 1;
346    }
347
348    let dp = data.as_ptr();
349    let op = out.as_mut_ptr();
350    let mut tsf_prev = 0.0f64;
351    let mut i = begin;
352    while i < n {
353        let newv = *dp.add(i);
354
355        let y_plus = y + newv;
356        let xy_plus = xy + newv * p;
357
358        *op.add(i) = if newv != 0.0 {
359            100.0 * (newv - tsf_prev) / newv
360        } else {
361            f64::NAN
362        };
363
364        let b = xy_plus * p_bd - y_plus * x_bd;
365        tsf_prev = y_plus * inv_p + b * tsf_coeff;
366
367        let old_idx = i + 1 - period;
368        let oldv = *dp.add(old_idx);
369        xy = xy_plus - y_plus;
370        y = y_plus - oldv;
371
372        i += 1;
373    }
374}
375
376#[inline]
377pub fn fosc_scalar(data: &[f64], period: usize, first: usize, out: &mut [f64]) {
378    unsafe { fosc_core(data, period, first, out) }
379}
380
381#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
382#[inline]
383pub unsafe fn fosc_avx2(data: &[f64], period: usize, first: usize, out: &mut [f64]) {
384    fosc_core(data, period, first, out)
385}
386
387#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
388#[inline]
389pub unsafe fn fosc_avx512(data: &[f64], period: usize, first: usize, out: &mut [f64]) {
390    fosc_avx2(data, period, first, out)
391}
392
393#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
394#[inline]
395pub unsafe fn fosc_avx512_short(data: &[f64], period: usize, first: usize, out: &mut [f64]) {
396    fosc_avx2(data, period, first, out)
397}
398
399#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
400#[inline]
401pub unsafe fn fosc_avx512_long(data: &[f64], period: usize, first: usize, out: &mut [f64]) {
402    fosc_avx2(data, period, first, out)
403}
404
405#[derive(Debug, Clone)]
406pub struct FoscStream {
407    period: usize,
408    buffer: Vec<f64>,
409    idx: usize,
410    filled: bool,
411
412    x: f64,
413    x2: f64,
414    inv_den: f64,
415    inv_p: f64,
416    p_f64: f64,
417    p1: f64,
418
419    y: f64,
420    xy: f64,
421
422    tsf: f64,
423
424    count: usize,
425}
426
427impl FoscStream {
428    pub fn try_new(params: FoscParams) -> Result<Self, FoscError> {
429        let period = params.period.unwrap_or(5);
430        if period == 0 {
431            return Err(FoscError::InvalidPeriod {
432                period,
433                data_len: 0,
434            });
435        }
436        let p = period as f64;
437
438        let x = 0.5 * p * (p + 1.0);
439        let x2 = (p * (p + 1.0) * (2.0 * p + 1.0)) / 6.0;
440        let den = p * x2 - x * x;
441        let inv_den = if den.abs() < f64::EPSILON {
442            0.0
443        } else {
444            1.0 / den
445        };
446
447        Ok(Self {
448            period,
449            buffer: vec![0.0; period],
450            idx: 0,
451            filled: false,
452
453            x,
454            x2,
455            inv_den,
456            inv_p: 1.0 / p,
457            p_f64: p,
458            p1: p + 1.0,
459
460            y: 0.0,
461            xy: 0.0,
462            tsf: 0.0,
463            count: 0,
464        })
465    }
466
467    #[inline(always)]
468    pub fn update(&mut self, value: f64) -> Option<f64> {
469        if self.count < self.period {
470            self.buffer[self.idx] = value;
471            self.y += value;
472            self.xy += value * (self.count as f64 + 1.0);
473
474            self.idx += 1;
475            if self.idx == self.period {
476                self.idx = 0;
477            }
478            self.count += 1;
479
480            if self.count == self.period {
481                self.filled = true;
482
483                let b = (self.p_f64.mul_add(self.xy, -self.x * self.y)) * self.inv_den;
484                let a = (self.y - b * self.x) * self.inv_p;
485                self.tsf = b.mul_add(self.p1, a);
486            }
487            return None;
488        }
489
490        let out = if value.is_finite() && value != 0.0 {
491            100.0 * (1.0 - self.tsf / value)
492        } else {
493            f64::NAN
494        };
495
496        let old = self.buffer[self.idx];
497        self.buffer[self.idx] = value;
498        self.idx += 1;
499        if self.idx == self.period {
500            self.idx = 0;
501        }
502
503        let y_prev = self.y;
504        self.y = y_prev - old + value;
505        self.xy = self.xy - y_prev + self.p_f64 * value;
506
507        let b = (self.p_f64.mul_add(self.xy, -self.x * self.y)) * self.inv_den;
508        let a = (self.y - b * self.x) * self.inv_p;
509        self.tsf = b.mul_add(self.p1, a);
510
511        Some(out)
512    }
513}
514
515#[derive(Clone, Debug)]
516pub struct FoscBatchRange {
517    pub period: (usize, usize, usize),
518}
519
520impl Default for FoscBatchRange {
521    fn default() -> Self {
522        Self {
523            period: (5, 254, 1),
524        }
525    }
526}
527
528#[derive(Clone, Debug, Default)]
529pub struct FoscBatchBuilder {
530    range: FoscBatchRange,
531    kernel: Kernel,
532}
533
534impl FoscBatchBuilder {
535    pub fn new() -> Self {
536        Self::default()
537    }
538    pub fn kernel(mut self, k: Kernel) -> Self {
539        self.kernel = k;
540        self
541    }
542    #[inline]
543    pub fn period_range(mut self, start: usize, end: usize, step: usize) -> Self {
544        self.range.period = (start, end, step);
545        self
546    }
547    #[inline]
548    pub fn period_static(mut self, p: usize) -> Self {
549        self.range.period = (p, p, 0);
550        self
551    }
552    pub fn apply_slice(self, data: &[f64]) -> Result<FoscBatchOutput, FoscError> {
553        fosc_batch_with_kernel(data, &self.range, self.kernel)
554    }
555    pub fn with_default_slice(data: &[f64], k: Kernel) -> Result<FoscBatchOutput, FoscError> {
556        FoscBatchBuilder::new().kernel(k).apply_slice(data)
557    }
558    pub fn apply_candles(self, c: &Candles, src: &str) -> Result<FoscBatchOutput, FoscError> {
559        let slice = source_type(c, src);
560        self.apply_slice(slice)
561    }
562    pub fn with_default_candles(c: &Candles) -> Result<FoscBatchOutput, FoscError> {
563        FoscBatchBuilder::new()
564            .kernel(Kernel::Auto)
565            .apply_candles(c, "close")
566    }
567}
568
569#[derive(Clone, Debug)]
570pub struct FoscBatchOutput {
571    pub values: Vec<f64>,
572    pub combos: Vec<FoscParams>,
573    pub rows: usize,
574    pub cols: usize,
575}
576
577impl FoscBatchOutput {
578    pub fn row_for_params(&self, p: &FoscParams) -> Option<usize> {
579        self.combos
580            .iter()
581            .position(|c| c.period.unwrap_or(5) == p.period.unwrap_or(5))
582    }
583    pub fn values_for(&self, p: &FoscParams) -> Option<&[f64]> {
584        self.row_for_params(p).map(|row| {
585            let start = row * self.cols;
586            &self.values[start..start + self.cols]
587        })
588    }
589}
590
591#[inline(always)]
592fn expand_grid(r: &FoscBatchRange) -> Result<Vec<FoscParams>, FoscError> {
593    #[inline]
594    fn axis_usize((start, end, step): (usize, usize, usize)) -> Result<Vec<usize>, FoscError> {
595        if step == 0 || start == end {
596            return Ok(vec![start]);
597        }
598        if start < end {
599            let v: Vec<usize> = (start..=end).step_by(step).collect();
600            return if v.is_empty() {
601                Err(FoscError::InvalidRange { start, end, step })
602            } else {
603                Ok(v)
604            };
605        } else {
606            let mut v = Vec::new();
607            let mut cur = start;
608            loop {
609                v.push(cur);
610                if cur <= end {
611                    break;
612                }
613                match cur.checked_sub(step) {
614                    Some(next) => cur = next,
615                    None => break,
616                }
617                if cur == usize::MAX {
618                    break;
619                }
620                if cur <= end {
621                    break;
622                }
623            }
624            if v.is_empty() {
625                Err(FoscError::InvalidRange { start, end, step })
626            } else {
627                Ok(v)
628            }
629        }
630    }
631
632    let periods = axis_usize(r.period)?;
633    if periods.is_empty() {
634        return Err(FoscError::InvalidRange {
635            start: r.period.0,
636            end: r.period.1,
637            step: r.period.2,
638        });
639    }
640    let mut out = Vec::with_capacity(periods.len());
641    for &p in &periods {
642        out.push(FoscParams { period: Some(p) });
643    }
644    Ok(out)
645}
646
647#[inline(always)]
648pub fn fosc_batch_slice(
649    data: &[f64],
650    sweep: &FoscBatchRange,
651    kern: Kernel,
652) -> Result<FoscBatchOutput, FoscError> {
653    fosc_batch_inner(data, sweep, kern, false)
654}
655
656#[inline(always)]
657pub fn fosc_batch_par_slice(
658    data: &[f64],
659    sweep: &FoscBatchRange,
660    kern: Kernel,
661) -> Result<FoscBatchOutput, FoscError> {
662    fosc_batch_inner(data, sweep, kern, true)
663}
664
665pub fn fosc_batch_with_kernel(
666    data: &[f64],
667    sweep: &FoscBatchRange,
668    k: Kernel,
669) -> Result<FoscBatchOutput, FoscError> {
670    let kernel = match k {
671        Kernel::Auto => detect_best_batch_kernel(),
672        other if other.is_batch() => other,
673        other => return Err(FoscError::InvalidKernelForBatch(other)),
674    };
675    let simd = match kernel {
676        Kernel::Avx512Batch => Kernel::Avx512,
677        Kernel::Avx2Batch => Kernel::Avx2,
678        Kernel::ScalarBatch => Kernel::Scalar,
679        _ => unreachable!(),
680    };
681    fosc_batch_par_slice(data, sweep, simd)
682}
683
684#[inline(always)]
685fn fosc_batch_inner(
686    data: &[f64],
687    sweep: &FoscBatchRange,
688    kern: Kernel,
689    parallel: bool,
690) -> Result<FoscBatchOutput, FoscError> {
691    let combos = expand_grid(sweep)?;
692    let first = data
693        .iter()
694        .position(|x| !x.is_nan())
695        .ok_or(FoscError::AllValuesNaN)?;
696    let max_p = combos.iter().map(|c| c.period.unwrap()).max().unwrap();
697    if data.len() - first < max_p {
698        return Err(FoscError::NotEnoughValidData {
699            needed: max_p,
700            valid: data.len() - first,
701        });
702    }
703    let rows = combos.len();
704    let cols = data.len();
705    let mut buf_mu = make_uninit_matrix(rows, cols);
706
707    let warmup_periods: Vec<usize> = combos
708        .iter()
709        .map(|c| first + c.period.unwrap_or(5) - 1)
710        .collect();
711    init_matrix_prefixes(&mut buf_mu, cols, &warmup_periods);
712
713    let mut guard = core::mem::ManuallyDrop::new(buf_mu);
714    let out_f64: &mut [f64] =
715        unsafe { core::slice::from_raw_parts_mut(guard.as_mut_ptr() as *mut f64, guard.len()) };
716
717    let actual = match kern {
718        Kernel::Auto => Kernel::ScalarBatch,
719        k => k,
720    };
721    let simd = match actual {
722        Kernel::ScalarBatch => Kernel::Scalar,
723        Kernel::Scalar => Kernel::Scalar,
724        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
725        Kernel::Avx2Batch => Kernel::Avx2,
726        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
727        Kernel::Avx2 => Kernel::Avx2,
728        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
729        Kernel::Avx512Batch => Kernel::Avx512,
730        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
731        Kernel::Avx512 => Kernel::Avx512,
732        _ => unreachable!(),
733    };
734
735    let do_row = |row: usize, out_row: &mut [f64]| unsafe {
736        let period = combos[row].period.unwrap();
737        match simd {
738            Kernel::Scalar => fosc_row_scalar(data, first, period, out_row),
739            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
740            Kernel::Avx2 => fosc_row_avx2(data, first, period, out_row),
741            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
742            Kernel::Avx512 => fosc_row_avx512(data, first, period, out_row),
743            _ => unreachable!(),
744        }
745    };
746
747    if parallel {
748        #[cfg(not(target_arch = "wasm32"))]
749        {
750            out_f64
751                .par_chunks_mut(cols)
752                .enumerate()
753                .for_each(|(row, slice)| do_row(row, slice));
754        }
755
756        #[cfg(target_arch = "wasm32")]
757        {
758            for (row, slice) in out_f64.chunks_mut(cols).enumerate() {
759                do_row(row, slice);
760            }
761        }
762    } else {
763        for (row, slice) in out_f64.chunks_mut(cols).enumerate() {
764            do_row(row, slice);
765        }
766    }
767
768    let values = unsafe {
769        Vec::from_raw_parts(
770            guard.as_mut_ptr() as *mut f64,
771            guard.len(),
772            guard.capacity(),
773        )
774    };
775
776    Ok(FoscBatchOutput {
777        values,
778        combos,
779        rows,
780        cols,
781    })
782}
783
784#[inline(always)]
785fn fosc_batch_inner_into(
786    data: &[f64],
787    sweep: &FoscBatchRange,
788    kern: Kernel,
789    parallel: bool,
790    out: &mut [f64],
791) -> Result<Vec<FoscParams>, FoscError> {
792    let combos = expand_grid(sweep)?;
793    let first = data
794        .iter()
795        .position(|x| !x.is_nan())
796        .ok_or(FoscError::AllValuesNaN)?;
797    let max_p = combos.iter().map(|c| c.period.unwrap()).max().unwrap();
798    if data.len() - first < max_p {
799        return Err(FoscError::NotEnoughValidData {
800            needed: max_p,
801            valid: data.len() - first,
802        });
803    }
804
805    let cols = data.len();
806    let rows = combos.len();
807    let expected = rows.checked_mul(cols).ok_or(FoscError::InvalidRange {
808        start: sweep.period.0,
809        end: sweep.period.1,
810        step: sweep.period.2,
811    })?;
812    if out.len() != expected {
813        return Err(FoscError::OutputLengthMismatch {
814            expected,
815            got: out.len(),
816        });
817    }
818
819    let out_uninit: &mut [MaybeUninit<f64>] = unsafe {
820        core::slice::from_raw_parts_mut(out.as_mut_ptr() as *mut MaybeUninit<f64>, out.len())
821    };
822
823    let warmup_periods: Vec<usize> = combos
824        .iter()
825        .map(|c| first + c.period.unwrap() - 1)
826        .collect();
827    init_matrix_prefixes(out_uninit, cols, &warmup_periods);
828
829    let actual = match kern {
830        Kernel::Auto => detect_best_batch_kernel(),
831        k => k,
832    };
833    let simd = match actual {
834        Kernel::ScalarBatch => Kernel::Scalar,
835        Kernel::Scalar => Kernel::Scalar,
836        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
837        Kernel::Avx2Batch => Kernel::Avx2,
838        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
839        Kernel::Avx2 => Kernel::Avx2,
840        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
841        Kernel::Avx512Batch => Kernel::Avx512,
842        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
843        Kernel::Avx512 => Kernel::Avx512,
844        _ => unreachable!(),
845    };
846
847    let do_row = |row: usize, dst_mu: &mut [MaybeUninit<f64>]| unsafe {
848        let period = combos[row].period.unwrap();
849
850        let dst = core::slice::from_raw_parts_mut(dst_mu.as_mut_ptr() as *mut f64, dst_mu.len());
851        match simd {
852            Kernel::Scalar => fosc_row_scalar(data, first, period, dst),
853            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
854            Kernel::Avx2 => fosc_row_avx2(data, first, period, dst),
855            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
856            Kernel::Avx512 => fosc_row_avx512(data, first, period, dst),
857            _ => unreachable!(),
858        }
859    };
860
861    if parallel {
862        #[cfg(not(target_arch = "wasm32"))]
863        {
864            out_uninit
865                .par_chunks_mut(cols)
866                .enumerate()
867                .for_each(|(r, sl)| do_row(r, sl));
868        }
869        #[cfg(target_arch = "wasm32")]
870        {
871            for (r, sl) in out_uninit.chunks_mut(cols).enumerate() {
872                do_row(r, sl);
873            }
874        }
875    } else {
876        for (r, sl) in out_uninit.chunks_mut(cols).enumerate() {
877            do_row(r, sl);
878        }
879    }
880
881    Ok(combos)
882}
883
884#[inline(always)]
885unsafe fn fosc_row_scalar(data: &[f64], first: usize, period: usize, out: &mut [f64]) {
886    fosc_scalar(data, period, first, out);
887}
888
889#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
890#[inline(always)]
891unsafe fn fosc_row_avx2(data: &[f64], first: usize, period: usize, out: &mut [f64]) {
892    fosc_avx2(data, period, first, out);
893}
894
895#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
896#[inline(always)]
897unsafe fn fosc_row_avx512(data: &[f64], first: usize, period: usize, out: &mut [f64]) {
898    if period <= 32 {
899        fosc_row_avx512_short(data, first, period, out)
900    } else {
901        fosc_row_avx512_long(data, first, period, out)
902    }
903}
904
905#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
906#[inline(always)]
907unsafe fn fosc_row_avx512_short(data: &[f64], first: usize, period: usize, out: &mut [f64]) {
908    fosc_avx512_short(data, period, first, out)
909}
910
911#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
912#[inline(always)]
913unsafe fn fosc_row_avx512_long(data: &[f64], first: usize, period: usize, out: &mut [f64]) {
914    fosc_avx512_long(data, period, first, out)
915}
916
917#[cfg(feature = "python")]
918#[pyfunction(name = "fosc")]
919#[pyo3(signature = (data, period=5, kernel=None))]
920pub fn fosc_py<'py>(
921    py: Python<'py>,
922    data: PyReadonlyArray1<'py, f64>,
923    period: usize,
924    kernel: Option<&str>,
925) -> PyResult<Bound<'py, PyArray1<f64>>> {
926    let data_slice = data.as_slice()?;
927    let kernel_enum = validate_kernel(kernel, false)?;
928
929    let params = FoscParams {
930        period: Some(period),
931    };
932    let input = FoscInput::from_slice(data_slice, params);
933
934    py.allow_threads(|| {
935        let output = fosc_with_kernel(&input, kernel_enum)
936            .map_err(|e| PyErr::new::<PyValueError, _>(format!("Rust computation error: {}", e)))?;
937        Ok(output)
938    })
939    .map(|result| result.values.into_pyarray(py))
940}
941
942#[cfg(feature = "python")]
943#[pyfunction(name = "fosc_batch")]
944#[pyo3(signature = (data, period_range, kernel=None))]
945pub fn fosc_batch_py<'py>(
946    py: Python<'py>,
947    data: PyReadonlyArray1<'py, f64>,
948    period_range: (usize, usize, usize),
949    kernel: Option<&str>,
950) -> PyResult<Bound<'py, PyDict>> {
951    use numpy::{IntoPyArray, PyArray1, PyArrayMethods};
952    use pyo3::types::PyDict;
953
954    let slice_in = data.as_slice()?;
955    let sweep = FoscBatchRange {
956        period: period_range,
957    };
958
959    let combos = expand_grid(&sweep).map_err(|e| PyErr::new::<PyValueError, _>(e.to_string()))?;
960    let rows = combos.len();
961    let cols = slice_in.len();
962
963    let total = rows
964        .checked_mul(cols)
965        .ok_or_else(|| PyErr::new::<PyValueError, _>("rows*cols overflow in fosc_batch_py"))?;
966    let out_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
967    let out_slice = unsafe { out_arr.as_slice_mut()? };
968
969    let kern = validate_kernel(kernel, true)?;
970
971    let combos = py
972        .allow_threads(|| {
973            let batch = match kern {
974                Kernel::Auto => detect_best_batch_kernel(),
975                k => k,
976            };
977            let simd = match batch {
978                Kernel::ScalarBatch => Kernel::Scalar,
979                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
980                Kernel::Avx2Batch => Kernel::Avx2,
981                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
982                Kernel::Avx512Batch => Kernel::Avx512,
983                _ => unreachable!(),
984            };
985            fosc_batch_inner_into(slice_in, &sweep, simd, true, out_slice)
986        })
987        .map_err(|e| PyValueError::new_err(e.to_string()))?;
988
989    let dict = PyDict::new(py);
990
991    dict.set_item("values", out_arr.reshape((rows, cols))?)?;
992
993    dict.set_item(
994        "periods",
995        combos
996            .iter()
997            .map(|p| p.period.unwrap_or(5) as u64)
998            .collect::<Vec<_>>()
999            .into_pyarray(py),
1000    )?;
1001    dict.set_item("rows", rows)?;
1002    dict.set_item("cols", cols)?;
1003    Ok(dict)
1004}
1005
1006#[cfg(all(feature = "python", feature = "cuda"))]
1007#[pyfunction(name = "fosc_cuda_batch_dev")]
1008#[pyo3(signature = (data_f32, period_range, device_id=0))]
1009pub fn fosc_cuda_batch_dev_py<'py>(
1010    py: Python<'py>,
1011    data_f32: numpy::PyReadonlyArray1<'py, f32>,
1012    period_range: (usize, usize, usize),
1013    device_id: usize,
1014) -> PyResult<DeviceArrayF32Py> {
1015    use crate::cuda::cuda_available;
1016    use crate::cuda::oscillators::fosc_wrapper::CudaFosc;
1017    if !cuda_available() {
1018        return Err(PyValueError::new_err("CUDA not available"));
1019    }
1020    let slice_in = data_f32.as_slice()?;
1021    let sweep = FoscBatchRange {
1022        period: period_range,
1023    };
1024    let inner = py.allow_threads(|| {
1025        let cuda = CudaFosc::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
1026        cuda.fosc_batch_dev(slice_in, &sweep)
1027            .map_err(|e| PyValueError::new_err(e.to_string()))
1028    })?;
1029    let handle = make_device_array_py(device_id, inner)?;
1030    Ok(handle)
1031}
1032
1033#[cfg(all(feature = "python", feature = "cuda"))]
1034#[pyfunction(name = "fosc_cuda_many_series_one_param_dev")]
1035#[pyo3(signature = (data_tm_f32, period, device_id=0))]
1036pub fn fosc_cuda_many_series_one_param_dev_py(
1037    py: Python<'_>,
1038    data_tm_f32: numpy::PyReadonlyArray2<'_, f32>,
1039    period: usize,
1040    device_id: usize,
1041) -> PyResult<DeviceArrayF32Py> {
1042    use crate::cuda::cuda_available;
1043    use crate::cuda::oscillators::fosc_wrapper::CudaFosc;
1044    use numpy::PyUntypedArrayMethods;
1045
1046    if !cuda_available() {
1047        return Err(PyValueError::new_err("CUDA not available"));
1048    }
1049    let flat_in: &[f32] = data_tm_f32.as_slice()?;
1050    let rows = data_tm_f32.shape()[0];
1051    let cols = data_tm_f32.shape()[1];
1052    let params = FoscParams {
1053        period: Some(period),
1054    };
1055    let inner = py.allow_threads(|| {
1056        let cuda = CudaFosc::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
1057        cuda.fosc_many_series_one_param_time_major_dev(flat_in, cols, rows, &params)
1058            .map_err(|e| PyValueError::new_err(e.to_string()))
1059    })?;
1060    let handle = make_device_array_py(device_id, inner)?;
1061    Ok(handle)
1062}
1063
1064#[cfg(feature = "python")]
1065#[pyclass(name = "FoscStream")]
1066pub struct FoscStreamPy {
1067    inner: FoscStream,
1068}
1069
1070#[cfg(feature = "python")]
1071#[pymethods]
1072impl FoscStreamPy {
1073    #[new]
1074    #[pyo3(signature = (period=5))]
1075    pub fn new(period: usize) -> PyResult<Self> {
1076        let params = FoscParams {
1077            period: Some(period),
1078        };
1079        let inner = FoscStream::try_new(params).map_err(|e| {
1080            PyErr::new::<PyValueError, _>(format!("Failed to create stream: {}", e))
1081        })?;
1082        Ok(Self { inner })
1083    }
1084
1085    pub fn update(&mut self, value: f64) -> Option<f64> {
1086        self.inner.update(value)
1087    }
1088}
1089
1090#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1091#[wasm_bindgen]
1092pub fn fosc_js(data: &[f64], period: usize) -> Result<Vec<f64>, JsValue> {
1093    let params = FoscParams {
1094        period: Some(period),
1095    };
1096    let input = FoscInput::from_slice(data, params);
1097
1098    let mut output = vec![0.0; data.len()];
1099    fosc_into_slice(&mut output, &input, Kernel::Auto)
1100        .map_err(|e| JsValue::from_str(&e.to_string()))?;
1101
1102    Ok(output)
1103}
1104
1105#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1106#[wasm_bindgen]
1107pub fn fosc_alloc(len: usize) -> *mut f64 {
1108    let mut vec = Vec::<f64>::with_capacity(len);
1109    let ptr = vec.as_mut_ptr();
1110    std::mem::forget(vec);
1111    ptr
1112}
1113
1114#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1115#[wasm_bindgen]
1116pub fn fosc_free(ptr: *mut f64, len: usize) {
1117    if !ptr.is_null() {
1118        unsafe {
1119            let _ = Vec::from_raw_parts(ptr, len, len);
1120        }
1121    }
1122}
1123
1124#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1125#[wasm_bindgen]
1126pub fn fosc_into(
1127    in_ptr: *const f64,
1128    out_ptr: *mut f64,
1129    len: usize,
1130    period: usize,
1131) -> Result<(), JsValue> {
1132    if in_ptr.is_null() || out_ptr.is_null() {
1133        return Err(JsValue::from_str("null pointer passed to fosc_into"));
1134    }
1135
1136    unsafe {
1137        let data = std::slice::from_raw_parts(in_ptr, len);
1138
1139        if period == 0 || period > len {
1140            return Err(JsValue::from_str("Invalid period"));
1141        }
1142
1143        let params = FoscParams {
1144            period: Some(period),
1145        };
1146        let input = FoscInput::from_slice(data, params);
1147
1148        if in_ptr == out_ptr {
1149            let mut temp = vec![0.0; len];
1150            fosc_into_slice(&mut temp, &input, Kernel::Auto)
1151                .map_err(|e| JsValue::from_str(&e.to_string()))?;
1152            let out = std::slice::from_raw_parts_mut(out_ptr, len);
1153            out.copy_from_slice(&temp);
1154        } else {
1155            let out = std::slice::from_raw_parts_mut(out_ptr, len);
1156            fosc_into_slice(out, &input, Kernel::Auto)
1157                .map_err(|e| JsValue::from_str(&e.to_string()))?;
1158        }
1159        Ok(())
1160    }
1161}
1162
1163#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1164#[derive(Serialize, Deserialize)]
1165pub struct FoscBatchConfig {
1166    pub period_range: (usize, usize, usize),
1167}
1168
1169#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1170#[derive(Serialize, Deserialize)]
1171pub struct FoscBatchJsOutput {
1172    pub values: Vec<f64>,
1173    pub combos: Vec<FoscParams>,
1174    pub rows: usize,
1175    pub cols: usize,
1176}
1177
1178#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1179#[wasm_bindgen(js_name = fosc_batch)]
1180pub fn fosc_batch_js(data: &[f64], config: JsValue) -> Result<JsValue, JsValue> {
1181    let config: FoscBatchConfig = serde_wasm_bindgen::from_value(config)
1182        .map_err(|e| JsValue::from_str(&format!("Invalid config: {}", e)))?;
1183
1184    let range = FoscBatchRange {
1185        period: config.period_range,
1186    };
1187
1188    let output = fosc_batch_inner(data, &range, Kernel::Auto, false)
1189        .map_err(|e| JsValue::from_str(&e.to_string()))?;
1190
1191    let js_output = FoscBatchJsOutput {
1192        values: output.values,
1193        combos: output.combos,
1194        rows: output.rows,
1195        cols: output.cols,
1196    };
1197
1198    serde_wasm_bindgen::to_value(&js_output)
1199        .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
1200}
1201
1202#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1203#[wasm_bindgen]
1204pub fn fosc_batch_into(
1205    in_ptr: *const f64,
1206    out_ptr: *mut f64,
1207    len: usize,
1208    period_start: usize,
1209    period_end: usize,
1210    period_step: usize,
1211) -> Result<usize, JsValue> {
1212    if in_ptr.is_null() || out_ptr.is_null() {
1213        return Err(JsValue::from_str("null pointer passed to fosc_batch_into"));
1214    }
1215
1216    unsafe {
1217        let data = std::slice::from_raw_parts(in_ptr, len);
1218
1219        let range = FoscBatchRange {
1220            period: (period_start, period_end, period_step),
1221        };
1222
1223        let output = fosc_batch_inner(data, &range, Kernel::Auto, false)
1224            .map_err(|e| JsValue::from_str(&e.to_string()))?;
1225
1226        let out = std::slice::from_raw_parts_mut(out_ptr, output.values.len());
1227        out.copy_from_slice(&output.values);
1228
1229        Ok(output.rows)
1230    }
1231}
1232
1233#[cfg(test)]
1234mod tests {
1235    use super::*;
1236    use crate::skip_if_unsupported;
1237    use crate::utilities::data_loader::read_candles_from_csv;
1238
1239    fn check_fosc_partial_params(
1240        test_name: &str,
1241        kernel: Kernel,
1242    ) -> Result<(), Box<dyn std::error::Error>> {
1243        skip_if_unsupported!(kernel, test_name);
1244        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1245        let candles = read_candles_from_csv(file_path)?;
1246        let default_params = FoscParams { period: None };
1247        let input = FoscInput::from_candles(&candles, "close", default_params);
1248        let output = fosc_with_kernel(&input, kernel)?;
1249        assert_eq!(output.values.len(), candles.close.len());
1250        Ok(())
1251    }
1252
1253    fn check_fosc_basic_accuracy(
1254        test_name: &str,
1255        kernel: Kernel,
1256    ) -> Result<(), Box<dyn std::error::Error>> {
1257        skip_if_unsupported!(kernel, test_name);
1258        let test_data = [
1259            81.59, 81.06, 82.87, 83.00, 83.61, 83.15, 82.84, 82.84, 83.99, 84.55, 84.36, 85.53,
1260        ];
1261        let period = 5;
1262        let input = FoscInput::from_slice(
1263            &test_data,
1264            FoscParams {
1265                period: Some(period),
1266            },
1267        );
1268        let result = fosc_with_kernel(&input, kernel)?;
1269        assert_eq!(result.values.len(), test_data.len());
1270        for i in 0..(period - 1) {
1271            assert!(result.values[i].is_nan());
1272        }
1273        Ok(())
1274    }
1275
1276    fn check_fosc_with_nan_data(
1277        test_name: &str,
1278        kernel: Kernel,
1279    ) -> Result<(), Box<dyn std::error::Error>> {
1280        skip_if_unsupported!(kernel, test_name);
1281        let input_data = [f64::NAN, f64::NAN, 1.0, 2.0, 3.0, 4.0, 5.0];
1282        let params = FoscParams { period: Some(3) };
1283        let input = FoscInput::from_slice(&input_data, params);
1284        let result = fosc_with_kernel(&input, kernel)?;
1285        assert_eq!(result.values.len(), input_data.len());
1286        Ok(())
1287    }
1288
1289    fn check_fosc_zero_period(
1290        test_name: &str,
1291        kernel: Kernel,
1292    ) -> Result<(), Box<dyn std::error::Error>> {
1293        skip_if_unsupported!(kernel, test_name);
1294        let input_data = [10.0, 20.0, 30.0];
1295        let params = FoscParams { period: Some(0) };
1296        let input = FoscInput::from_slice(&input_data, params);
1297        let res = fosc_with_kernel(&input, kernel);
1298        assert!(
1299            res.is_err(),
1300            "[{}] FOSC should fail with zero period",
1301            test_name
1302        );
1303        Ok(())
1304    }
1305
1306    fn check_fosc_period_exceeds_length(
1307        test_name: &str,
1308        kernel: Kernel,
1309    ) -> Result<(), Box<dyn std::error::Error>> {
1310        skip_if_unsupported!(kernel, test_name);
1311        let data_small = [10.0, 20.0, 30.0];
1312        let params = FoscParams { period: Some(10) };
1313        let input = FoscInput::from_slice(&data_small, params);
1314        let res = fosc_with_kernel(&input, kernel);
1315        assert!(
1316            res.is_err(),
1317            "[{}] FOSC should fail with period exceeding length",
1318            test_name
1319        );
1320        Ok(())
1321    }
1322
1323    fn check_fosc_very_small_dataset(
1324        test_name: &str,
1325        kernel: Kernel,
1326    ) -> Result<(), Box<dyn std::error::Error>> {
1327        skip_if_unsupported!(kernel, test_name);
1328        let single_point = [42.0];
1329        let params = FoscParams { period: Some(5) };
1330        let input = FoscInput::from_slice(&single_point, params);
1331        let res = fosc_with_kernel(&input, kernel);
1332        assert!(
1333            res.is_err(),
1334            "[{}] FOSC should fail with insufficient data",
1335            test_name
1336        );
1337        Ok(())
1338    }
1339
1340    fn check_fosc_all_values_nan(
1341        test_name: &str,
1342        kernel: Kernel,
1343    ) -> Result<(), Box<dyn std::error::Error>> {
1344        skip_if_unsupported!(kernel, test_name);
1345        let input_data = [f64::NAN, f64::NAN, f64::NAN];
1346        let params = FoscParams { period: Some(2) };
1347        let input = FoscInput::from_slice(&input_data, params);
1348        let res = fosc_with_kernel(&input, kernel);
1349        assert!(
1350            res.is_err(),
1351            "[{}] FOSC should fail with all NaN",
1352            test_name
1353        );
1354        Ok(())
1355    }
1356
1357    fn check_fosc_expected_values_reference(
1358        test_name: &str,
1359        kernel: Kernel,
1360    ) -> Result<(), Box<dyn std::error::Error>> {
1361        skip_if_unsupported!(kernel, test_name);
1362        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1363        let candles = read_candles_from_csv(file_path)?;
1364        let expected_last_five = [
1365            -0.8904444627923475,
1366            -0.4763353099245297,
1367            -0.2379782851444668,
1368            0.292790128025632,
1369            -0.6597902988090389,
1370        ];
1371        let params = FoscParams { period: Some(5) };
1372        let input = FoscInput::from_candles(&candles, "close", params);
1373        let result = fosc_with_kernel(&input, kernel)?;
1374        let valid_len = result.values.len();
1375        assert!(valid_len >= 5);
1376        let output_slice = &result.values[valid_len - 5..valid_len];
1377        for (i, &val) in output_slice.iter().enumerate() {
1378            let exp: f64 = expected_last_five[i];
1379            if exp.is_nan() {
1380                assert!(val.is_nan());
1381            } else {
1382                assert!(
1383                    (val - exp).abs() < 1e-7,
1384                    "Mismatch at index {}: expected {}, got {}",
1385                    i,
1386                    exp,
1387                    val
1388                );
1389            }
1390        }
1391        Ok(())
1392    }
1393
1394    macro_rules! generate_all_fosc_tests {
1395        ($($test_fn:ident),*) => {
1396            paste::paste! {
1397                $(
1398                    #[test]
1399                    fn [<$test_fn _scalar_f64>]() {
1400                        let _ = $test_fn(stringify!([<$test_fn _scalar_f64>]), Kernel::Scalar);
1401                    }
1402                )*
1403                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1404                $(
1405                    #[test]
1406                    fn [<$test_fn _avx2_f64>]() {
1407                        let _ = $test_fn(stringify!([<$test_fn _avx2_f64>]), Kernel::Avx2);
1408                    }
1409                    #[test]
1410                    fn [<$test_fn _avx512_f64>]() {
1411                        let _ = $test_fn(stringify!([<$test_fn _avx512_f64>]), Kernel::Avx512);
1412                    }
1413                )*
1414            }
1415        }
1416    }
1417
1418    #[cfg(debug_assertions)]
1419    fn check_fosc_no_poison(
1420        test_name: &str,
1421        kernel: Kernel,
1422    ) -> Result<(), Box<dyn std::error::Error>> {
1423        skip_if_unsupported!(kernel, test_name);
1424
1425        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1426        let candles = read_candles_from_csv(file_path)?;
1427
1428        let test_params = vec![
1429            FoscParams::default(),
1430            FoscParams { period: Some(2) },
1431            FoscParams { period: Some(3) },
1432            FoscParams { period: Some(5) },
1433            FoscParams { period: Some(7) },
1434            FoscParams { period: Some(10) },
1435            FoscParams { period: Some(14) },
1436            FoscParams { period: Some(20) },
1437            FoscParams { period: Some(30) },
1438            FoscParams { period: Some(50) },
1439            FoscParams { period: Some(100) },
1440            FoscParams { period: Some(200) },
1441        ];
1442
1443        for (param_idx, params) in test_params.iter().enumerate() {
1444            let input = FoscInput::from_candles(&candles, "close", params.clone());
1445            let output = fosc_with_kernel(&input, kernel)?;
1446
1447            for (i, &val) in output.values.iter().enumerate() {
1448                if val.is_nan() {
1449                    continue;
1450                }
1451
1452                let bits = val.to_bits();
1453
1454                if bits == 0x11111111_11111111 {
1455                    panic!(
1456                        "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at index {} \
1457						 with params: period={} (param set {})",
1458                        test_name,
1459                        val,
1460                        bits,
1461                        i,
1462                        params.period.unwrap_or(5),
1463                        param_idx
1464                    );
1465                }
1466
1467                if bits == 0x22222222_22222222 {
1468                    panic!(
1469                        "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at index {} \
1470						 with params: period={} (param set {})",
1471                        test_name,
1472                        val,
1473                        bits,
1474                        i,
1475                        params.period.unwrap_or(5),
1476                        param_idx
1477                    );
1478                }
1479
1480                if bits == 0x33333333_33333333 {
1481                    panic!(
1482                        "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at index {} \
1483						 with params: period={} (param set {})",
1484                        test_name,
1485                        val,
1486                        bits,
1487                        i,
1488                        params.period.unwrap_or(5),
1489                        param_idx
1490                    );
1491                }
1492            }
1493        }
1494
1495        Ok(())
1496    }
1497
1498    #[cfg(not(debug_assertions))]
1499    fn check_fosc_no_poison(
1500        _test_name: &str,
1501        _kernel: Kernel,
1502    ) -> Result<(), Box<dyn std::error::Error>> {
1503        Ok(())
1504    }
1505
1506    #[cfg(feature = "proptest")]
1507    #[allow(clippy::float_cmp)]
1508    fn check_fosc_property(
1509        test_name: &str,
1510        kernel: Kernel,
1511    ) -> Result<(), Box<dyn std::error::Error>> {
1512        use proptest::prelude::*;
1513        skip_if_unsupported!(kernel, test_name);
1514
1515        let strat1 = (2usize..=50).prop_flat_map(|period| {
1516            (
1517                prop::collection::vec(
1518                    (1e-6f64..1e6f64)
1519                        .prop_filter("finite and non-zero", |x| x.is_finite() && x.abs() > 1e-10),
1520                    period..400,
1521                ),
1522                Just(period),
1523            )
1524        });
1525
1526        proptest::test_runner::TestRunner::default().run(&strat1, |(data, period)| {
1527            let params = FoscParams {
1528                period: Some(period),
1529            };
1530            let input = FoscInput::from_slice(&data, params);
1531
1532            let FoscOutput { values: out } = fosc_with_kernel(&input, kernel).unwrap();
1533            let FoscOutput { values: ref_out } = fosc_with_kernel(&input, Kernel::Scalar).unwrap();
1534
1535            prop_assert_eq!(out.len(), data.len());
1536            prop_assert_eq!(ref_out.len(), data.len());
1537
1538            for i in 0..(period - 1) {
1539                prop_assert!(out[i].is_nan(), "Expected NaN at index {} during warmup", i);
1540                prop_assert!(
1541                    ref_out[i].is_nan(),
1542                    "Expected NaN at index {} during warmup (scalar)",
1543                    i
1544                );
1545            }
1546
1547            for i in (period - 1)..data.len() {
1548                let y = out[i];
1549                let r = ref_out[i];
1550
1551                if r.is_nan() {
1552                    prop_assert!(
1553                        y.is_nan(),
1554                        "Kernel mismatch at {}: scalar is NaN but kernel is {}",
1555                        i,
1556                        y
1557                    );
1558                } else if y.is_nan() {
1559                    prop_assert!(
1560                        r.is_nan(),
1561                        "Kernel mismatch at {}: kernel is NaN but scalar is {}",
1562                        i,
1563                        r
1564                    );
1565                } else {
1566                    let diff = (y - r).abs();
1567                    let tolerance = 1e-9 * r.abs().max(1.0);
1568                    prop_assert!(
1569                        diff <= tolerance,
1570                        "Kernel mismatch at {}: {} vs {} (diff: {})",
1571                        i,
1572                        y,
1573                        r,
1574                        diff
1575                    );
1576                }
1577
1578                let y_bits = y.to_bits();
1579                prop_assert_ne!(
1580                    y_bits,
1581                    0x11111111_11111111,
1582                    "Found alloc_with_nan_prefix poison at {}",
1583                    i
1584                );
1585                prop_assert_ne!(
1586                    y_bits,
1587                    0x22222222_22222222,
1588                    "Found init_matrix_prefixes poison at {}",
1589                    i
1590                );
1591                prop_assert_ne!(
1592                    y_bits,
1593                    0x33333333_33333333,
1594                    "Found make_uninit_matrix poison at {}",
1595                    i
1596                );
1597            }
1598
1599            Ok(())
1600        })?;
1601
1602        let strat2 = prop::collection::vec(
1603            (1e-6f64..1e6f64)
1604                .prop_filter("finite and non-zero", |x| x.is_finite() && x.abs() > 1e-10),
1605            10..1000,
1606        );
1607
1608        proptest::test_runner::TestRunner::default().run(&strat2, |data| {
1609            let period = 5;
1610            let params = FoscParams {
1611                period: Some(period),
1612            };
1613            let input = FoscInput::from_slice(&data, params);
1614
1615            let FoscOutput { values: out } = fosc_with_kernel(&input, kernel).unwrap();
1616
1617            for i in (period - 1)..data.len() {
1618                if !out[i].is_nan() {
1619                    prop_assert!(
1620                        out[i] >= -200.0 && out[i] <= 200.0,
1621                        "FOSC value {} at index {} is out of reasonable bounds",
1622                        out[i],
1623                        i
1624                    );
1625                }
1626            }
1627
1628            Ok(())
1629        })?;
1630
1631        let strat3 =
1632            (2usize..=20, 1e-6f64..10f64, 10usize..100).prop_map(|(period, start, len)| {
1633                let data: Vec<f64> = (0..len).map(|i| start + (i as f64) * 0.1).collect();
1634                (data, period)
1635            });
1636
1637        proptest::test_runner::TestRunner::default().run(&strat3, |(data, period)| {
1638            let params = FoscParams {
1639                period: Some(period),
1640            };
1641            let input = FoscInput::from_slice(&data, params);
1642
1643            let FoscOutput { values: out } = fosc_with_kernel(&input, kernel).unwrap();
1644
1645            let start_idx = if period > 0 { period } else { period - 1 };
1646            let valid_fosc: Vec<f64> = out
1647                .iter()
1648                .skip(start_idx)
1649                .filter(|v| !v.is_nan())
1650                .copied()
1651                .collect();
1652
1653            if valid_fosc.len() > 5 {
1654                for &val in &valid_fosc {
1655                    prop_assert!(val.abs() < 5.0, "FOSC {} too large for linear trend", val);
1656                }
1657
1658                let mean: f64 = valid_fosc.iter().sum::<f64>() / valid_fosc.len() as f64;
1659                let variance: f64 = valid_fosc.iter().map(|v| (v - mean).powi(2)).sum::<f64>()
1660                    / valid_fosc.len() as f64;
1661                let std_dev = variance.sqrt();
1662
1663                prop_assert!(
1664                    std_dev < 1.0,
1665                    "FOSC standard deviation {} too high for linear trend",
1666                    std_dev
1667                );
1668            }
1669
1670            Ok(())
1671        })?;
1672
1673        let strat4 = (2usize..=20, 10usize..100).prop_map(|(period, len)| {
1674            let data: Vec<f64> = (0..len)
1675                .map(|i| 100.0 + 10.0 * (i as f64 * 0.5).sin())
1676                .collect();
1677            (data, period)
1678        });
1679
1680        proptest::test_runner::TestRunner::default().run(&strat4, |(data, period)| {
1681            let params = FoscParams {
1682                period: Some(period),
1683            };
1684            let input = FoscInput::from_slice(&data, params);
1685
1686            let FoscOutput { values: out } = fosc_with_kernel(&input, kernel).unwrap();
1687
1688            let valid_values: Vec<f64> = out
1689                .iter()
1690                .skip(period - 1)
1691                .filter(|v| !v.is_nan())
1692                .copied()
1693                .collect();
1694
1695            if valid_values.len() > 10 {
1696                let mean: f64 = valid_values.iter().sum::<f64>() / valid_values.len() as f64;
1697
1698                prop_assert!(
1699                    mean.abs() < 10.0,
1700                    "Mean FOSC {} is too far from zero for oscillating data",
1701                    mean
1702                );
1703            }
1704
1705            Ok(())
1706        })?;
1707
1708        let strat5 = (2usize..=20, 50f64..200f64, 20usize..100).prop_flat_map(
1709            |(period, base_price, len)| {
1710                (
1711                    prop::collection::vec(
1712                        (-0.5f64..0.5f64, 0f64..0.5f64, 0f64..0.5f64).prop_map(
1713                            move |(change, high_diff, low_diff)| {
1714                                base_price * (1.0 + change * 0.01) + high_diff
1715                            },
1716                        ),
1717                        len,
1718                    ),
1719                    Just(period),
1720                )
1721            },
1722        );
1723
1724        proptest::test_runner::TestRunner::default().run(&strat5, |(data, period)| {
1725            let params = FoscParams {
1726                period: Some(period),
1727            };
1728            let input = FoscInput::from_slice(&data, params);
1729
1730            let result = fosc_with_kernel(&input, kernel);
1731            prop_assert!(result.is_ok(), "FOSC failed for OHLC-like data");
1732
1733            let FoscOutput { values: out } = result.unwrap();
1734
1735            if period == 2 {
1736                for i in 1..data.len() {
1737                    if !out[i].is_nan() {
1738                        prop_assert!(
1739                            out[i].abs() < 100.0,
1740                            "FOSC {} at index {} unreasonable for period=2",
1741                            out[i],
1742                            i
1743                        );
1744                    }
1745                }
1746            }
1747
1748            if data.windows(2).all(|w| (w[0] - w[1]).abs() < 1e-10) && data.len() > period {
1749                for i in (period - 1)..data.len() {
1750                    if !out[i].is_nan() {
1751                        prop_assert!(
1752                            out[i].abs() < 1e-6,
1753                            "FOSC {} at index {} should be ~0 for constant data",
1754                            out[i],
1755                            i
1756                        );
1757                    }
1758                }
1759            }
1760
1761            Ok(())
1762        })?;
1763
1764        let strat6 = (2usize..=10, 10usize..50).prop_flat_map(|(period, len)| {
1765            (
1766                prop::collection::vec(
1767                    prop::strategy::Union::new(vec![
1768                        (0.9f64..1.0)
1769                            .prop_map(|p| {
1770                                if p < 0.95 {
1771                                    100.0 + p
1772                                } else if p < 0.98 {
1773                                    1e-12
1774                                } else {
1775                                    0.0
1776                                }
1777                            })
1778                            .boxed(),
1779                        (50f64..150f64).boxed(),
1780                        (-1e-12f64..1e-12f64)
1781                            .prop_filter("not exactly zero", |x| x.abs() > 1e-15)
1782                            .boxed(),
1783                    ]),
1784                    len,
1785                ),
1786                Just(period),
1787            )
1788        });
1789
1790        proptest::test_runner::TestRunner::default().run(&strat6, |(data, period)| {
1791            let params = FoscParams {
1792                period: Some(period),
1793            };
1794            let input = FoscInput::from_slice(&data, params);
1795
1796            let result = fosc_with_kernel(&input, kernel);
1797            prop_assert!(result.is_ok(), "FOSC failed for near-zero data");
1798
1799            let FoscOutput { values: out } = result.unwrap();
1800
1801            for i in (period - 1)..data.len() {
1802                if data[i] == 0.0 {
1803                    prop_assert!(
1804                        out[i].is_nan(),
1805                        "FOSC should be NaN when price is 0.0 at index {}",
1806                        i
1807                    );
1808                } else if data[i].abs() < 1e-10 {
1809                    if !out[i].is_nan() {
1810                        prop_assert!(
1811                            out[i].abs() < 10000.0,
1812                            "FOSC {} unreasonably large for tiny price {} at index {}",
1813                            out[i],
1814                            data[i],
1815                            i
1816                        );
1817                    }
1818                }
1819            }
1820
1821            Ok(())
1822        })?;
1823
1824        Ok(())
1825    }
1826
1827    generate_all_fosc_tests!(
1828        check_fosc_partial_params,
1829        check_fosc_basic_accuracy,
1830        check_fosc_with_nan_data,
1831        check_fosc_zero_period,
1832        check_fosc_period_exceeds_length,
1833        check_fosc_very_small_dataset,
1834        check_fosc_all_values_nan,
1835        check_fosc_expected_values_reference,
1836        check_fosc_no_poison
1837    );
1838
1839    #[cfg(feature = "proptest")]
1840    generate_all_fosc_tests!(check_fosc_property);
1841
1842    fn check_batch_default_row(
1843        test: &str,
1844        kernel: Kernel,
1845    ) -> Result<(), Box<dyn std::error::Error>> {
1846        skip_if_unsupported!(kernel, test);
1847        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1848        let c = read_candles_from_csv(file)?;
1849        let output = FoscBatchBuilder::new()
1850            .kernel(kernel)
1851            .apply_candles(&c, "close")?;
1852        let def = FoscParams::default();
1853        let row = output.values_for(&def).expect("default row missing");
1854        assert_eq!(row.len(), c.close.len());
1855        let expected = [
1856            -0.8904444627923475,
1857            -0.4763353099245297,
1858            -0.2379782851444668,
1859            0.292790128025632,
1860            -0.6597902988090389,
1861        ];
1862        let start = row.len() - 5;
1863        for (i, &v) in row[start..].iter().enumerate() {
1864            assert!(
1865                (v - expected[i]).abs() < 1e-7,
1866                "[{test}] default-row mismatch at idx {i}: {v} vs {expected:?}"
1867            );
1868        }
1869        Ok(())
1870    }
1871
1872    macro_rules! gen_batch_tests {
1873        ($fn_name:ident) => {
1874            paste::paste! {
1875                #[test] fn [<$fn_name _scalar>]()      {
1876                    let _ = $fn_name(stringify!([<$fn_name _scalar>]), Kernel::ScalarBatch);
1877                }
1878                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1879                #[test] fn [<$fn_name _avx2>]()        {
1880                    let _ = $fn_name(stringify!([<$fn_name _avx2>]), Kernel::Avx2Batch);
1881                }
1882                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1883                #[test] fn [<$fn_name _avx512>]()      {
1884                    let _ = $fn_name(stringify!([<$fn_name _avx512>]), Kernel::Avx512Batch);
1885                }
1886                #[test] fn [<$fn_name _auto_detect>]() {
1887                    let _ = $fn_name(stringify!([<$fn_name _auto_detect>]),
1888                                     Kernel::Auto);
1889                }
1890            }
1891        };
1892    }
1893    fn check_batch_sweep(test: &str, kernel: Kernel) -> Result<(), Box<dyn std::error::Error>> {
1894        skip_if_unsupported!(kernel, test);
1895
1896        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1897        let c = read_candles_from_csv(file)?;
1898
1899        let output = FoscBatchBuilder::new()
1900            .kernel(kernel)
1901            .period_range(5, 25, 5)
1902            .apply_candles(&c, "close")?;
1903
1904        let expected_combos = 5;
1905        assert_eq!(output.combos.len(), expected_combos);
1906        assert_eq!(output.rows, expected_combos);
1907        assert_eq!(output.cols, c.close.len());
1908
1909        Ok(())
1910    }
1911
1912    #[cfg(debug_assertions)]
1913    fn check_batch_no_poison(test: &str, kernel: Kernel) -> Result<(), Box<dyn std::error::Error>> {
1914        skip_if_unsupported!(kernel, test);
1915
1916        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1917        let c = read_candles_from_csv(file)?;
1918
1919        let test_configs = vec![
1920            (2, 10, 2),
1921            (5, 25, 5),
1922            (30, 60, 15),
1923            (2, 5, 1),
1924            (10, 20, 2),
1925            (5, 5, 0),
1926            (5, 50, 15),
1927            (100, 200, 50),
1928        ];
1929
1930        for (cfg_idx, &(p_start, p_end, p_step)) in test_configs.iter().enumerate() {
1931            let output = FoscBatchBuilder::new()
1932                .kernel(kernel)
1933                .period_range(p_start, p_end, p_step)
1934                .apply_candles(&c, "close")?;
1935
1936            for (idx, &val) in output.values.iter().enumerate() {
1937                if val.is_nan() {
1938                    continue;
1939                }
1940
1941                let bits = val.to_bits();
1942                let row = idx / output.cols;
1943                let col = idx % output.cols;
1944                let combo = &output.combos[row];
1945
1946                if bits == 0x11111111_11111111 {
1947                    panic!(
1948                        "[{}] Config {}: Found alloc_with_nan_prefix poison value {} (0x{:016X}) \
1949						 at row {} col {} (flat index {}) with params: period={}",
1950                        test,
1951                        cfg_idx,
1952                        val,
1953                        bits,
1954                        row,
1955                        col,
1956                        idx,
1957                        combo.period.unwrap_or(5)
1958                    );
1959                }
1960
1961                if bits == 0x22222222_22222222 {
1962                    panic!(
1963                        "[{}] Config {}: Found init_matrix_prefixes poison value {} (0x{:016X}) \
1964						 at row {} col {} (flat index {}) with params: period={}",
1965                        test,
1966                        cfg_idx,
1967                        val,
1968                        bits,
1969                        row,
1970                        col,
1971                        idx,
1972                        combo.period.unwrap_or(5)
1973                    );
1974                }
1975
1976                if bits == 0x33333333_33333333 {
1977                    panic!(
1978                        "[{}] Config {}: Found make_uninit_matrix poison value {} (0x{:016X}) \
1979						 at row {} col {} (flat index {}) with params: period={}",
1980                        test,
1981                        cfg_idx,
1982                        val,
1983                        bits,
1984                        row,
1985                        col,
1986                        idx,
1987                        combo.period.unwrap_or(5)
1988                    );
1989                }
1990            }
1991        }
1992
1993        Ok(())
1994    }
1995
1996    #[cfg(not(debug_assertions))]
1997    fn check_batch_no_poison(
1998        _test: &str,
1999        _kernel: Kernel,
2000    ) -> Result<(), Box<dyn std::error::Error>> {
2001        Ok(())
2002    }
2003
2004    gen_batch_tests!(check_batch_default_row);
2005    gen_batch_tests!(check_batch_sweep);
2006    gen_batch_tests!(check_batch_no_poison);
2007
2008    #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
2009    #[test]
2010    fn test_fosc_into_matches_api() -> Result<(), Box<dyn std::error::Error>> {
2011        let n = 512usize;
2012        let mut data = Vec::with_capacity(n);
2013        for i in 0..n {
2014            let x = i as f64;
2015
2016            data.push(50.0 + 0.05 * x + (0.1 * x).sin() * 2.0);
2017        }
2018
2019        let input = FoscInput::from_slice(&data, FoscParams::default());
2020
2021        let baseline = fosc_with_kernel(&input, Kernel::Auto)?.values;
2022
2023        let mut out = vec![0.0; data.len()];
2024        fosc_into(&input, &mut out)?;
2025
2026        assert_eq!(baseline.len(), out.len());
2027
2028        #[inline]
2029        fn eq_or_both_nan(a: f64, b: f64) -> bool {
2030            (a.is_nan() && b.is_nan()) || (a == b)
2031        }
2032
2033        for i in 0..out.len() {
2034            assert!(
2035                eq_or_both_nan(baseline[i], out[i]),
2036                "mismatch at {}: baseline={} out={}",
2037                i,
2038                baseline[i],
2039                out[i]
2040            );
2041        }
2042
2043        Ok(())
2044    }
2045}