Skip to main content

vector_ta/indicators/
prb.rs

1#[cfg(feature = "python")]
2use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
3#[cfg(feature = "python")]
4use pyo3::exceptions::PyValueError;
5#[cfg(feature = "python")]
6use pyo3::prelude::*;
7#[cfg(feature = "python")]
8use pyo3::types::PyDict;
9
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, &params)
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}