Skip to main content

vector_ta/indicators/
rocp.rs

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