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
10#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
11use serde::{Deserialize, Serialize};
12#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
13use wasm_bindgen::prelude::*;
14
15use crate::utilities::data_loader::{source_type, Candles};
16use crate::utilities::enums::Kernel;
17use crate::utilities::helpers::{
18 alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
19 make_uninit_matrix,
20};
21#[cfg(feature = "python")]
22use crate::utilities::kernel_validation::validate_kernel;
23use aligned_vec::{AVec, CACHELINE_ALIGN};
24
25#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
26use core::arch::x86_64::*;
27
28#[cfg(not(target_arch = "wasm32"))]
29use rayon::prelude::*;
30
31use std::convert::AsRef;
32use std::error::Error;
33use std::mem::MaybeUninit;
34use thiserror::Error;
35
36#[derive(Debug, Clone)]
37pub enum PrbData<'a> {
38 Candles {
39 candles: &'a Candles,
40 source: &'a str,
41 },
42 Slice(&'a [f64]),
43}
44
45#[derive(Debug, Clone)]
46pub struct PrbOutput {
47 pub values: Vec<f64>,
48 pub upper_band: Vec<f64>,
49 pub lower_band: Vec<f64>,
50}
51
52#[derive(Debug, Clone)]
53#[cfg_attr(
54 all(target_arch = "wasm32", feature = "wasm"),
55 derive(Serialize, Deserialize)
56)]
57pub struct PrbParams {
58 pub smooth_data: Option<bool>,
59 pub smooth_period: Option<usize>,
60 pub regression_period: Option<usize>,
61 pub polynomial_order: Option<usize>,
62 pub regression_offset: Option<i32>,
63 pub ndev: Option<f64>,
64 pub equ_from: Option<usize>,
65}
66
67impl Default for PrbParams {
68 fn default() -> Self {
69 Self {
70 smooth_data: Some(true),
71 smooth_period: Some(10),
72 regression_period: Some(100),
73 polynomial_order: Some(2),
74 regression_offset: Some(0),
75 ndev: Some(2.0),
76 equ_from: Some(0),
77 }
78 }
79}
80
81#[derive(Debug, Clone)]
82pub struct PrbInput<'a> {
83 pub data: PrbData<'a>,
84 pub params: PrbParams,
85}
86
87impl<'a> AsRef<[f64]> for PrbInput<'a> {
88 fn as_ref(&self) -> &[f64] {
89 match &self.data {
90 PrbData::Candles { candles, source } => source_type(candles, source),
91 PrbData::Slice(slice) => slice,
92 }
93 }
94}
95
96impl<'a> PrbInput<'a> {
97 #[inline]
98 pub fn from_candles(c: &'a Candles, s: &'a str, p: PrbParams) -> Self {
99 Self {
100 data: PrbData::Candles {
101 candles: c,
102 source: s,
103 },
104 params: p,
105 }
106 }
107
108 #[inline]
109 pub fn from_slice(sl: &'a [f64], p: PrbParams) -> Self {
110 Self {
111 data: PrbData::Slice(sl),
112 params: p,
113 }
114 }
115
116 #[inline]
117 pub fn with_default_candles(c: &'a Candles) -> Self {
118 Self::from_candles(c, "close", PrbParams::default())
119 }
120
121 #[inline]
122 pub fn get_smooth_data(&self) -> bool {
123 self.params.smooth_data.unwrap_or(true)
124 }
125
126 #[inline]
127 pub fn get_smooth_period(&self) -> usize {
128 self.params.smooth_period.unwrap_or(10)
129 }
130
131 #[inline]
132 pub fn get_regression_period(&self) -> usize {
133 self.params.regression_period.unwrap_or(100)
134 }
135
136 #[inline]
137 pub fn get_polynomial_order(&self) -> usize {
138 self.params.polynomial_order.unwrap_or(2)
139 }
140
141 #[inline]
142 pub fn get_regression_offset(&self) -> i32 {
143 self.params.regression_offset.unwrap_or(0)
144 }
145
146 pub fn get_ndev(&self) -> f64 {
147 self.params.ndev.unwrap_or(2.0)
148 }
149
150 #[inline]
151 pub fn get_equ_from(&self) -> usize {
152 self.params.equ_from.unwrap_or(0)
153 }
154}
155
156#[derive(Copy, Clone, Debug)]
157pub struct PrbBuilder {
158 smooth_data: Option<bool>,
159 smooth_period: Option<usize>,
160 regression_period: Option<usize>,
161 polynomial_order: Option<usize>,
162 regression_offset: Option<i32>,
163 ndev: Option<f64>,
164 equ_from: Option<usize>,
165 kernel: Kernel,
166}
167
168impl Default for PrbBuilder {
169 fn default() -> Self {
170 Self {
171 smooth_data: None,
172 smooth_period: None,
173 regression_period: None,
174 polynomial_order: None,
175 regression_offset: None,
176 ndev: None,
177 equ_from: None,
178 kernel: Kernel::Auto,
179 }
180 }
181}
182
183impl PrbBuilder {
184 #[inline(always)]
185 pub fn new() -> Self {
186 Self::default()
187 }
188
189 #[inline(always)]
190 pub fn smooth_data(mut self, s: bool) -> Self {
191 self.smooth_data = Some(s);
192 self
193 }
194
195 #[inline(always)]
196 pub fn smooth_period(mut self, p: usize) -> Self {
197 self.smooth_period = Some(p);
198 self
199 }
200
201 #[inline(always)]
202 pub fn regression_period(mut self, p: usize) -> Self {
203 self.regression_period = Some(p);
204 self
205 }
206
207 #[inline(always)]
208 pub fn polynomial_order(mut self, o: usize) -> Self {
209 self.polynomial_order = Some(o);
210 self
211 }
212
213 #[inline(always)]
214 pub fn regression_offset(mut self, o: i32) -> Self {
215 self.regression_offset = Some(o);
216 self
217 }
218
219 #[inline(always)]
220 pub fn ndev(mut self, n: f64) -> Self {
221 self.ndev = Some(n);
222 self
223 }
224
225 #[inline(always)]
226 pub fn equ_from(mut self, e: usize) -> Self {
227 self.equ_from = Some(e);
228 self
229 }
230
231 #[inline(always)]
232 pub fn kernel(mut self, k: Kernel) -> Self {
233 self.kernel = k;
234 self
235 }
236
237 #[inline(always)]
238 pub fn apply(self, c: &Candles) -> Result<PrbOutput, PrbError> {
239 let p = PrbParams {
240 smooth_data: self.smooth_data,
241 smooth_period: self.smooth_period,
242 regression_period: self.regression_period,
243 polynomial_order: self.polynomial_order,
244 regression_offset: self.regression_offset,
245 ndev: self.ndev,
246 equ_from: self.equ_from,
247 };
248 let i = PrbInput::from_candles(c, "close", p);
249 prb_with_kernel(&i, self.kernel)
250 }
251
252 #[inline(always)]
253 pub fn apply_slice(self, d: &[f64]) -> Result<PrbOutput, PrbError> {
254 let p = PrbParams {
255 smooth_data: self.smooth_data,
256 smooth_period: self.smooth_period,
257 regression_period: self.regression_period,
258 polynomial_order: self.polynomial_order,
259 regression_offset: self.regression_offset,
260 ndev: self.ndev,
261 equ_from: self.equ_from,
262 };
263 let i = PrbInput::from_slice(d, p);
264 prb_with_kernel(&i, self.kernel)
265 }
266
267 #[inline(always)]
268 pub fn into_stream(self) -> Result<PrbStream, PrbError> {
269 let p = PrbParams {
270 smooth_data: self.smooth_data,
271 smooth_period: self.smooth_period,
272 regression_period: self.regression_period,
273 polynomial_order: self.polynomial_order,
274 regression_offset: self.regression_offset,
275 ndev: self.ndev,
276 equ_from: self.equ_from,
277 };
278 PrbStream::try_new(p)
279 }
280}
281
282#[derive(Debug, Error)]
283pub enum PrbError {
284 #[error("prb: Input data slice is empty.")]
285 EmptyInputData,
286
287 #[error("prb: All values are NaN.")]
288 AllValuesNaN,
289
290 #[error("prb: Invalid period: period = {period}, data length = {data_len}")]
291 InvalidPeriod { period: usize, data_len: usize },
292
293 #[error("prb: Not enough valid data: needed = {needed}, valid = {valid}")]
294 NotEnoughValidData { needed: usize, valid: usize },
295
296 #[error("prb: Invalid polynomial order: {order} (must be >= 1)")]
297 InvalidOrder { order: usize },
298
299 #[error("prb: Invalid smooth period: {period} (must be >= 2)")]
300 InvalidSmoothPeriod { period: usize },
301
302 #[error("prb: Matrix is singular and cannot be decomposed")]
303 SingularMatrix,
304
305 #[error("prb: Output length mismatch: expected {expected}, got {got}")]
306 OutputLengthMismatch { expected: usize, got: usize },
307
308 #[error("prb: Invalid range: start={start}, end={end}, step={step}")]
309 InvalidRange {
310 start: String,
311 end: String,
312 step: String,
313 },
314
315 #[error("prb: Invalid kernel for batch: {0:?}")]
316 InvalidKernelForBatch(crate::utilities::enums::Kernel),
317}
318
319pub struct PrbStream {
320 smooth_data: bool,
321 smooth_period: usize,
322 regression_period: usize,
323 polynomial_order: usize,
324 regression_offset: i32,
325 equ_from: usize,
326 ndev: f64,
327
328 ssf_y1: f64,
329 ssf_y2: f64,
330 ssf_c1: f64,
331 ssf_c2: f64,
332 ssf_c3: f64,
333
334 ring: Vec<f64>,
335 head: usize,
336 count: usize,
337
338 sum: f64,
339 sumsq: f64,
340
341 m: usize,
342 l: Vec<f64>,
343 u: Vec<f64>,
344 binom: Vec<f64>,
345 n_pow: Vec<f64>,
346
347 moments: Vec<f64>,
348 moments_prev: Vec<f64>,
349
350 tmp_y: Vec<f64>,
351 coeffs: Vec<f64>,
352
353 x_pos: f64,
354 inv_n: f64,
355}
356
357impl PrbStream {
358 pub fn try_new(params: PrbParams) -> Result<Self, PrbError> {
359 let smooth_data = params.smooth_data.unwrap_or(true);
360 let smooth_period = params.smooth_period.unwrap_or(10);
361 let n = params.regression_period.unwrap_or(100);
362 let k = params.polynomial_order.unwrap_or(2);
363 let regression_offset = params.regression_offset.unwrap_or(0);
364 let ndev = params.ndev.unwrap_or(2.0);
365 let equ_from = params.equ_from.unwrap_or(0);
366
367 if k < 1 {
368 return Err(PrbError::InvalidOrder { order: k });
369 }
370 if smooth_data && smooth_period < 2 {
371 return Err(PrbError::InvalidSmoothPeriod {
372 period: smooth_period,
373 });
374 }
375 if n == 0 {
376 return Err(PrbError::InvalidPeriod {
377 period: n,
378 data_len: 0,
379 });
380 }
381
382 let pi = core::f64::consts::PI;
383 let omega = 2.0 * pi / (smooth_period as f64);
384 let a = (-core::f64::consts::SQRT_2 * pi / (smooth_period as f64)).exp();
385 let b = 2.0 * a * ((core::f64::consts::SQRT_2 / 2.0) * omega).cos();
386 let c3 = -a * a;
387 let c2 = b;
388 let c1 = 1.0 - c2 - c3;
389
390 let pre = build_fixed_design(n, k)?;
391
392 let m = k + 1;
393 let x_pos = (n as f64) - (regression_offset as f64) + (equ_from as f64);
394 let inv_n = 1.0 / (n as f64);
395
396 Ok(Self {
397 smooth_data,
398 smooth_period,
399 regression_period: n,
400 polynomial_order: k,
401 regression_offset,
402 equ_from,
403 ndev,
404
405 ssf_y1: f64::NAN,
406 ssf_y2: f64::NAN,
407 ssf_c1: c1,
408 ssf_c2: c2,
409 ssf_c3: c3,
410
411 ring: vec![0.0; n],
412 head: 0,
413 count: 0,
414
415 sum: 0.0,
416 sumsq: 0.0,
417
418 m,
419 l: pre.l,
420 u: pre.u,
421 binom: pre.binom,
422 n_pow: pre.n_pow,
423
424 moments: vec![0.0; m],
425 moments_prev: vec![0.0; m],
426 tmp_y: vec![0.0; m],
427 coeffs: vec![0.0; m],
428
429 x_pos,
430 inv_n,
431 })
432 }
433
434 #[inline]
435 fn ssf_step(&mut self, x: f64) -> f64 {
436 if !self.smooth_data {
437 return x;
438 }
439
440 let prev1 = if self.ssf_y1.is_nan() { x } else { self.ssf_y1 };
441 let prev2 = if self.ssf_y2.is_nan() {
442 prev1
443 } else {
444 self.ssf_y2
445 };
446 let y = self.ssf_c1 * x + self.ssf_c2 * prev1 + self.ssf_c3 * prev2;
447 self.ssf_y2 = self.ssf_y1;
448 self.ssf_y1 = y;
449 y
450 }
451
452 #[inline]
453 fn reset_after_nan(&mut self) {
454 self.head = 0;
455 self.count = 0;
456 self.sum = 0.0;
457 self.sumsq = 0.0;
458 for v in &mut self.ring {
459 *v = 0.0;
460 }
461 self.moments.fill(0.0);
462 self.moments_prev.fill(0.0);
463 self.tmp_y.fill(0.0);
464 self.coeffs.fill(0.0);
465 self.ssf_y1 = f64::NAN;
466 self.ssf_y2 = f64::NAN;
467 }
468
469 pub fn update(&mut self, value: f64) -> Option<(f64, f64, f64)> {
470 if value.is_nan() {
471 self.reset_after_nan();
472 return None;
473 }
474
475 let y_new = self.ssf_step(value);
476 let n = self.regression_period;
477 let k = self.polynomial_order;
478 let m = self.m;
479
480 if self.count < n {
481 let j = (self.count + 1) as f64;
482
483 self.moments[0] += y_new;
484
485 let mut p = j;
486 for r in 1..=k {
487 self.moments[r] += y_new * p;
488 p *= j;
489 }
490
491 self.ring[self.head] = y_new;
492 self.head = (self.head + 1) % n;
493 self.count += 1;
494 self.sum += y_new;
495 self.sumsq += y_new * y_new;
496
497 if self.count < n + self.equ_from {
498 return None;
499 }
500 return Some(self.solve_eval_and_band());
501 }
502
503 let y_old = self.ring[self.head];
504 self.ring[self.head] = y_new;
505 self.head = (self.head + 1) % n;
506
507 self.sum += y_new - y_old;
508 self.sumsq += y_new * y_new - y_old * y_old;
509
510 self.moments_prev.copy_from_slice(&self.moments);
511 self.moments[0] = self.moments_prev[0] - y_old + y_new;
512 for r in 1..=k {
513 let row = r * m;
514 let mut acc = 0.0;
515 for mm in 0..=r {
516 let sign = if ((r - mm) & 1) == 1 { -1.0 } else { 1.0 };
517 acc += sign * self.binom[row + mm] * self.moments_prev[mm];
518 }
519 self.moments[r] = acc + self.n_pow[r] * y_new;
520 }
521
522 Some(self.solve_eval_and_band())
523 }
524
525 #[inline(always)]
526 fn solve_eval_and_band(&mut self) -> (f64, f64, f64) {
527 let m = self.m;
528 for r in 0..m {
529 let row = r * m;
530 let mut acc = self.moments[r];
531 for c in 0..r {
532 acc -= self.l[row + c] * self.tmp_y[c];
533 }
534 let diag = self.l[row + r];
535 self.tmp_y[r] = acc / diag;
536 }
537
538 for r in (0..m).rev() {
539 let row = r * m;
540 let mut acc = self.tmp_y[r];
541 for c in (r + 1)..m {
542 acc -= self.u[row + c] * self.coeffs[c];
543 }
544 self.coeffs[r] = acc / self.u[row + r];
545 }
546
547 let mut reg = 0.0f64;
548 for p in (0..m).rev() {
549 reg = reg.mul_add(self.x_pos, self.coeffs[p]);
550 }
551
552 let mean = self.sum * self.inv_n;
553 let var = (self.sumsq * self.inv_n) - mean * mean;
554 let stdev = if var > 0.0 { var.sqrt() } else { 0.0 };
555 let upper = reg + self.ndev * stdev;
556 let lower = reg - self.ndev * stdev;
557 (reg, upper, lower)
558 }
559}
560
561#[inline]
562fn ssf_filter(data: &[f64], period: usize, first: usize) -> Vec<f64> {
563 let len = data.len();
564 if len == 0 {
565 return Vec::new();
566 }
567
568 let mut out = alloc_with_nan_prefix(len, first);
569
570 let pi = std::f64::consts::PI;
571 let omega = 2.0 * pi / (period as f64);
572 let a = (-std::f64::consts::SQRT_2 * pi / (period as f64)).exp();
573 let b = 2.0 * a * ((std::f64::consts::SQRT_2 / 2.0) * omega).cos();
574 let c3 = -a * a;
575 let c2 = b;
576 let c1 = 1.0 - c2 - c3;
577
578 let mut y1 = f64::NAN;
579 let mut y2 = f64::NAN;
580
581 for i in first..len {
582 let prev1 = if y1.is_nan() { data[i] } else { y1 };
583 let prev2 = if y2.is_nan() { prev1 } else { y2 };
584 let y = c1 * data[i] + c2 * prev1 + c3 * prev2;
585 out[i] = y;
586 y2 = y1;
587 y1 = y;
588 }
589 out
590}
591
592fn lu_decomposition(matrix: &[f64], size: usize) -> Result<(Vec<f64>, Vec<f64>), PrbError> {
593 let mut l = vec![0.0; size * size];
594 let mut u = vec![0.0; size * size];
595
596 for j in 0..size {
597 u[j] = matrix[j];
598 }
599
600 if u[0].abs() < 1e-10 {
601 return Err(PrbError::SingularMatrix);
602 }
603
604 for i in 1..size {
605 l[i * size] = matrix[i * size] / u[0];
606 }
607
608 for i in 0..size {
609 l[i * size + i] = 1.0;
610 }
611
612 for i in 1..size {
613 for j in i..size {
614 let mut sum = 0.0;
615 for k in 0..i {
616 sum += l[i * size + k] * u[k * size + j];
617 }
618 u[i * size + j] = matrix[i * size + j] - sum;
619
620 if j > i {
621 let mut sum = 0.0;
622 for k in 0..i {
623 sum += l[j * size + k] * u[k * size + i];
624 }
625 if u[i * size + i].abs() < 1e-10 {
626 return Err(PrbError::SingularMatrix);
627 }
628 l[j * size + i] = (matrix[j * size + i] - sum) / u[i * size + i];
629 }
630 }
631 }
632
633 Ok((l, u))
634}
635
636fn forward_substitution(l: &[f64], b: &[f64], size: usize) -> Vec<f64> {
637 let mut y = vec![0.0; size];
638
639 for i in 0..size {
640 let mut sum = b[i];
641 for j in 0..i {
642 sum -= l[i * size + j] * y[j];
643 }
644 y[i] = sum / l[i * size + i];
645 }
646
647 y
648}
649
650fn backward_substitution(u: &[f64], y: &[f64], size: usize) -> Vec<f64> {
651 let mut x = vec![0.0; size];
652
653 for i in (0..size).rev() {
654 let mut sum = y[i];
655 for j in (i + 1)..size {
656 sum -= u[i * size + j] * x[j];
657 }
658 x[i] = sum / u[i * size + i];
659 }
660
661 x
662}
663
664struct PrbWorkspace {
665 x_power_sums: Vec<f64>,
666 xy_sums: Vec<f64>,
667 matrix: Vec<f64>,
668 l: Vec<f64>,
669 u: Vec<f64>,
670 y: Vec<f64>,
671 coeffs: Vec<f64>,
672 x_vals: Vec<f64>,
673}
674
675impl PrbWorkspace {
676 fn ensure(&mut self, order: usize, reg_p: usize) {
677 let ms = order + 1;
678 let sq = ms * ms;
679 if self.x_power_sums.len() < 2 * order + 1 {
680 self.x_power_sums.resize(2 * order + 1, 0.0);
681 }
682 if self.xy_sums.len() < ms {
683 self.xy_sums.resize(ms, 0.0);
684 }
685 if self.matrix.len() < sq {
686 self.matrix.resize(sq, 0.0);
687 }
688 if self.l.len() < sq {
689 self.l.resize(sq, 0.0);
690 }
691 if self.u.len() < sq {
692 self.u.resize(sq, 0.0);
693 }
694 if self.y.len() < ms {
695 self.y.resize(ms, 0.0);
696 }
697 if self.coeffs.len() < ms {
698 self.coeffs.resize(ms, 0.0);
699 }
700 if self.x_vals.len() < reg_p {
701 self.x_vals.resize(reg_p, 0.0);
702 for i in 0..reg_p {
703 self.x_vals[i] = (i + 1) as f64;
704 }
705 }
706 }
707}
708
709#[inline]
710fn poly_coeffs_into(
711 x_vals: &[f64],
712 y_window: &[f64],
713 order: usize,
714 x_power_sums: &mut [f64],
715 xy_sums: &mut [f64],
716 matrix: &mut [f64],
717 l: &mut [f64],
718 u: &mut [f64],
719 y: &mut [f64],
720 coeffs: &mut [f64],
721) -> Result<(), PrbError> {
722 let n = x_vals.len();
723 let m = order + 1;
724
725 for p in 0..=(2 * order) {
726 let mut s = 0.0;
727 for i in 0..n {
728 s += x_vals[i].powi(p as i32);
729 }
730 x_power_sums[p] = s;
731 }
732
733 for p in 0..=order {
734 let mut s = 0.0;
735 for i in 0..n {
736 s += x_vals[i].powi(p as i32) * y_window[i];
737 }
738 xy_sums[p] = s;
739 }
740
741 for i in 0..m {
742 for j in 0..m {
743 matrix[i * m + j] = x_power_sums[i + j];
744 }
745 }
746
747 let (l2, u2) = lu_decomposition(matrix, m)?;
748 l[..m * m].copy_from_slice(&l2);
749 u[..m * m].copy_from_slice(&u2);
750
751 for i in 0..m {
752 y[i] = xy_sums[i];
753 }
754 let yy = forward_substitution(l, y, m);
755 let xx = backward_substitution(u, &yy, m);
756 coeffs[..m].copy_from_slice(&xx);
757 Ok(())
758}
759
760fn calculate_regression_coefficients(
761 x_vals: &[f64],
762 y_vals: &[f64],
763 order: usize,
764) -> Result<Vec<f64>, PrbError> {
765 let n = x_vals.len();
766 let matrix_size = order + 1;
767
768 let mut x_power_sums = vec![0.0; 2 * order + 1];
769 for p in 0..=(2 * order) {
770 let mut sum = 0.0;
771 for i in 0..n {
772 sum += x_vals[i].powi(p as i32);
773 }
774 x_power_sums[p] = sum;
775 }
776
777 let mut xy_sums = vec![0.0; order + 1];
778 for p in 0..=order {
779 let mut sum = 0.0;
780 for i in 0..n {
781 sum += x_vals[i].powi(p as i32) * y_vals[i];
782 }
783 xy_sums[p] = sum;
784 }
785
786 let mut matrix = vec![0.0; matrix_size * matrix_size];
787 for i in 0..matrix_size {
788 for j in 0..matrix_size {
789 matrix[i * matrix_size + j] = x_power_sums[i + j];
790 }
791 }
792
793 let (l, u) = lu_decomposition(&matrix, matrix_size)?;
794 let y = forward_substitution(&l, &xy_sums, matrix_size);
795 let coefficients = backward_substitution(&u, &y, matrix_size);
796
797 Ok(coefficients)
798}
799
800#[inline]
801fn evaluate_polynomial(coefficients: &[f64], x: f64) -> f64 {
802 let mut result = 0.0;
803 for (i, &coef) in coefficients.iter().enumerate() {
804 result += coef * x.powi(i as i32);
805 }
806 result
807}
808
809#[inline(always)]
810fn prb_compute_into(
811 data: &[f64],
812 smooth_data: bool,
813 smooth_period: usize,
814 regression_period: usize,
815 polynomial_order: usize,
816 regression_offset: i32,
817 ndev: f64,
818 equ_from: usize,
819 first: usize,
820 kernel: Kernel,
821 out: &mut [f64],
822 out_upper: &mut [f64],
823 out_lower: &mut [f64],
824) -> Result<(), PrbError> {
825 #[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
826 {
827 if matches!(kernel, Kernel::Scalar | Kernel::ScalarBatch) {
828 unsafe {
829 prb_simd128(
830 data,
831 smooth_data,
832 smooth_period,
833 regression_period,
834 polynomial_order,
835 regression_offset,
836 ndev,
837 equ_from,
838 first,
839 out,
840 out_upper,
841 out_lower,
842 )?;
843 }
844 return Ok(());
845 }
846 }
847
848 match kernel {
849 Kernel::Scalar | Kernel::ScalarBatch => prb_scalar(
850 data,
851 smooth_data,
852 smooth_period,
853 regression_period,
854 polynomial_order,
855 regression_offset,
856 ndev,
857 equ_from,
858 first,
859 out,
860 out_upper,
861 out_lower,
862 )?,
863 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
864 Kernel::Avx2 | Kernel::Avx2Batch => unsafe {
865 prb_avx2(
866 data,
867 smooth_data,
868 smooth_period,
869 regression_period,
870 polynomial_order,
871 regression_offset,
872 ndev,
873 equ_from,
874 first,
875 out,
876 out_upper,
877 out_lower,
878 )?
879 },
880 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
881 Kernel::Avx512 | Kernel::Avx512Batch => unsafe {
882 prb_avx512(
883 data,
884 smooth_data,
885 smooth_period,
886 regression_period,
887 polynomial_order,
888 regression_offset,
889 ndev,
890 equ_from,
891 first,
892 out,
893 out_upper,
894 out_lower,
895 )?
896 },
897 #[cfg(not(all(feature = "nightly-avx", target_arch = "x86_64")))]
898 Kernel::Avx2 | Kernel::Avx2Batch | Kernel::Avx512 | Kernel::Avx512Batch => prb_scalar(
899 data,
900 smooth_data,
901 smooth_period,
902 regression_period,
903 polynomial_order,
904 regression_offset,
905 ndev,
906 equ_from,
907 first,
908 out,
909 out_upper,
910 out_lower,
911 )?,
912 _ => unreachable!(),
913 }
914 Ok(())
915}
916
917#[inline]
918fn prb_scalar(
919 data: &[f64],
920 smooth_data: bool,
921 smooth_period: usize,
922 regression_period: usize,
923 polynomial_order: usize,
924 regression_offset: i32,
925 ndev: f64,
926 equ_from: usize,
927 first: usize,
928 out: &mut [f64],
929 out_upper: &mut [f64],
930 out_lower: &mut [f64],
931) -> Result<(), PrbError> {
932 let len = data.len();
933 if len == 0 {
934 return Err(PrbError::EmptyInputData);
935 }
936
937 let smoothed_buf;
938 let smoothed: &[f64] = if smooth_data {
939 smoothed_buf = ssf_filter(data, smooth_period, first);
940 &smoothed_buf
941 } else {
942 data
943 };
944
945 let n = regression_period;
946 let k = polynomial_order;
947 let m = k + 1;
948 let n_f = n as f64;
949
950 let warmup = first + n - 1 + equ_from;
951 if warmup >= len {
952 return Err(PrbError::NotEnoughValidData {
953 needed: n,
954 valid: len.saturating_sub(first),
955 });
956 }
957 let x_pos = n_f - (regression_offset as f64) + (equ_from as f64);
958
959 let max_pow = 2 * k;
960 let mut sx = vec![0.0f64; max_pow + 1];
961 for j in 1..=n {
962 let jf = j as f64;
963 let mut pwr = 1.0;
964
965 sx[0] += 1.0;
966
967 for p in 1..=max_pow {
968 pwr *= jf;
969 sx[p] += pwr;
970 }
971 }
972
973 let mut a = vec![0.0f64; m * m];
974 for i in 0..m {
975 for j in 0..m {
976 a[i * m + j] = sx[i + j];
977 }
978 }
979
980 let (l, u) = lu_decomposition(&a, m)?;
981
982 let stride = m;
983 let mut binom = vec![0.0f64; stride * stride];
984 for r in 0..=k {
985 let r_off = r * stride;
986 binom[r_off + 0] = 1.0;
987 binom[r_off + r] = 1.0;
988 for c in 1..r {
989 let prev = (r - 1) * stride;
990 binom[r_off + c] = binom[prev + (c - 1)] + binom[prev + c];
991 }
992 }
993 let mut n_pow = vec![0.0f64; m];
994 n_pow[0] = 1.0;
995 for r in 1..=k {
996 n_pow[r] = n_pow[r - 1] * n_f;
997 }
998
999 let mut start = warmup + 1 - n - equ_from;
1000 let mut s_xy = vec![0.0f64; m];
1001 let mut sum = 0.0f64;
1002 let mut sumsq = 0.0f64;
1003 {
1004 let y_win = &smoothed[start..start + n];
1005 for (idx, &y) in y_win.iter().enumerate() {
1006 sum += y;
1007 sumsq += y * y;
1008
1009 let jf = (idx as f64) + 1.0;
1010 s_xy[0] += y;
1011 let mut w = jf;
1012 for p in 1..=k {
1013 s_xy[p] = y.mul_add(w, s_xy[p]);
1014 w *= jf;
1015 }
1016 }
1017 }
1018
1019 let mut tmp_y = vec![0.0f64; m];
1020 let mut coeffs = vec![0.0f64; m];
1021 let mut s_prev = vec![0.0f64; m];
1022 let inv_n = 1.0 / n_f;
1023
1024 for i in warmup..len {
1025 for r in 0..m {
1026 let mut acc = s_xy[r];
1027 let row = r * m;
1028 for c in 0..r {
1029 acc -= l[row + c] * tmp_y[c];
1030 }
1031 let diag = l[row + r];
1032 tmp_y[r] = acc / diag;
1033 }
1034
1035 for r in (0..m).rev() {
1036 let row = r * m;
1037 let mut acc = tmp_y[r];
1038 for c in (r + 1)..m {
1039 acc -= u[row + c] * coeffs[c];
1040 }
1041 let diag = u[row + r];
1042 coeffs[r] = acc / diag;
1043 }
1044
1045 let mut reg = 0.0f64;
1046 for p in (0..m).rev() {
1047 reg = reg.mul_add(x_pos, coeffs[p]);
1048 }
1049
1050 let mean = sum * inv_n;
1051 let var = (sumsq * inv_n) - mean * mean;
1052 let stdev = if var > 0.0 { var.sqrt() } else { 0.0 };
1053
1054 out[i] = reg;
1055 out_upper[i] = reg + ndev * stdev;
1056 out_lower[i] = reg - ndev * stdev;
1057
1058 if i + 1 == len {
1059 break;
1060 }
1061 let y_old = smoothed[start];
1062 let y_new_idx = start + n;
1063 if y_new_idx >= len {
1064 break;
1065 }
1066 let y_new = smoothed[y_new_idx];
1067
1068 s_prev.copy_from_slice(&s_xy);
1069
1070 s_xy[0] = s_prev[0] - y_old + y_new;
1071 sum = sum - y_old + y_new;
1072 sumsq = sumsq - y_old * y_old + y_new * y_new;
1073
1074 for r in 1..=k {
1075 let row = r * stride;
1076 let mut acc = 0.0f64;
1077 for m2 in 0..=r {
1078 let sign = if ((r - m2) & 1) == 1 { -1.0 } else { 1.0 };
1079 acc += sign * binom[row + m2] * s_prev[m2];
1080 }
1081 s_xy[r] = acc + n_pow[r] * y_new;
1082 }
1083
1084 start += 1;
1085 }
1086
1087 Ok(())
1088}
1089
1090struct PrbFixedDesign {
1091 m: usize,
1092 l: Vec<f64>,
1093 u: Vec<f64>,
1094 binom: Vec<f64>,
1095 n_pow: Vec<f64>,
1096}
1097
1098#[inline]
1099fn build_fixed_design(n: usize, k: usize) -> Result<PrbFixedDesign, PrbError> {
1100 let m = k + 1;
1101
1102 let max_pow = 2 * k;
1103 let mut sx = vec![0.0f64; max_pow + 1];
1104 for j in 1..=n {
1105 let jf = j as f64;
1106 let mut pwr = 1.0;
1107 sx[0] += 1.0;
1108 for p in 1..=max_pow {
1109 pwr *= jf;
1110 sx[p] += pwr;
1111 }
1112 }
1113
1114 let mut a = vec![0.0f64; m * m];
1115 for i in 0..m {
1116 for j in 0..m {
1117 a[i * m + j] = sx[i + j];
1118 }
1119 }
1120 let (l, u) = lu_decomposition(&a, m)?;
1121
1122 let mut binom = vec![0.0f64; m * m];
1123 for r in 0..=k {
1124 let r_off = r * m;
1125 binom[r_off + 0] = 1.0;
1126 binom[r_off + r] = 1.0;
1127 for c in 1..r {
1128 let prev = (r - 1) * m;
1129 binom[r_off + c] = binom[prev + (c - 1)] + binom[prev + c];
1130 }
1131 }
1132 let mut n_pow = vec![0.0f64; m];
1133 n_pow[0] = 1.0;
1134 let n_f = n as f64;
1135 for r in 1..=k {
1136 n_pow[r] = n_pow[r - 1] * n_f;
1137 }
1138
1139 Ok(PrbFixedDesign {
1140 m,
1141 l,
1142 u,
1143 binom,
1144 n_pow,
1145 })
1146}
1147
1148#[inline]
1149fn prb_run_with_fixed_design(
1150 smoothed: &[f64],
1151 n: usize,
1152 k: usize,
1153 regression_offset: i32,
1154 ndev: f64,
1155 equ_from: usize,
1156 first: usize,
1157 pre: &PrbFixedDesign,
1158 out: &mut [f64],
1159 out_upper: &mut [f64],
1160 out_lower: &mut [f64],
1161) -> Result<(), PrbError> {
1162 let len = smoothed.len();
1163 let warmup = first + n - 1 + equ_from;
1164 if warmup >= len {
1165 return Err(PrbError::NotEnoughValidData {
1166 needed: n,
1167 valid: len.saturating_sub(first),
1168 });
1169 }
1170 let n_f = n as f64;
1171 let x_pos = n_f - (regression_offset as f64) + (equ_from as f64);
1172 let inv_n = 1.0 / n_f;
1173
1174 let m = pre.m;
1175 let l = &pre.l;
1176 let u = &pre.u;
1177 let binom = &pre.binom;
1178 let n_pow = &pre.n_pow;
1179
1180 let mut start = warmup + 1 - n - equ_from;
1181 let mut s_xy = vec![0.0f64; m];
1182 let mut sum = 0.0f64;
1183 let mut sumsq = 0.0f64;
1184 for (idx, &y) in smoothed[start..start + n].iter().enumerate() {
1185 sum += y;
1186 sumsq += y * y;
1187 let jf = (idx as f64) + 1.0;
1188 s_xy[0] += y;
1189 let mut w = jf;
1190 for p in 1..=k {
1191 s_xy[p] = y.mul_add(w, s_xy[p]);
1192 w *= jf;
1193 }
1194 }
1195
1196 let mut tmp_y = vec![0.0f64; m];
1197 let mut coeffs = vec![0.0f64; m];
1198 let mut s_prev = vec![0.0f64; m];
1199
1200 for i in warmup..len {
1201 for r in 0..m {
1202 let mut acc = s_xy[r];
1203 let row = r * m;
1204 for c in 0..r {
1205 acc -= l[row + c] * tmp_y[c];
1206 }
1207 tmp_y[r] = acc / l[row + r];
1208 }
1209 for r in (0..m).rev() {
1210 let mut acc = tmp_y[r];
1211 let row = r * m;
1212 for c in (r + 1)..m {
1213 acc -= u[row + c] * coeffs[c];
1214 }
1215 coeffs[r] = acc / u[row + r];
1216 }
1217
1218 let mut reg = 0.0f64;
1219 for p in (0..m).rev() {
1220 reg = reg.mul_add(x_pos, coeffs[p]);
1221 }
1222 let mean = sum * inv_n;
1223 let var = (sumsq * inv_n) - mean * mean;
1224 let stdev = if var > 0.0 { var.sqrt() } else { 0.0 };
1225 out[i] = reg;
1226 out_upper[i] = reg + ndev * stdev;
1227 out_lower[i] = reg - ndev * stdev;
1228
1229 if i + 1 == len {
1230 break;
1231 }
1232
1233 let y_old = smoothed[start];
1234 let y_new_idx = start + n;
1235 if y_new_idx >= len {
1236 break;
1237 }
1238 let y_new = smoothed[y_new_idx];
1239
1240 s_prev.copy_from_slice(&s_xy);
1241 s_xy[0] = s_prev[0] - y_old + y_new;
1242 sum = sum - y_old + y_new;
1243 sumsq = sumsq - y_old * y_old + y_new * y_new;
1244 for r in 1..=k {
1245 let row = r * m;
1246 let mut acc = 0.0f64;
1247 for m2 in 0..=r {
1248 let sign = if ((r - m2) & 1) == 1 { -1.0 } else { 1.0 };
1249 acc += sign * binom[row + m2] * s_prev[m2];
1250 }
1251 s_xy[r] = acc + n_pow[r] * y_new;
1252 }
1253 start += 1;
1254 }
1255 Ok(())
1256}
1257
1258#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1259#[target_feature(enable = "avx2,fma")]
1260unsafe fn prb_avx2(
1261 data: &[f64],
1262 smooth_data: bool,
1263 smooth_period: usize,
1264 regression_period: usize,
1265 polynomial_order: usize,
1266 regression_offset: i32,
1267 ndev: f64,
1268 equ_from: usize,
1269 first: usize,
1270 out: &mut [f64],
1271 out_upper: &mut [f64],
1272 out_lower: &mut [f64],
1273) -> Result<(), PrbError> {
1274 prb_scalar(
1275 data,
1276 smooth_data,
1277 smooth_period,
1278 regression_period,
1279 polynomial_order,
1280 regression_offset,
1281 ndev,
1282 equ_from,
1283 first,
1284 out,
1285 out_upper,
1286 out_lower,
1287 )
1288}
1289
1290#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1291#[target_feature(enable = "avx512f,avx512dq,fma")]
1292unsafe fn prb_avx512(
1293 data: &[f64],
1294 smooth_data: bool,
1295 smooth_period: usize,
1296 regression_period: usize,
1297 polynomial_order: usize,
1298 regression_offset: i32,
1299 ndev: f64,
1300 equ_from: usize,
1301 first: usize,
1302 out: &mut [f64],
1303 out_upper: &mut [f64],
1304 out_lower: &mut [f64],
1305) -> Result<(), PrbError> {
1306 prb_scalar(
1307 data,
1308 smooth_data,
1309 smooth_period,
1310 regression_period,
1311 polynomial_order,
1312 regression_offset,
1313 ndev,
1314 equ_from,
1315 first,
1316 out,
1317 out_upper,
1318 out_lower,
1319 )
1320}
1321
1322#[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
1323unsafe fn prb_simd128(
1324 data: &[f64],
1325 smooth_data: bool,
1326 smooth_period: usize,
1327 regression_period: usize,
1328 polynomial_order: usize,
1329 regression_offset: i32,
1330 ndev: f64,
1331 equ_from: usize,
1332 first: usize,
1333 out: &mut [f64],
1334 out_upper: &mut [f64],
1335 out_lower: &mut [f64],
1336) -> Result<(), PrbError> {
1337 prb_scalar(
1338 data,
1339 smooth_data,
1340 smooth_period,
1341 regression_period,
1342 polynomial_order,
1343 regression_offset,
1344 ndev,
1345 equ_from,
1346 first,
1347 out,
1348 out_upper,
1349 out_lower,
1350 )
1351}
1352
1353#[inline]
1354pub fn prb(input: &PrbInput) -> Result<PrbOutput, PrbError> {
1355 prb_with_kernel(input, Kernel::Auto)
1356}
1357
1358pub fn prb_with_kernel(input: &PrbInput, kernel: Kernel) -> Result<PrbOutput, PrbError> {
1359 let data: &[f64] = input.as_ref();
1360 let len = data.len();
1361 if len == 0 {
1362 return Err(PrbError::EmptyInputData);
1363 }
1364
1365 let first = data
1366 .iter()
1367 .position(|x| !x.is_nan())
1368 .ok_or(PrbError::AllValuesNaN)?;
1369
1370 let smooth_data = input.get_smooth_data();
1371 let smooth_period = input.get_smooth_period();
1372 let regression_period = input.get_regression_period();
1373 let polynomial_order = input.get_polynomial_order();
1374 let regression_offset = input.get_regression_offset();
1375 let ndev = input.get_ndev();
1376 let equ_from = input.get_equ_from();
1377
1378 if polynomial_order < 1 {
1379 return Err(PrbError::InvalidOrder {
1380 order: polynomial_order,
1381 });
1382 }
1383
1384 if smooth_data && smooth_period < 2 {
1385 return Err(PrbError::InvalidSmoothPeriod {
1386 period: smooth_period,
1387 });
1388 }
1389
1390 if regression_period == 0 || regression_period > len {
1391 return Err(PrbError::InvalidPeriod {
1392 period: regression_period,
1393 data_len: len,
1394 });
1395 }
1396
1397 let warmup = first + regression_period - 1 + equ_from;
1398 if warmup >= len {
1399 return Err(PrbError::NotEnoughValidData {
1400 needed: regression_period,
1401 valid: len - first,
1402 });
1403 }
1404
1405 let chosen = match kernel {
1406 Kernel::Auto => detect_best_kernel(),
1407 k => k,
1408 };
1409
1410 let mut values = alloc_with_nan_prefix(len, warmup);
1411 let mut upper_band = alloc_with_nan_prefix(len, warmup);
1412 let mut lower_band = alloc_with_nan_prefix(len, warmup);
1413
1414 prb_compute_into(
1415 data,
1416 smooth_data,
1417 smooth_period,
1418 regression_period,
1419 polynomial_order,
1420 regression_offset,
1421 ndev,
1422 equ_from,
1423 first,
1424 chosen,
1425 &mut values,
1426 &mut upper_band,
1427 &mut lower_band,
1428 )?;
1429
1430 Ok(PrbOutput {
1431 values,
1432 upper_band,
1433 lower_band,
1434 })
1435}
1436
1437#[inline]
1438pub fn prb_into_slice(
1439 dst_main: &mut [f64],
1440 dst_upper: &mut [f64],
1441 dst_lower: &mut [f64],
1442 input: &PrbInput,
1443 kern: Kernel,
1444) -> Result<(), PrbError> {
1445 let data: &[f64] = input.as_ref();
1446 let len = data.len();
1447 if len == 0 {
1448 return Err(PrbError::EmptyInputData);
1449 }
1450 if dst_main.len() != len || dst_upper.len() != len || dst_lower.len() != len {
1451 return Err(PrbError::OutputLengthMismatch {
1452 expected: len,
1453 got: dst_main.len().max(dst_upper.len()).max(dst_lower.len()),
1454 });
1455 }
1456
1457 let first = data
1458 .iter()
1459 .position(|x| !x.is_nan())
1460 .ok_or(PrbError::AllValuesNaN)?;
1461 let smooth_data = input.get_smooth_data();
1462 let smooth_period = input.get_smooth_period();
1463 let regression_period = input.get_regression_period();
1464 let polynomial_order = input.get_polynomial_order();
1465 let regression_offset = input.get_regression_offset();
1466 let ndev = input.get_ndev();
1467 let equ_from = input.get_equ_from();
1468
1469 if polynomial_order < 1 {
1470 return Err(PrbError::InvalidOrder {
1471 order: polynomial_order,
1472 });
1473 }
1474 if smooth_data && smooth_period < 2 {
1475 return Err(PrbError::InvalidSmoothPeriod {
1476 period: smooth_period,
1477 });
1478 }
1479 if regression_period == 0 || regression_period > len {
1480 return Err(PrbError::InvalidPeriod {
1481 period: regression_period,
1482 data_len: len,
1483 });
1484 }
1485
1486 let warmup = first + regression_period - 1 + equ_from;
1487 for v in &mut dst_main[..warmup] {
1488 *v = f64::NAN;
1489 }
1490 for v in &mut dst_upper[..warmup] {
1491 *v = f64::NAN;
1492 }
1493 for v in &mut dst_lower[..warmup] {
1494 *v = f64::NAN;
1495 }
1496
1497 let chosen = match kern {
1498 Kernel::Auto => detect_best_kernel(),
1499 k => k,
1500 };
1501
1502 prb_compute_into(
1503 data,
1504 smooth_data,
1505 smooth_period,
1506 regression_period,
1507 polynomial_order,
1508 regression_offset,
1509 ndev,
1510 equ_from,
1511 first,
1512 chosen,
1513 dst_main,
1514 dst_upper,
1515 dst_lower,
1516 )
1517}
1518
1519#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
1520#[inline]
1521pub fn prb_into(
1522 input: &PrbInput,
1523 out_main: &mut [f64],
1524 out_upper: &mut [f64],
1525 out_lower: &mut [f64],
1526) -> Result<(), PrbError> {
1527 prb_into_slice(out_main, out_upper, out_lower, input, Kernel::Auto)
1528}
1529
1530#[derive(Clone, Debug)]
1531pub struct PrbBatchRange {
1532 pub smooth_period: (usize, usize, usize),
1533 pub regression_period: (usize, usize, usize),
1534 pub polynomial_order: (usize, usize, usize),
1535 pub regression_offset: (i32, i32, i32),
1536}
1537
1538impl Default for PrbBatchRange {
1539 fn default() -> Self {
1540 Self {
1541 smooth_period: (10, 10, 0),
1542 regression_period: (100, 349, 1),
1543 polynomial_order: (2, 2, 0),
1544 regression_offset: (0, 0, 0),
1545 }
1546 }
1547}
1548
1549#[derive(Clone, Debug)]
1550pub struct PrbBatchOutput {
1551 pub values: Vec<f64>,
1552 pub upper_band: Vec<f64>,
1553 pub lower_band: Vec<f64>,
1554 pub combos: Vec<PrbParams>,
1555 pub rows: usize,
1556 pub cols: usize,
1557}
1558
1559impl PrbBatchOutput {
1560 pub fn row_for_params(&self, p: &PrbParams) -> Option<usize> {
1561 self.combos.iter().position(|c| {
1562 c.smooth_period.unwrap_or(10) == p.smooth_period.unwrap_or(10)
1563 && c.regression_period.unwrap_or(100) == p.regression_period.unwrap_or(100)
1564 && c.polynomial_order.unwrap_or(2) == p.polynomial_order.unwrap_or(2)
1565 && c.regression_offset.unwrap_or(0) == p.regression_offset.unwrap_or(0)
1566 })
1567 }
1568
1569 pub fn values_for(&self, p: &PrbParams) -> Option<&[f64]> {
1570 self.row_for_params(p).map(|row| {
1571 let start = row * self.cols;
1572 &self.values[start..start + self.cols]
1573 })
1574 }
1575}
1576
1577#[derive(Clone, Debug, Default)]
1578pub struct PrbBatchBuilder {
1579 range: PrbBatchRange,
1580 kernel: Kernel,
1581 smooth_data: bool,
1582}
1583
1584impl PrbBatchBuilder {
1585 pub fn new() -> Self {
1586 Self::default()
1587 }
1588
1589 pub fn kernel(mut self, k: Kernel) -> Self {
1590 self.kernel = k;
1591 self
1592 }
1593
1594 pub fn smooth_data(mut self, s: bool) -> Self {
1595 self.smooth_data = s;
1596 self
1597 }
1598
1599 #[inline]
1600 pub fn smooth_period_range(mut self, start: usize, end: usize, step: usize) -> Self {
1601 self.range.smooth_period = (start, end, step);
1602 self
1603 }
1604
1605 #[inline]
1606 pub fn regression_period_range(mut self, start: usize, end: usize, step: usize) -> Self {
1607 self.range.regression_period = (start, end, step);
1608 self
1609 }
1610
1611 #[inline]
1612 pub fn polynomial_order_range(mut self, start: usize, end: usize, step: usize) -> Self {
1613 self.range.polynomial_order = (start, end, step);
1614 self
1615 }
1616
1617 #[inline]
1618 pub fn regression_offset_range(mut self, start: i32, end: i32, step: i32) -> Self {
1619 self.range.regression_offset = (start, end, step);
1620 self
1621 }
1622
1623 pub fn apply_slice(self, data: &[f64]) -> Result<PrbBatchOutput, PrbError> {
1624 prb_batch_with_kernel(data, &self.range, self.kernel, self.smooth_data)
1625 }
1626
1627 pub fn apply_candles(self, c: &Candles, src: &str) -> Result<PrbBatchOutput, PrbError> {
1628 let slice = source_type(c, src);
1629 self.apply_slice(slice)
1630 }
1631}
1632
1633pub fn prb_batch_with_kernel(
1634 data: &[f64],
1635 sweep: &PrbBatchRange,
1636 kernel: Kernel,
1637 smooth_data: bool,
1638) -> Result<PrbBatchOutput, PrbError> {
1639 let kernel = match kernel {
1640 Kernel::Auto => detect_best_batch_kernel(),
1641 other if other.is_batch() => other,
1642 _ => {
1643 return Err(PrbError::InvalidKernelForBatch(kernel));
1644 }
1645 };
1646
1647 let simd = match kernel {
1648 Kernel::Avx512Batch => Kernel::Avx512,
1649 Kernel::Avx2Batch => Kernel::Avx2,
1650 Kernel::ScalarBatch => Kernel::Scalar,
1651 _ => unreachable!(),
1652 };
1653
1654 prb_batch_par_slice(data, sweep, simd, smooth_data)
1655}
1656
1657#[inline(always)]
1658pub fn prb_batch_slice(
1659 data: &[f64],
1660 sweep: &PrbBatchRange,
1661 kern: Kernel,
1662 smooth_data: bool,
1663) -> Result<PrbBatchOutput, PrbError> {
1664 prb_batch_inner(data, sweep, kern, smooth_data, false)
1665}
1666
1667#[inline(always)]
1668pub fn prb_batch_par_slice(
1669 data: &[f64],
1670 sweep: &PrbBatchRange,
1671 kern: Kernel,
1672 smooth_data: bool,
1673) -> Result<PrbBatchOutput, PrbError> {
1674 prb_batch_inner(data, sweep, kern, smooth_data, true)
1675}
1676
1677#[inline(always)]
1678fn prb_batch_inner(
1679 data: &[f64],
1680 sweep: &PrbBatchRange,
1681 kern: Kernel,
1682 smooth_data: bool,
1683 parallel: bool,
1684) -> Result<PrbBatchOutput, PrbError> {
1685 use core::mem::ManuallyDrop;
1686 let combos = expand_grid(sweep, smooth_data)?;
1687 let cols = data.len();
1688 let rows = combos.len();
1689 if cols == 0 {
1690 return Err(PrbError::AllValuesNaN);
1691 }
1692
1693 let _ = rows
1694 .checked_mul(cols)
1695 .ok_or_else(|| PrbError::InvalidRange {
1696 start: rows.to_string(),
1697 end: cols.to_string(),
1698 step: "rows*cols".into(),
1699 })?;
1700
1701 for c in &combos {
1702 let n = c.regression_period.unwrap_or(100);
1703 if n == 0 || n > cols {
1704 return Err(PrbError::InvalidPeriod {
1705 period: n,
1706 data_len: cols,
1707 });
1708 }
1709 }
1710
1711 let first = data
1712 .iter()
1713 .position(|x| !x.is_nan())
1714 .ok_or(PrbError::AllValuesNaN)?;
1715 let warm: Vec<usize> = combos
1716 .iter()
1717 .map(|c| first + c.regression_period.unwrap() - 1)
1718 .collect();
1719
1720 let mut mu_main = make_uninit_matrix(rows, cols);
1721 let mut mu_up = make_uninit_matrix(rows, cols);
1722 let mut mu_lo = make_uninit_matrix(rows, cols);
1723
1724 init_matrix_prefixes(&mut mu_main, cols, &warm);
1725 init_matrix_prefixes(&mut mu_up, cols, &warm);
1726 init_matrix_prefixes(&mut mu_lo, cols, &warm);
1727
1728 let mut g_main = ManuallyDrop::new(mu_main);
1729 let mut g_up = ManuallyDrop::new(mu_up);
1730 let mut g_lo = ManuallyDrop::new(mu_lo);
1731
1732 let out_main: &mut [f64] =
1733 unsafe { core::slice::from_raw_parts_mut(g_main.as_mut_ptr() as *mut f64, g_main.len()) };
1734 let out_up: &mut [f64] =
1735 unsafe { core::slice::from_raw_parts_mut(g_up.as_mut_ptr() as *mut f64, g_up.len()) };
1736 let out_lo: &mut [f64] =
1737 unsafe { core::slice::from_raw_parts_mut(g_lo.as_mut_ptr() as *mut f64, g_lo.len()) };
1738
1739 use std::collections::{BTreeSet, HashMap};
1740 let mut keyset: BTreeSet<(usize, usize)> = BTreeSet::new();
1741 for c in &combos {
1742 keyset.insert((
1743 c.regression_period.unwrap_or(100),
1744 c.polynomial_order.unwrap_or(2),
1745 ));
1746 }
1747 let mut pre_map_local: HashMap<(usize, usize), PrbFixedDesign> =
1748 HashMap::with_capacity(keyset.len());
1749 for (n, k) in keyset {
1750 pre_map_local.insert((n, k), build_fixed_design(n, k)?);
1751 }
1752 let pre_map = std::sync::Arc::new(pre_map_local);
1753
1754 let smoothed_map = if smooth_data {
1755 let mut sps: BTreeSet<usize> = BTreeSet::new();
1756 for c in &combos {
1757 sps.insert(c.smooth_period.unwrap_or(10));
1758 }
1759 let mut map: HashMap<usize, Vec<f64>> = HashMap::with_capacity(sps.len());
1760 for sp in sps {
1761 map.insert(sp, ssf_filter(data, sp, first));
1762 }
1763 Some(std::sync::Arc::new(map))
1764 } else {
1765 None
1766 };
1767
1768 if parallel {
1769 #[cfg(not(target_arch = "wasm32"))]
1770 {
1771 use rayon::prelude::*;
1772 let main_ptr = out_main.as_mut_ptr() as usize;
1773 let up_ptr = out_up.as_mut_ptr() as usize;
1774 let lo_ptr = out_lo.as_mut_ptr() as usize;
1775 let pre_map = pre_map.clone();
1776 let smoothed_map = smoothed_map.clone();
1777 (0..rows)
1778 .into_par_iter()
1779 .try_for_each(|row| -> Result<(), PrbError> {
1780 let c = &combos[row];
1781 unsafe {
1782 let r_main = std::slice::from_raw_parts_mut(
1783 (main_ptr as *mut f64).add(row * cols),
1784 cols,
1785 );
1786 let r_up = std::slice::from_raw_parts_mut(
1787 (up_ptr as *mut f64).add(row * cols),
1788 cols,
1789 );
1790 let r_lo = std::slice::from_raw_parts_mut(
1791 (lo_ptr as *mut f64).add(row * cols),
1792 cols,
1793 );
1794
1795 if smooth_data {
1796 let sp = c.smooth_period.unwrap_or(10);
1797 let sm_ref = smoothed_map
1798 .as_ref()
1799 .and_then(|m| m.get(&sp))
1800 .expect("missing smoothed cache");
1801 let n = c.regression_period.unwrap_or(100);
1802 let k = c.polynomial_order.unwrap_or(2);
1803 let pre = pre_map.get(&(n, k)).expect("missing precompute");
1804 prb_run_with_fixed_design(
1805 sm_ref,
1806 n,
1807 k,
1808 c.regression_offset.unwrap_or(0),
1809 c.ndev.unwrap_or(2.0),
1810 c.equ_from.unwrap_or(0),
1811 first,
1812 pre,
1813 r_main,
1814 r_up,
1815 r_lo,
1816 )
1817 } else {
1818 let n = c.regression_period.unwrap_or(100);
1819 let k = c.polynomial_order.unwrap_or(2);
1820 let pre = pre_map.get(&(n, k)).expect("missing precompute");
1821 prb_run_with_fixed_design(
1822 data,
1823 n,
1824 k,
1825 c.regression_offset.unwrap_or(0),
1826 c.ndev.unwrap_or(2.0),
1827 c.equ_from.unwrap_or(0),
1828 first,
1829 pre,
1830 r_main,
1831 r_up,
1832 r_lo,
1833 )
1834 }
1835 }
1836 })?;
1837 }
1838 #[cfg(target_arch = "wasm32")]
1839 {
1840 for row in 0..rows {
1841 let c = &combos[row];
1842 let r_main = &mut out_main[row * cols..(row + 1) * cols];
1843 let r_up = &mut out_up[row * cols..(row + 1) * cols];
1844 let r_lo = &mut out_lo[row * cols..(row + 1) * cols];
1845
1846 prb_compute_into(
1847 data,
1848 smooth_data,
1849 c.smooth_period.unwrap_or(10),
1850 c.regression_period.unwrap_or(100),
1851 c.polynomial_order.unwrap_or(2),
1852 c.regression_offset.unwrap_or(0),
1853 c.ndev.unwrap_or(2.0),
1854 c.equ_from.unwrap_or(0),
1855 first,
1856 kern,
1857 r_main,
1858 r_up,
1859 r_lo,
1860 )?;
1861 }
1862 }
1863 } else {
1864 for row in 0..rows {
1865 let c = &combos[row];
1866 let r_main = &mut out_main[row * cols..(row + 1) * cols];
1867 let r_up = &mut out_up[row * cols..(row + 1) * cols];
1868 let r_lo = &mut out_lo[row * cols..(row + 1) * cols];
1869
1870 if smooth_data {
1871 let sp = c.smooth_period.unwrap_or(10);
1872 let sm_ref = smoothed_map
1873 .as_ref()
1874 .and_then(|m| m.get(&sp))
1875 .expect("missing smoothed cache");
1876 let n = c.regression_period.unwrap_or(100);
1877 let k = c.polynomial_order.unwrap_or(2);
1878 let pre = pre_map.get(&(n, k)).expect("missing precompute");
1879 prb_run_with_fixed_design(
1880 sm_ref,
1881 n,
1882 k,
1883 c.regression_offset.unwrap_or(0),
1884 c.ndev.unwrap_or(2.0),
1885 c.equ_from.unwrap_or(0),
1886 first,
1887 pre,
1888 r_main,
1889 r_up,
1890 r_lo,
1891 )?;
1892 } else {
1893 let n = c.regression_period.unwrap_or(100);
1894 let k = c.polynomial_order.unwrap_or(2);
1895 let pre = pre_map.get(&(n, k)).expect("missing precompute");
1896 prb_run_with_fixed_design(
1897 data,
1898 n,
1899 k,
1900 c.regression_offset.unwrap_or(0),
1901 c.ndev.unwrap_or(2.0),
1902 c.equ_from.unwrap_or(0),
1903 first,
1904 pre,
1905 r_main,
1906 r_up,
1907 r_lo,
1908 )?;
1909 }
1910 }
1911 }
1912
1913 let values = unsafe {
1914 Vec::from_raw_parts(
1915 g_main.as_mut_ptr() as *mut f64,
1916 g_main.len(),
1917 g_main.capacity(),
1918 )
1919 };
1920 let upper_band =
1921 unsafe { Vec::from_raw_parts(g_up.as_mut_ptr() as *mut f64, g_up.len(), g_up.capacity()) };
1922 let lower_band =
1923 unsafe { Vec::from_raw_parts(g_lo.as_mut_ptr() as *mut f64, g_lo.len(), g_lo.capacity()) };
1924
1925 Ok(PrbBatchOutput {
1926 values,
1927 upper_band,
1928 lower_band,
1929 combos,
1930 rows,
1931 cols,
1932 })
1933}
1934
1935#[inline(always)]
1936fn expand_grid(r: &PrbBatchRange, smooth_flag: bool) -> Result<Vec<PrbParams>, PrbError> {
1937 fn axis_usize((start, end, step): (usize, usize, usize)) -> Result<Vec<usize>, PrbError> {
1938 if step == 0 || start == end {
1939 return Ok(vec![start]);
1940 }
1941 if start < end {
1942 let mut v = Vec::new();
1943 let mut x = start;
1944 let st = step.max(1);
1945 while x <= end {
1946 v.push(x);
1947 let next = match x.checked_add(st) {
1948 Some(n) if n != x => n,
1949 _ => break,
1950 };
1951 x = next;
1952 }
1953 if v.is_empty() {
1954 return Err(PrbError::InvalidRange {
1955 start: start.to_string(),
1956 end: end.to_string(),
1957 step: step.to_string(),
1958 });
1959 }
1960 return Ok(v);
1961 }
1962
1963 let mut v = Vec::new();
1964 let mut x = start as isize;
1965 let end_i = end as isize;
1966 let st = (step as isize).max(1);
1967 while x >= end_i {
1968 v.push(x as usize);
1969 x -= st;
1970 }
1971 if v.is_empty() {
1972 return Err(PrbError::InvalidRange {
1973 start: start.to_string(),
1974 end: end.to_string(),
1975 step: step.to_string(),
1976 });
1977 }
1978 Ok(v)
1979 }
1980 fn axis_i32((start, end, step): (i32, i32, i32)) -> Result<Vec<i32>, PrbError> {
1981 if step == 0 || start == end {
1982 return Ok(vec![start]);
1983 }
1984 let mut v = Vec::new();
1985 if start < end {
1986 let mut x = start;
1987 let st = step.max(1);
1988 while x <= end {
1989 v.push(x);
1990 let next = match x.checked_add(st) {
1991 Some(n) if n != x => n,
1992 _ => break,
1993 };
1994 x = next;
1995 }
1996 } else {
1997 let mut x = start;
1998 let st = step.abs().max(1);
1999 while x >= end {
2000 v.push(x);
2001 let next = match x.checked_sub(st) {
2002 Some(n) if n != x => n,
2003 _ => break,
2004 };
2005 x = next;
2006 }
2007 }
2008 if v.is_empty() {
2009 return Err(PrbError::InvalidRange {
2010 start: start.to_string(),
2011 end: end.to_string(),
2012 step: step.to_string(),
2013 });
2014 }
2015 Ok(v)
2016 }
2017
2018 let sps = axis_usize(r.smooth_period)?;
2019 let rps = axis_usize(r.regression_period)?;
2020 let pos = axis_usize(r.polynomial_order)?;
2021 let ros = axis_i32(r.regression_offset)?;
2022
2023 let cap = sps
2024 .len()
2025 .checked_mul(rps.len())
2026 .and_then(|x| x.checked_mul(pos.len()))
2027 .and_then(|x| x.checked_mul(ros.len()))
2028 .ok_or_else(|| PrbError::InvalidRange {
2029 start: "cap".into(),
2030 end: "overflow".into(),
2031 step: "mul".into(),
2032 })?;
2033
2034 let mut out = Vec::with_capacity(cap);
2035 for &sp in &sps {
2036 for &rp in &rps {
2037 for &po in &pos {
2038 for &ro in &ros {
2039 out.push(PrbParams {
2040 smooth_data: Some(smooth_flag),
2041 smooth_period: Some(sp),
2042 regression_period: Some(rp),
2043 polynomial_order: Some(po),
2044 regression_offset: Some(ro),
2045 ndev: Some(2.0),
2046 equ_from: Some(0),
2047 });
2048 }
2049 }
2050 }
2051 }
2052 if out.is_empty() {
2053 return Err(PrbError::InvalidRange {
2054 start: "range".into(),
2055 end: "range".into(),
2056 step: "empty".into(),
2057 });
2058 }
2059 Ok(out)
2060}
2061
2062#[cfg(feature = "python")]
2063#[pyfunction(name = "prb")]
2064#[pyo3(signature = (data, smooth_data, smooth_period, regression_period, polynomial_order, regression_offset, ndev, kernel=None))]
2065pub fn prb_py<'py>(
2066 py: Python<'py>,
2067 data: PyReadonlyArray1<'py, f64>,
2068 smooth_data: bool,
2069 smooth_period: usize,
2070 regression_period: usize,
2071 polynomial_order: usize,
2072 regression_offset: i32,
2073 ndev: f64,
2074 kernel: Option<&str>,
2075) -> PyResult<(
2076 Bound<'py, PyArray1<f64>>,
2077 Bound<'py, PyArray1<f64>>,
2078 Bound<'py, PyArray1<f64>>,
2079)> {
2080 let slice_in = data.as_slice()?;
2081 let params = PrbParams {
2082 smooth_data: Some(smooth_data),
2083 smooth_period: Some(smooth_period),
2084 regression_period: Some(regression_period),
2085 polynomial_order: Some(polynomial_order),
2086 regression_offset: Some(regression_offset),
2087 ndev: Some(ndev),
2088 equ_from: Some(0),
2089 };
2090 let input = PrbInput::from_slice(slice_in, params);
2091 let kern = validate_kernel(kernel, false)?;
2092 let (m, u, l) = py
2093 .allow_threads(|| {
2094 prb_with_kernel(&input, kern).map(|o| (o.values, o.upper_band, o.lower_band))
2095 })
2096 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2097 Ok((m.into_pyarray(py), u.into_pyarray(py), l.into_pyarray(py)))
2098}
2099
2100#[cfg(feature = "python")]
2101#[pyfunction]
2102#[pyo3(name = "prb_batch")]
2103pub fn prb_batch_py<'py>(
2104 py: Python<'py>,
2105 data: PyReadonlyArray1<'py, f64>,
2106 smooth_data: bool,
2107 smooth_period_start: usize,
2108 smooth_period_end: usize,
2109 smooth_period_step: usize,
2110 regression_period_start: usize,
2111 regression_period_end: usize,
2112 regression_period_step: usize,
2113 polynomial_order_start: usize,
2114 polynomial_order_end: usize,
2115 polynomial_order_step: usize,
2116 regression_offset_start: i32,
2117 regression_offset_end: i32,
2118 regression_offset_step: i32,
2119 kernel: Option<&str>,
2120) -> PyResult<Bound<'py, PyDict>> {
2121 let slice_in = data.as_slice()?;
2122 let kern = validate_kernel(kernel, true)?;
2123
2124 let sweep = PrbBatchRange {
2125 smooth_period: (smooth_period_start, smooth_period_end, smooth_period_step),
2126 regression_period: (
2127 regression_period_start,
2128 regression_period_end,
2129 regression_period_step,
2130 ),
2131 polynomial_order: (
2132 polynomial_order_start,
2133 polynomial_order_end,
2134 polynomial_order_step,
2135 ),
2136 regression_offset: (
2137 regression_offset_start,
2138 regression_offset_end,
2139 regression_offset_step,
2140 ),
2141 };
2142
2143 let out = prb_batch_with_kernel(slice_in, &sweep, kern, smooth_data)
2144 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2145
2146 let rows = out.rows;
2147 let cols = out.cols;
2148
2149 let dict = PyDict::new(py);
2150
2151 use ndarray::Array2;
2152 let values_arr = Array2::from_shape_vec((rows, cols), out.values)
2153 .map_err(|e| PyValueError::new_err(format!("Failed to reshape values: {}", e)))?;
2154 let upper_arr = Array2::from_shape_vec((rows, cols), out.upper_band)
2155 .map_err(|e| PyValueError::new_err(format!("Failed to reshape upper: {}", e)))?;
2156 let lower_arr = Array2::from_shape_vec((rows, cols), out.lower_band)
2157 .map_err(|e| PyValueError::new_err(format!("Failed to reshape lower: {}", e)))?;
2158
2159 dict.set_item("values", values_arr.into_pyarray(py))?;
2160 dict.set_item("upper", upper_arr.into_pyarray(py))?;
2161 dict.set_item("lower", lower_arr.into_pyarray(py))?;
2162
2163 dict.set_item(
2164 "smooth_periods",
2165 out.combos
2166 .iter()
2167 .map(|p| p.smooth_period.unwrap_or(10) as u64)
2168 .collect::<Vec<_>>()
2169 .into_pyarray(py),
2170 )?;
2171 dict.set_item(
2172 "regression_periods",
2173 out.combos
2174 .iter()
2175 .map(|p| p.regression_period.unwrap_or(100) as u64)
2176 .collect::<Vec<_>>()
2177 .into_pyarray(py),
2178 )?;
2179 dict.set_item(
2180 "polynomial_orders",
2181 out.combos
2182 .iter()
2183 .map(|p| p.polynomial_order.unwrap_or(2) as u64)
2184 .collect::<Vec<_>>()
2185 .into_pyarray(py),
2186 )?;
2187 dict.set_item(
2188 "regression_offsets",
2189 out.combos
2190 .iter()
2191 .map(|p| p.regression_offset.unwrap_or(0))
2192 .collect::<Vec<_>>()
2193 .into_pyarray(py),
2194 )?;
2195
2196 dict.set_item("rows", rows)?;
2197 dict.set_item("cols", cols)?;
2198 Ok(dict.into())
2199}
2200
2201#[cfg(feature = "python")]
2202#[pyclass]
2203pub struct PrbStreamPy {
2204 stream: PrbStream,
2205}
2206
2207#[cfg(feature = "python")]
2208#[pymethods]
2209impl PrbStreamPy {
2210 #[new]
2211 fn new(
2212 smooth_data: Option<bool>,
2213 smooth_period: Option<usize>,
2214 regression_period: Option<usize>,
2215 polynomial_order: Option<usize>,
2216 regression_offset: Option<i32>,
2217 ndev: Option<f64>,
2218 ) -> PyResult<Self> {
2219 let params = PrbParams {
2220 smooth_data,
2221 smooth_period,
2222 regression_period,
2223 polynomial_order,
2224 regression_offset,
2225 ndev,
2226 equ_from: Some(0),
2227 };
2228 let stream =
2229 PrbStream::try_new(params).map_err(|e| PyValueError::new_err(e.to_string()))?;
2230 Ok(PrbStreamPy { stream })
2231 }
2232
2233 fn update(&mut self, value: f64) -> Option<(f64, f64, f64)> {
2234 self.stream.update(value)
2235 }
2236}
2237
2238#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2239#[wasm_bindgen]
2240pub struct PrbJsResult {
2241 values: Vec<f64>,
2242 rows: usize,
2243 cols: usize,
2244}
2245
2246#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2247#[wasm_bindgen]
2248impl PrbJsResult {
2249 #[wasm_bindgen(getter)]
2250 pub fn values(&self) -> Vec<f64> {
2251 self.values.clone()
2252 }
2253
2254 #[wasm_bindgen(getter)]
2255 pub fn rows(&self) -> usize {
2256 self.rows
2257 }
2258
2259 #[wasm_bindgen(getter)]
2260 pub fn cols(&self) -> usize {
2261 self.cols
2262 }
2263}
2264
2265#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2266#[wasm_bindgen(js_name = "prb")]
2267pub fn prb_js(
2268 data: &[f64],
2269 smooth_data: bool,
2270 smooth_period: usize,
2271 regression_period: usize,
2272 polynomial_order: usize,
2273 regression_offset: i32,
2274 ndev: f64,
2275) -> Result<PrbJsResult, JsValue> {
2276 let params = PrbParams {
2277 smooth_data: Some(smooth_data),
2278 smooth_period: Some(smooth_period),
2279 regression_period: Some(regression_period),
2280 polynomial_order: Some(polynomial_order),
2281 regression_offset: Some(regression_offset),
2282 ndev: Some(ndev),
2283 equ_from: Some(0),
2284 };
2285 let input = PrbInput::from_slice(data, params);
2286
2287 let output = prb(&input).map_err(|e| JsValue::from_str(&e.to_string()))?;
2288
2289 let mut values = output.values;
2290 values.extend(output.upper_band);
2291 values.extend(output.lower_band);
2292
2293 Ok(PrbJsResult {
2294 values,
2295 rows: 3,
2296 cols: data.len(),
2297 })
2298}
2299
2300#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2301#[wasm_bindgen(js_name = "prb_batch")]
2302pub fn prb_batch_js(
2303 data: &[f64],
2304 smooth_data: bool,
2305 smooth_period_start: usize,
2306 smooth_period_end: usize,
2307 smooth_period_step: usize,
2308 regression_period_start: usize,
2309 regression_period_end: usize,
2310 regression_period_step: usize,
2311 polynomial_order_start: usize,
2312 polynomial_order_end: usize,
2313 polynomial_order_step: usize,
2314 regression_offset_start: i32,
2315 regression_offset_end: i32,
2316 regression_offset_step: i32,
2317) -> Result<JsValue, JsValue> {
2318 let sweep = PrbBatchRange {
2319 smooth_period: (smooth_period_start, smooth_period_end, smooth_period_step),
2320 regression_period: (
2321 regression_period_start,
2322 regression_period_end,
2323 regression_period_step,
2324 ),
2325 polynomial_order: (
2326 polynomial_order_start,
2327 polynomial_order_end,
2328 polynomial_order_step,
2329 ),
2330 regression_offset: (
2331 regression_offset_start,
2332 regression_offset_end,
2333 regression_offset_step,
2334 ),
2335 };
2336
2337 let out = prb_batch_slice(data, &sweep, detect_best_kernel(), smooth_data)
2338 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2339
2340 let mut values =
2341 Vec::with_capacity(out.values.len() + out.upper_band.len() + out.lower_band.len());
2342 values.extend_from_slice(&out.values);
2343 values.extend_from_slice(&out.upper_band);
2344 values.extend_from_slice(&out.lower_band);
2345
2346 let flat = PrbBatchFlatJs {
2347 values,
2348 rows: 3 * out.rows,
2349 cols: out.cols,
2350 combos: out.combos,
2351 };
2352 serde_wasm_bindgen::to_value(&flat)
2353 .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
2354}
2355
2356#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2357#[wasm_bindgen]
2358pub fn prb_alloc(len: usize) -> *mut f64 {
2359 let mut v = Vec::<f64>::with_capacity(len);
2360 let p = v.as_mut_ptr();
2361 core::mem::forget(v);
2362 p
2363}
2364
2365#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2366#[wasm_bindgen]
2367pub fn prb_free(ptr: *mut f64, len: usize) {
2368 unsafe {
2369 let _ = Vec::from_raw_parts(ptr, len, len);
2370 }
2371}
2372
2373#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2374#[wasm_bindgen]
2375pub fn prb_into(
2376 in_ptr: *const f64,
2377 out_main: *mut f64,
2378 out_upper: *mut f64,
2379 out_lower: *mut f64,
2380 len: usize,
2381 smooth_data: bool,
2382 smooth_period: usize,
2383 regression_period: usize,
2384 polynomial_order: usize,
2385 regression_offset: i32,
2386 ndev: f64,
2387) -> Result<(), JsValue> {
2388 if in_ptr.is_null() || out_main.is_null() || out_upper.is_null() || out_lower.is_null() {
2389 return Err(JsValue::from_str("null pointer passed to prb_into"));
2390 }
2391 unsafe {
2392 let data = core::slice::from_raw_parts(in_ptr, len);
2393 let mut m = core::slice::from_raw_parts_mut(out_main, len);
2394 let mut u = core::slice::from_raw_parts_mut(out_upper, len);
2395 let mut l = core::slice::from_raw_parts_mut(out_lower, len);
2396 let params = PrbParams {
2397 smooth_data: Some(smooth_data),
2398 smooth_period: Some(smooth_period),
2399 regression_period: Some(regression_period),
2400 polynomial_order: Some(polynomial_order),
2401 regression_offset: Some(regression_offset),
2402 ndev: Some(ndev),
2403 equ_from: Some(0),
2404 };
2405 let input = PrbInput::from_slice(data, params);
2406 prb_into_slice(&mut m, &mut u, &mut l, &input, detect_best_kernel())
2407 .map_err(|e| JsValue::from_str(&e.to_string()))
2408 }
2409}
2410
2411#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2412#[derive(Serialize, Deserialize)]
2413pub struct PrbBatchJsOutput {
2414 pub values: Vec<f64>,
2415 pub upper: Vec<f64>,
2416 pub lower: Vec<f64>,
2417 pub combos: Vec<PrbParams>,
2418 pub rows: usize,
2419 pub cols: usize,
2420}
2421
2422#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2423#[derive(Serialize, Deserialize)]
2424pub struct PrbBatchFlatJs {
2425 pub values: Vec<f64>,
2426 pub rows: usize,
2427 pub cols: usize,
2428 pub combos: Vec<PrbParams>,
2429}
2430
2431#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2432#[wasm_bindgen(js_name = "prb_batch_unified")]
2433pub fn prb_batch_unified_js(
2434 data: &[f64],
2435 config: JsValue,
2436 smooth_data: bool,
2437) -> Result<JsValue, JsValue> {
2438 #[derive(Serialize, Deserialize)]
2439 struct Cfg {
2440 smooth_period: (usize, usize, usize),
2441 regression_period: (usize, usize, usize),
2442 polynomial_order: (usize, usize, usize),
2443 regression_offset: (i32, i32, i32),
2444 }
2445 let cfg: Cfg = serde_wasm_bindgen::from_value(config)
2446 .map_err(|e| JsValue::from_str(&format!("Invalid config: {}", e)))?;
2447 let sweep = PrbBatchRange {
2448 smooth_period: cfg.smooth_period,
2449 regression_period: cfg.regression_period,
2450 polynomial_order: cfg.polynomial_order,
2451 regression_offset: cfg.regression_offset,
2452 };
2453 let out = prb_batch_inner(data, &sweep, detect_best_kernel(), smooth_data, false)
2454 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2455 let js = PrbBatchJsOutput {
2456 values: out.values,
2457 upper: out.upper_band,
2458 lower: out.lower_band,
2459 combos: out.combos,
2460 rows: out.rows,
2461 cols: out.cols,
2462 };
2463 serde_wasm_bindgen::to_value(&js)
2464 .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
2465}
2466
2467#[cfg(all(feature = "python", feature = "cuda"))]
2468use crate::cuda::{cuda_available, CudaPrb};
2469#[cfg(all(feature = "python", feature = "cuda"))]
2470use crate::indicators::moving_averages::alma::DeviceArrayF32Py;
2471#[cfg(all(feature = "python", feature = "cuda"))]
2472#[cfg(all(feature = "python", feature = "cuda"))]
2473use pyo3::{pyfunction, PyResult, Python};
2474#[cfg(all(feature = "python", feature = "cuda"))]
2475#[cfg(all(feature = "python", feature = "cuda"))]
2476#[pyfunction(name = "prb_cuda_batch_dev")]
2477#[pyo3(signature = (data_f32, smooth_data, smooth_period_range=(10,10,0), regression_period_range=(100,100,0), polynomial_order_range=(2,2,0), regression_offset_range=(0,0,0), device_id=0))]
2478pub fn prb_cuda_batch_dev_py(
2479 py: Python<'_>,
2480 data_f32: PyReadonlyArray1<'_, f32>,
2481 smooth_data: bool,
2482 smooth_period_range: (usize, usize, usize),
2483 regression_period_range: (usize, usize, usize),
2484 polynomial_order_range: (usize, usize, usize),
2485 regression_offset_range: (i32, i32, i32),
2486 device_id: usize,
2487) -> PyResult<(DeviceArrayF32Py, DeviceArrayF32Py, DeviceArrayF32Py)> {
2488 if !cuda_available() {
2489 return Err(PyValueError::new_err("CUDA not available"));
2490 }
2491 let slice = data_f32.as_slice()?;
2492 let sweep = PrbBatchRange {
2493 smooth_period: smooth_period_range,
2494 regression_period: regression_period_range,
2495 polynomial_order: polynomial_order_range,
2496 regression_offset: regression_offset_range,
2497 };
2498 let (main_d, up_d, lo_d, ctx, dev) = py.allow_threads(|| {
2499 let cuda = CudaPrb::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
2500 let ctx = cuda.context_arc();
2501 let dev = cuda.device_id();
2502 cuda.prb_batch_dev(slice, &sweep, smooth_data)
2503 .map(|(m, u, l)| (m, u, l, ctx, dev))
2504 .map_err(|e| PyValueError::new_err(e.to_string()))
2505 })?;
2506 Ok((
2507 DeviceArrayF32Py {
2508 inner: main_d,
2509 _ctx: Some(ctx.clone()),
2510 device_id: Some(dev),
2511 },
2512 DeviceArrayF32Py {
2513 inner: up_d,
2514 _ctx: Some(ctx.clone()),
2515 device_id: Some(dev),
2516 },
2517 DeviceArrayF32Py {
2518 inner: lo_d,
2519 _ctx: Some(ctx),
2520 device_id: Some(dev),
2521 },
2522 ))
2523}
2524
2525#[cfg(all(feature = "python", feature = "cuda"))]
2526#[pyfunction(name = "prb_cuda_many_series_one_param_dev")]
2527#[pyo3(signature = (prices_tm_f32, cols, rows, smooth_data, smooth_period, regression_period, polynomial_order, regression_offset, ndev=2.0, device_id=0))]
2528pub fn prb_cuda_many_series_one_param_dev_py(
2529 py: Python<'_>,
2530 prices_tm_f32: PyReadonlyArray1<'_, f32>,
2531 cols: usize,
2532 rows: usize,
2533 smooth_data: bool,
2534 smooth_period: usize,
2535 regression_period: usize,
2536 polynomial_order: usize,
2537 regression_offset: i32,
2538 ndev: f64,
2539 device_id: usize,
2540) -> PyResult<(DeviceArrayF32Py, DeviceArrayF32Py, DeviceArrayF32Py)> {
2541 if !cuda_available() {
2542 return Err(PyValueError::new_err("CUDA not available"));
2543 }
2544 let tm = prices_tm_f32.as_slice()?;
2545 let params = PrbParams {
2546 smooth_data: Some(smooth_data),
2547 smooth_period: Some(smooth_period),
2548 regression_period: Some(regression_period),
2549 polynomial_order: Some(polynomial_order),
2550 regression_offset: Some(regression_offset),
2551 ndev: Some(ndev),
2552 equ_from: Some(0),
2553 };
2554 let (m_d, u_d, l_d, ctx, dev) = py.allow_threads(|| {
2555 let cuda = CudaPrb::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
2556 let ctx = cuda.context_arc();
2557 let dev = cuda.device_id();
2558 cuda.prb_many_series_one_param_time_major_dev(tm, cols, rows, ¶ms)
2559 .map(|(m, u, l)| (m, u, l, ctx, dev))
2560 .map_err(|e| PyValueError::new_err(e.to_string()))
2561 })?;
2562 Ok((
2563 DeviceArrayF32Py {
2564 inner: m_d,
2565 _ctx: Some(ctx.clone()),
2566 device_id: Some(dev),
2567 },
2568 DeviceArrayF32Py {
2569 inner: u_d,
2570 _ctx: Some(ctx.clone()),
2571 device_id: Some(dev),
2572 },
2573 DeviceArrayF32Py {
2574 inner: l_d,
2575 _ctx: Some(ctx),
2576 device_id: Some(dev),
2577 },
2578 ))
2579}
2580
2581#[cfg(test)]
2582mod tests {
2583 use super::*;
2584 use crate::skip_if_unsupported;
2585 use crate::utilities::data_loader::read_candles_from_csv;
2586 use std::error::Error;
2587
2588 #[test]
2589 fn test_prb_into_matches_api() -> Result<(), Box<dyn Error>> {
2590 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2591 let candles = read_candles_from_csv(file_path)?;
2592
2593 let input = PrbInput::from_candles(&candles, "close", PrbParams::default());
2594
2595 let base = prb(&input)?;
2596 let len = candles.close.len();
2597
2598 let mut main = vec![0.0f64; len];
2599 let mut up = vec![0.0f64; len];
2600 let mut lo = vec![0.0f64; len];
2601
2602 #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
2603 {
2604 prb_into(&input, &mut main, &mut up, &mut lo)?;
2605 }
2606
2607 #[inline]
2608 fn eq_or_both_nan(a: f64, b: f64) -> bool {
2609 (a.is_nan() && b.is_nan()) || (a == b)
2610 }
2611
2612 assert_eq!(base.values.len(), len);
2613 assert_eq!(base.upper_band.len(), len);
2614 assert_eq!(base.lower_band.len(), len);
2615
2616 for i in 0..len {
2617 assert!(
2618 eq_or_both_nan(base.values[i], main[i]),
2619 "values mismatch at {}: got {}, expected {}",
2620 i,
2621 main[i],
2622 base.values[i]
2623 );
2624 assert!(
2625 eq_or_both_nan(base.upper_band[i], up[i]),
2626 "upper mismatch at {}: got {}, expected {}",
2627 i,
2628 up[i],
2629 base.upper_band[i]
2630 );
2631 assert!(
2632 eq_or_both_nan(base.lower_band[i], lo[i]),
2633 "lower mismatch at {}: got {}, expected {}",
2634 i,
2635 lo[i],
2636 base.lower_band[i]
2637 );
2638 }
2639
2640 Ok(())
2641 }
2642
2643 fn check_prb_accuracy(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2644 skip_if_unsupported!(kernel, test_name);
2645 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2646 let candles = read_candles_from_csv(file_path)?;
2647
2648 let params = PrbParams {
2649 smooth_data: Some(false),
2650 smooth_period: None,
2651 regression_period: Some(100),
2652 polynomial_order: Some(2),
2653 regression_offset: Some(0),
2654 ndev: Some(2.0),
2655 equ_from: Some(0),
2656 };
2657 let input = PrbInput::from_candles(&candles, "close", params);
2658 let result = prb_with_kernel(&input, kernel)?;
2659
2660 let expected_last_five = [
2661 59083.04826441,
2662 58900.06593477,
2663 58722.13172976,
2664 58575.33291206,
2665 58376.00589983,
2666 ];
2667
2668 let non_nan_values: Vec<f64> = result
2669 .values
2670 .iter()
2671 .filter(|v| !v.is_nan())
2672 .copied()
2673 .collect();
2674
2675 assert!(
2676 non_nan_values.len() >= 5,
2677 "[{}] Should have at least 5 non-NaN values",
2678 test_name
2679 );
2680
2681 let start = non_nan_values.len() - 5;
2682 for i in 0..5 {
2683 let actual = non_nan_values[start + i];
2684 let expected_val = expected_last_five[i];
2685 let diff = (actual - expected_val).abs();
2686 let tolerance = expected_val.abs() * 0.01;
2687
2688 assert!(
2689 diff < tolerance,
2690 "[{}] PRB {:?} mismatch at idx {}: got {}, expected {}",
2691 test_name,
2692 kernel,
2693 i,
2694 actual,
2695 expected_val
2696 );
2697 }
2698 Ok(())
2699 }
2700
2701 fn check_prb_partial_params(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2702 skip_if_unsupported!(kernel, test_name);
2703 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2704 let candles = read_candles_from_csv(file_path)?;
2705
2706 let params = PrbParams {
2707 smooth_data: None,
2708 smooth_period: None,
2709 regression_period: None,
2710 polynomial_order: None,
2711 regression_offset: None,
2712 ndev: None,
2713 equ_from: None,
2714 };
2715 let input = PrbInput::from_candles(&candles, "close", params);
2716 let output = prb_with_kernel(&input, kernel)?;
2717 assert_eq!(output.values.len(), candles.close.len());
2718
2719 Ok(())
2720 }
2721
2722 fn check_prb_zero_period(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2723 skip_if_unsupported!(kernel, test_name);
2724 let input_data = [10.0, 20.0, 30.0];
2725 let params = PrbParams {
2726 smooth_data: Some(false),
2727 smooth_period: None,
2728 regression_period: Some(0),
2729 polynomial_order: None,
2730 regression_offset: None,
2731 ndev: None,
2732 equ_from: None,
2733 };
2734 let input = PrbInput::from_slice(&input_data, params);
2735 let res = prb_with_kernel(&input, kernel);
2736 assert!(
2737 res.is_err(),
2738 "[{}] PRB should fail with zero period",
2739 test_name
2740 );
2741 Ok(())
2742 }
2743
2744 fn check_prb_period_exceeds_length(
2745 test_name: &str,
2746 kernel: Kernel,
2747 ) -> Result<(), Box<dyn Error>> {
2748 skip_if_unsupported!(kernel, test_name);
2749 let data_small = [10.0, 20.0, 30.0];
2750 let params = PrbParams {
2751 smooth_data: Some(false),
2752 smooth_period: None,
2753 regression_period: Some(10),
2754 polynomial_order: None,
2755 regression_offset: None,
2756 ndev: None,
2757 equ_from: None,
2758 };
2759 let input = PrbInput::from_slice(&data_small, params);
2760 let res = prb_with_kernel(&input, kernel);
2761 assert!(
2762 res.is_err(),
2763 "[{}] PRB should fail with period exceeding length",
2764 test_name
2765 );
2766 Ok(())
2767 }
2768
2769 fn check_prb_very_small_dataset(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2770 skip_if_unsupported!(kernel, test_name);
2771 let single_point = [42.0];
2772 let params = PrbParams {
2773 smooth_data: Some(false),
2774 smooth_period: None,
2775 regression_period: Some(100),
2776 polynomial_order: None,
2777 regression_offset: None,
2778 ndev: None,
2779 equ_from: None,
2780 };
2781 let input = PrbInput::from_slice(&single_point, params);
2782 let res = prb_with_kernel(&input, kernel);
2783 assert!(
2784 res.is_err(),
2785 "[{}] PRB should fail with insufficient data",
2786 test_name
2787 );
2788 Ok(())
2789 }
2790
2791 fn check_prb_empty_input(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2792 skip_if_unsupported!(kernel, test_name);
2793 let empty: [f64; 0] = [];
2794 let input = PrbInput::from_slice(&empty, PrbParams::default());
2795 let res = prb_with_kernel(&input, kernel);
2796 assert!(
2797 matches!(res, Err(PrbError::EmptyInputData)),
2798 "[{}] PRB should fail with empty input",
2799 test_name
2800 );
2801 Ok(())
2802 }
2803
2804 fn check_prb_invalid_smooth_period(
2805 test_name: &str,
2806 kernel: Kernel,
2807 ) -> Result<(), Box<dyn Error>> {
2808 skip_if_unsupported!(kernel, test_name);
2809 let data = [1.0, 2.0, 3.0, 4.0, 5.0];
2810 let params = PrbParams {
2811 smooth_data: Some(true),
2812 smooth_period: Some(1),
2813 regression_period: Some(2),
2814 polynomial_order: None,
2815 regression_offset: None,
2816 ndev: None,
2817 equ_from: None,
2818 };
2819 let input = PrbInput::from_slice(&data, params);
2820 let res = prb_with_kernel(&input, kernel);
2821 assert!(
2822 matches!(res, Err(PrbError::InvalidSmoothPeriod { .. })),
2823 "[{}] PRB should fail with invalid smooth period",
2824 test_name
2825 );
2826 Ok(())
2827 }
2828
2829 fn check_prb_invalid_order(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2830 skip_if_unsupported!(kernel, test_name);
2831 let data = [1.0, 2.0, 3.0, 4.0, 5.0];
2832 let params = PrbParams {
2833 smooth_data: Some(false),
2834 smooth_period: None,
2835 regression_period: Some(2),
2836 polynomial_order: Some(0),
2837 regression_offset: None,
2838 ndev: None,
2839 equ_from: None,
2840 };
2841 let input = PrbInput::from_slice(&data, params);
2842 let res = prb_with_kernel(&input, kernel);
2843 assert!(
2844 matches!(res, Err(PrbError::InvalidOrder { .. })),
2845 "[{}] PRB should fail with invalid polynomial order",
2846 test_name
2847 );
2848 Ok(())
2849 }
2850
2851 fn check_prb_reinput(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2852 skip_if_unsupported!(kernel, test_name);
2853 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2854 let candles = read_candles_from_csv(file_path)?;
2855
2856 let first_params = PrbParams {
2857 smooth_data: Some(false),
2858 smooth_period: None,
2859 regression_period: Some(50),
2860 polynomial_order: Some(2),
2861 regression_offset: None,
2862 ndev: None,
2863 equ_from: None,
2864 };
2865 let first_input = PrbInput::from_candles(&candles, "close", first_params);
2866 let first_result = prb_with_kernel(&first_input, kernel)?;
2867
2868 let second_params = PrbParams {
2869 smooth_data: Some(false),
2870 smooth_period: None,
2871 regression_period: Some(50),
2872 polynomial_order: Some(2),
2873 regression_offset: None,
2874 ndev: None,
2875 equ_from: None,
2876 };
2877 let second_input = PrbInput::from_slice(&first_result.values, second_params);
2878 let second_result = prb_with_kernel(&second_input, kernel)?;
2879
2880 assert_eq!(second_result.values.len(), first_result.values.len());
2881
2882 Ok(())
2883 }
2884
2885 fn check_prb_nan_handling(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2886 skip_if_unsupported!(kernel, test_name);
2887 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2888 let candles = read_candles_from_csv(file_path)?;
2889
2890 let input = PrbInput::from_candles(
2891 &candles,
2892 "close",
2893 PrbParams {
2894 smooth_data: Some(false),
2895 smooth_period: None,
2896 regression_period: Some(50),
2897 polynomial_order: Some(2),
2898 regression_offset: None,
2899 ndev: None,
2900 equ_from: None,
2901 },
2902 );
2903 let res = prb_with_kernel(&input, kernel)?;
2904 assert_eq!(res.values.len(), candles.close.len());
2905 if res.values.len() > 240 {
2906 for (i, &val) in res.values[240..].iter().enumerate() {
2907 assert!(
2908 !val.is_nan(),
2909 "[{}] Found unexpected NaN at out-index {}",
2910 test_name,
2911 240 + i
2912 );
2913 }
2914 }
2915 Ok(())
2916 }
2917
2918 fn check_prb_streaming(test_name: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
2919 let params = PrbParams {
2920 smooth_data: Some(false),
2921 smooth_period: None,
2922 regression_period: Some(10),
2923 polynomial_order: Some(2),
2924 regression_offset: None,
2925 ndev: None,
2926 equ_from: None,
2927 };
2928
2929 let mut stream = PrbStream::try_new(params)?;
2930
2931 for i in 1..=15 {
2932 let val = i as f64 * 10.0;
2933 let result = stream.update(val);
2934 if i >= 10 {
2935 assert!(
2936 result.is_some(),
2937 "[{}] Stream should produce output after warmup",
2938 test_name
2939 );
2940 } else {
2941 assert!(
2942 result.is_none(),
2943 "[{}] Stream should not produce output during warmup",
2944 test_name
2945 );
2946 }
2947 }
2948
2949 Ok(())
2950 }
2951
2952 fn check_prb_no_poison(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2953 skip_if_unsupported!(kernel, test_name);
2954
2955 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2956 let candles = read_candles_from_csv(file_path)?;
2957
2958 let params = PrbParams {
2959 smooth_data: Some(false),
2960 smooth_period: None,
2961 regression_period: Some(50),
2962 polynomial_order: Some(2),
2963 regression_offset: None,
2964 ndev: None,
2965 equ_from: None,
2966 };
2967 let input = PrbInput::from_candles(&candles, "close", params);
2968 let output = prb_with_kernel(&input, kernel)?;
2969
2970 #[cfg(debug_assertions)]
2971 {
2972 for arr in [
2973 &output.values[..],
2974 &output.upper_band[..],
2975 &output.lower_band[..],
2976 ] {
2977 for (idx, &val) in arr.iter().enumerate() {
2978 if val.is_nan() {
2979 continue;
2980 }
2981
2982 let bits = val.to_bits();
2983
2984 if bits == 0x11111111_11111111 {
2985 panic!(
2986 "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at index {}",
2987 test_name, val, bits, idx
2988 );
2989 }
2990
2991 if bits == 0x22222222_22222222 {
2992 panic!(
2993 "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at index {}",
2994 test_name, val, bits, idx
2995 );
2996 }
2997
2998 if bits == 0x33333333_33333333 {
2999 panic!(
3000 "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at index {}",
3001 test_name, val, bits, idx
3002 );
3003 }
3004 }
3005 }
3006 }
3007
3008 Ok(())
3009 }
3010
3011 fn check_batch_default_row(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
3012 skip_if_unsupported!(kernel, test);
3013 let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
3014 let c = read_candles_from_csv(file)?;
3015 let out = PrbBatchBuilder::new()
3016 .kernel(kernel)
3017 .smooth_data(false)
3018 .apply_candles(&c, "close")?;
3019
3020 let def = PrbParams {
3021 smooth_data: Some(false),
3022 ..PrbParams::default()
3023 };
3024 let row = out.values_for(&def).expect("default row missing");
3025 assert_eq!(row.len(), c.close.len());
3026 Ok(())
3027 }
3028
3029 fn check_batch_sweep(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
3030 skip_if_unsupported!(kernel, test);
3031 let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
3032 let c = read_candles_from_csv(file)?;
3033 let out = PrbBatchBuilder::new()
3034 .kernel(kernel)
3035 .smooth_data(false)
3036 .smooth_period_range(8, 12, 2)
3037 .regression_period_range(50, 60, 5)
3038 .polynomial_order_range(1, 3, 1)
3039 .regression_offset_range(0, 2, 1)
3040 .apply_candles(&c, "close")?;
3041 let expected = 3 * 3 * 3 * 3;
3042 assert_eq!(out.combos.len(), expected);
3043 assert_eq!(out.rows, expected);
3044 assert_eq!(out.cols, c.close.len());
3045 Ok(())
3046 }
3047
3048 #[cfg(debug_assertions)]
3049 fn check_batch_no_poison(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
3050 skip_if_unsupported!(kernel, test);
3051 let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
3052 let c = read_candles_from_csv(file)?;
3053 let out = PrbBatchBuilder::new()
3054 .kernel(kernel)
3055 .smooth_data(false)
3056 .regression_period_range(9, 12, 1)
3057 .apply_candles(&c, "close")?;
3058 for arr in [&out.values[..], &out.upper_band[..], &out.lower_band[..]] {
3059 for (idx, &v) in arr.iter().enumerate() {
3060 if v.is_nan() {
3061 continue;
3062 }
3063 let b = v.to_bits();
3064 assert!(
3065 b != 0x11111111_11111111
3066 && b != 0x22222222_22222222
3067 && b != 0x33333333_33333333,
3068 "[{}] poison at flat index {}",
3069 test,
3070 idx
3071 );
3072 }
3073 }
3074 Ok(())
3075 }
3076
3077 macro_rules! generate_all_prb_tests {
3078 ($($test_fn:ident),*) => {
3079 paste::paste! {
3080 $(
3081 #[test]
3082 fn [<$test_fn _scalar_f64>]() {
3083 let _ = $test_fn(stringify!([<$test_fn _scalar_f64>]), Kernel::Scalar);
3084 }
3085 )*
3086 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
3087 $(
3088 #[test]
3089 fn [<$test_fn _avx2_f64>]() {
3090 let _ = $test_fn(stringify!([<$test_fn _avx2_f64>]), Kernel::Avx2);
3091 }
3092 #[test]
3093 fn [<$test_fn _avx512_f64>]() {
3094 let _ = $test_fn(stringify!([<$test_fn _avx512_f64>]), Kernel::Avx512);
3095 }
3096 )*
3097 #[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
3098 $(
3099 #[test]
3100 fn [<$test_fn _simd128_f64>]() {
3101 let _ = $test_fn(stringify!([<$test_fn _simd128_f64>]), Kernel::Scalar);
3102 }
3103 )*
3104 }
3105 }
3106 }
3107
3108 generate_all_prb_tests!(
3109 check_prb_accuracy,
3110 check_prb_partial_params,
3111 check_prb_zero_period,
3112 check_prb_period_exceeds_length,
3113 check_prb_very_small_dataset,
3114 check_prb_empty_input,
3115 check_prb_invalid_smooth_period,
3116 check_prb_invalid_order,
3117 check_prb_reinput,
3118 check_prb_nan_handling,
3119 check_prb_streaming,
3120 check_prb_no_poison
3121 );
3122
3123 macro_rules! gen_batch_tests {
3124 ($fn_name:ident) => {
3125 paste::paste! {
3126 #[test] fn [<$fn_name _scalar>]() { let _ = $fn_name(stringify!([<$fn_name _scalar>]), Kernel::ScalarBatch); }
3127 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
3128 #[test] fn [<$fn_name _avx2>]() { let _ = $fn_name(stringify!([<$fn_name _avx2>]), Kernel::Avx2Batch); }
3129 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
3130 #[test] fn [<$fn_name _avx512>]() { let _ = $fn_name(stringify!([<$fn_name _avx512>]), Kernel::Avx512Batch); }
3131 #[test] fn [<$fn_name _auto_detect>]() { let _ = $fn_name(stringify!([<$fn_name _auto_detect>]), Kernel::Auto); }
3132 }
3133 };
3134 }
3135
3136 gen_batch_tests!(check_batch_default_row);
3137 gen_batch_tests!(check_batch_sweep);
3138 gen_batch_tests!(check_batch_no_poison);
3139}