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}