Skip to main content

vector_ta/indicators/
polynomial_regression_extrapolation.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, init_matrix_prefixes, make_uninit_matrix,
19};
20#[cfg(feature = "python")]
21use crate::utilities::kernel_validation::validate_kernel;
22#[cfg(not(target_arch = "wasm32"))]
23use rayon::prelude::*;
24use std::convert::AsRef;
25#[cfg(test)]
26use std::error::Error as StdError;
27use std::mem::{ManuallyDrop, MaybeUninit};
28use thiserror::Error;
29
30const DEFAULT_LENGTH: usize = 100;
31const DEFAULT_EXTRAPOLATE: usize = 10;
32const DEFAULT_DEGREE: usize = 3;
33const MAX_DEGREE: usize = 8;
34const SINGULAR_EPSILON: f64 = 1e-12;
35
36impl<'a> AsRef<[f64]> for PolynomialRegressionExtrapolationInput<'a> {
37    #[inline(always)]
38    fn as_ref(&self) -> &[f64] {
39        match &self.data {
40            PolynomialRegressionExtrapolationData::Slice(slice) => slice,
41            PolynomialRegressionExtrapolationData::Candles { candles, source } => {
42                source_type(candles, source)
43            }
44        }
45    }
46}
47
48#[derive(Debug, Clone)]
49pub enum PolynomialRegressionExtrapolationData<'a> {
50    Candles {
51        candles: &'a Candles,
52        source: &'a str,
53    },
54    Slice(&'a [f64]),
55}
56
57#[derive(Debug, Clone)]
58pub struct PolynomialRegressionExtrapolationOutput {
59    pub values: Vec<f64>,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
63#[cfg_attr(
64    all(target_arch = "wasm32", feature = "wasm"),
65    derive(Serialize, Deserialize)
66)]
67pub struct PolynomialRegressionExtrapolationParams {
68    pub length: Option<usize>,
69    pub extrapolate: Option<usize>,
70    pub degree: Option<usize>,
71}
72
73impl Default for PolynomialRegressionExtrapolationParams {
74    fn default() -> Self {
75        Self {
76            length: Some(DEFAULT_LENGTH),
77            extrapolate: Some(DEFAULT_EXTRAPOLATE),
78            degree: Some(DEFAULT_DEGREE),
79        }
80    }
81}
82
83#[derive(Debug, Clone)]
84pub struct PolynomialRegressionExtrapolationInput<'a> {
85    pub data: PolynomialRegressionExtrapolationData<'a>,
86    pub params: PolynomialRegressionExtrapolationParams,
87}
88
89impl<'a> PolynomialRegressionExtrapolationInput<'a> {
90    #[inline]
91    pub fn from_candles(
92        candles: &'a Candles,
93        source: &'a str,
94        params: PolynomialRegressionExtrapolationParams,
95    ) -> Self {
96        Self {
97            data: PolynomialRegressionExtrapolationData::Candles { candles, source },
98            params,
99        }
100    }
101
102    #[inline]
103    pub fn from_slice(slice: &'a [f64], params: PolynomialRegressionExtrapolationParams) -> Self {
104        Self {
105            data: PolynomialRegressionExtrapolationData::Slice(slice),
106            params,
107        }
108    }
109
110    #[inline]
111    pub fn with_default_candles(candles: &'a Candles) -> Self {
112        Self::from_candles(
113            candles,
114            "close",
115            PolynomialRegressionExtrapolationParams::default(),
116        )
117    }
118
119    #[inline]
120    pub fn get_length(&self) -> usize {
121        self.params.length.unwrap_or(DEFAULT_LENGTH)
122    }
123
124    #[inline]
125    pub fn get_extrapolate(&self) -> usize {
126        self.params.extrapolate.unwrap_or(DEFAULT_EXTRAPOLATE)
127    }
128
129    #[inline]
130    pub fn get_degree(&self) -> usize {
131        self.params.degree.unwrap_or(DEFAULT_DEGREE)
132    }
133}
134
135#[derive(Copy, Clone, Debug)]
136pub struct PolynomialRegressionExtrapolationBuilder {
137    length: Option<usize>,
138    extrapolate: Option<usize>,
139    degree: Option<usize>,
140    kernel: Kernel,
141}
142
143impl Default for PolynomialRegressionExtrapolationBuilder {
144    fn default() -> Self {
145        Self {
146            length: None,
147            extrapolate: None,
148            degree: None,
149            kernel: Kernel::Auto,
150        }
151    }
152}
153
154impl PolynomialRegressionExtrapolationBuilder {
155    #[inline(always)]
156    pub fn new() -> Self {
157        Self::default()
158    }
159
160    #[inline(always)]
161    pub fn length(mut self, length: usize) -> Self {
162        self.length = Some(length);
163        self
164    }
165
166    #[inline(always)]
167    pub fn extrapolate(mut self, extrapolate: usize) -> Self {
168        self.extrapolate = Some(extrapolate);
169        self
170    }
171
172    #[inline(always)]
173    pub fn degree(mut self, degree: usize) -> Self {
174        self.degree = Some(degree);
175        self
176    }
177
178    #[inline(always)]
179    pub fn kernel(mut self, kernel: Kernel) -> Self {
180        self.kernel = kernel;
181        self
182    }
183
184    #[inline(always)]
185    pub fn apply(
186        self,
187        candles: &Candles,
188    ) -> Result<PolynomialRegressionExtrapolationOutput, PolynomialRegressionExtrapolationError>
189    {
190        let params = PolynomialRegressionExtrapolationParams {
191            length: self.length,
192            extrapolate: self.extrapolate,
193            degree: self.degree,
194        };
195        let input = PolynomialRegressionExtrapolationInput::from_candles(candles, "close", params);
196        polynomial_regression_extrapolation_with_kernel(&input, self.kernel)
197    }
198
199    #[inline(always)]
200    pub fn apply_slice(
201        self,
202        data: &[f64],
203    ) -> Result<PolynomialRegressionExtrapolationOutput, PolynomialRegressionExtrapolationError>
204    {
205        let params = PolynomialRegressionExtrapolationParams {
206            length: self.length,
207            extrapolate: self.extrapolate,
208            degree: self.degree,
209        };
210        let input = PolynomialRegressionExtrapolationInput::from_slice(data, params);
211        polynomial_regression_extrapolation_with_kernel(&input, self.kernel)
212    }
213
214    #[inline(always)]
215    pub fn into_stream(
216        self,
217    ) -> Result<PolynomialRegressionExtrapolationStream, PolynomialRegressionExtrapolationError>
218    {
219        let params = PolynomialRegressionExtrapolationParams {
220            length: self.length,
221            extrapolate: self.extrapolate,
222            degree: self.degree,
223        };
224        PolynomialRegressionExtrapolationStream::try_new(params)
225    }
226}
227
228#[derive(Debug, Error)]
229pub enum PolynomialRegressionExtrapolationError {
230    #[error("polynomial_regression_extrapolation: Input data slice is empty.")]
231    EmptyInputData,
232    #[error("polynomial_regression_extrapolation: All values are NaN.")]
233    AllValuesNaN,
234    #[error(
235        "polynomial_regression_extrapolation: Invalid length: length = {length}, data length = {data_len}"
236    )]
237    InvalidLength { length: usize, data_len: usize },
238    #[error(
239        "polynomial_regression_extrapolation: Invalid degree: degree = {degree}, max = {max_degree}"
240    )]
241    InvalidDegree { degree: usize, max_degree: usize },
242    #[error(
243        "polynomial_regression_extrapolation: Degree exceeds length: degree = {degree}, length = {length}"
244    )]
245    DegreeExceedsLength { degree: usize, length: usize },
246    #[error(
247        "polynomial_regression_extrapolation: Not enough valid data: needed = {needed}, valid = {valid}"
248    )]
249    NotEnoughValidData { needed: usize, valid: usize },
250    #[error(
251        "polynomial_regression_extrapolation: Singular polynomial fit for length = {length}, degree = {degree}"
252    )]
253    SingularFit { length: usize, degree: usize },
254    #[error(
255        "polynomial_regression_extrapolation: Output length mismatch: expected = {expected}, got = {got}"
256    )]
257    OutputLengthMismatch { expected: usize, got: usize },
258    #[error(
259        "polynomial_regression_extrapolation: Invalid range for {axis}: start = {start}, end = {end}, step = {step}"
260    )]
261    InvalidRange {
262        axis: &'static str,
263        start: usize,
264        end: usize,
265        step: usize,
266    },
267    #[error("polynomial_regression_extrapolation: Invalid kernel for batch: {0:?}")]
268    InvalidKernelForBatch(Kernel),
269}
270
271#[derive(Clone)]
272struct PreparedPolynomialRegressionExtrapolation<'a> {
273    data: &'a [f64],
274    first: usize,
275    length: usize,
276    weights: Vec<f64>,
277    kernel: Kernel,
278}
279
280#[derive(Clone)]
281struct BatchRowSpec {
282    params: PolynomialRegressionExtrapolationParams,
283    length: usize,
284    weights: Vec<f64>,
285}
286
287#[inline]
288pub fn polynomial_regression_extrapolation(
289    input: &PolynomialRegressionExtrapolationInput,
290) -> Result<PolynomialRegressionExtrapolationOutput, PolynomialRegressionExtrapolationError> {
291    polynomial_regression_extrapolation_with_kernel(input, Kernel::Auto)
292}
293
294#[inline(always)]
295fn normalize_single_kernel(_kernel: Kernel) -> Kernel {
296    Kernel::Scalar
297}
298
299fn solve_dense_system_in_place(matrix: &mut [f64], rhs: &mut [f64], n: usize) -> Result<(), ()> {
300    for pivot_col in 0..n {
301        let mut pivot_row = pivot_col;
302        let mut pivot_abs = matrix[pivot_col * n + pivot_col].abs();
303        for row in (pivot_col + 1)..n {
304            let candidate = matrix[row * n + pivot_col].abs();
305            if candidate > pivot_abs {
306                pivot_abs = candidate;
307                pivot_row = row;
308            }
309        }
310        if pivot_abs <= SINGULAR_EPSILON {
311            return Err(());
312        }
313        if pivot_row != pivot_col {
314            for col in pivot_col..n {
315                matrix.swap(pivot_col * n + col, pivot_row * n + col);
316            }
317            rhs.swap(pivot_col, pivot_row);
318        }
319        let pivot = matrix[pivot_col * n + pivot_col];
320        for row in (pivot_col + 1)..n {
321            let factor = matrix[row * n + pivot_col] / pivot;
322            if factor == 0.0 {
323                continue;
324            }
325            matrix[row * n + pivot_col] = 0.0;
326            for col in (pivot_col + 1)..n {
327                matrix[row * n + col] -= factor * matrix[pivot_col * n + col];
328            }
329            rhs[row] -= factor * rhs[pivot_col];
330        }
331    }
332
333    for row in (0..n).rev() {
334        let mut acc = rhs[row];
335        for col in (row + 1)..n {
336            acc -= matrix[row * n + col] * rhs[col];
337        }
338        let pivot = matrix[row * n + row];
339        if pivot.abs() <= SINGULAR_EPSILON {
340            return Err(());
341        }
342        rhs[row] = acc / pivot;
343    }
344    Ok(())
345}
346
347fn build_forecast_weights(
348    length: usize,
349    extrapolate: usize,
350    degree: usize,
351) -> Result<Vec<f64>, PolynomialRegressionExtrapolationError> {
352    if degree > MAX_DEGREE {
353        return Err(PolynomialRegressionExtrapolationError::InvalidDegree {
354            degree,
355            max_degree: MAX_DEGREE,
356        });
357    }
358    if length == 0 {
359        return Err(PolynomialRegressionExtrapolationError::InvalidLength {
360            length,
361            data_len: 0,
362        });
363    }
364    if degree + 1 > length {
365        return Err(PolynomialRegressionExtrapolationError::DegreeExceedsLength { degree, length });
366    }
367
368    let order_count = degree + 1;
369    let mut normal = vec![0.0; order_count * order_count];
370    for row in 0..order_count {
371        for col in 0..order_count {
372            let power = row + col;
373            let mut sum = 0.0;
374            for x in 0..length {
375                sum += (x as f64).powi(power as i32);
376            }
377            normal[row * order_count + col] = sum;
378        }
379    }
380
381    let x_eval = -(extrapolate as f64);
382    let mut rhs = vec![0.0; order_count];
383    for (power, value) in rhs.iter_mut().enumerate() {
384        *value = x_eval.powi(power as i32);
385    }
386    solve_dense_system_in_place(&mut normal, &mut rhs, order_count)
387        .map_err(|_| PolynomialRegressionExtrapolationError::SingularFit { length, degree })?;
388
389    let mut weights = vec![0.0; length];
390    for (x, weight) in weights.iter_mut().enumerate() {
391        let xf = x as f64;
392        let mut acc = 0.0f64;
393        for power in (0..order_count).rev() {
394            acc = acc.mul_add(xf, rhs[power]);
395        }
396        *weight = acc;
397    }
398    Ok(weights)
399}
400
401fn polynomial_regression_extrapolation_prepare<'a>(
402    input: &'a PolynomialRegressionExtrapolationInput,
403    kernel: Kernel,
404) -> Result<PreparedPolynomialRegressionExtrapolation<'a>, PolynomialRegressionExtrapolationError> {
405    let data = input.as_ref();
406    if data.is_empty() {
407        return Err(PolynomialRegressionExtrapolationError::EmptyInputData);
408    }
409    let first = data
410        .iter()
411        .position(|value| !value.is_nan())
412        .ok_or(PolynomialRegressionExtrapolationError::AllValuesNaN)?;
413
414    let length = input.get_length();
415    if length == 0 || length > data.len() {
416        return Err(PolynomialRegressionExtrapolationError::InvalidLength {
417            length,
418            data_len: data.len(),
419        });
420    }
421
422    let valid = data.len() - first;
423    if valid < length {
424        return Err(PolynomialRegressionExtrapolationError::NotEnoughValidData {
425            needed: length,
426            valid,
427        });
428    }
429
430    let weights = build_forecast_weights(length, input.get_extrapolate(), input.get_degree())?;
431    Ok(PreparedPolynomialRegressionExtrapolation {
432        data,
433        first,
434        length,
435        weights,
436        kernel: normalize_single_kernel(kernel),
437    })
438}
439
440#[inline(always)]
441fn polynomial_regression_extrapolation_scalar(
442    data: &[f64],
443    first: usize,
444    length: usize,
445    weights: &[f64],
446    out: &mut [f64],
447) {
448    let mut valid_run = 0usize;
449    for idx in first..data.len() {
450        let value = data[idx];
451        if value.is_nan() {
452            valid_run = 0;
453            out[idx] = f64::NAN;
454            continue;
455        }
456
457        valid_run += 1;
458        if valid_run < length {
459            out[idx] = f64::NAN;
460            continue;
461        }
462
463        let mut acc = 0.0;
464        for offset in 0..length {
465            acc += weights[offset] * data[idx - offset];
466        }
467        out[idx] = acc;
468    }
469}
470
471#[inline(always)]
472fn polynomial_regression_extrapolation_compute_into(
473    prepared: &PreparedPolynomialRegressionExtrapolation,
474    out: &mut [f64],
475) {
476    match prepared.kernel {
477        Kernel::Scalar => polynomial_regression_extrapolation_scalar(
478            prepared.data,
479            prepared.first,
480            prepared.length,
481            &prepared.weights,
482            out,
483        ),
484        _ => unreachable!(),
485    }
486}
487
488pub fn polynomial_regression_extrapolation_with_kernel(
489    input: &PolynomialRegressionExtrapolationInput,
490    kernel: Kernel,
491) -> Result<PolynomialRegressionExtrapolationOutput, PolynomialRegressionExtrapolationError> {
492    let prepared = polynomial_regression_extrapolation_prepare(input, kernel)?;
493    let warmup = prepared.first + prepared.length - 1;
494    let mut out = alloc_with_nan_prefix(prepared.data.len(), warmup);
495    polynomial_regression_extrapolation_compute_into(&prepared, &mut out);
496    Ok(PolynomialRegressionExtrapolationOutput { values: out })
497}
498
499#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
500#[inline]
501pub fn polynomial_regression_extrapolation_into(
502    input: &PolynomialRegressionExtrapolationInput,
503    out: &mut [f64],
504) -> Result<(), PolynomialRegressionExtrapolationError> {
505    polynomial_regression_extrapolation_into_slice(out, input, Kernel::Auto)
506}
507
508#[inline]
509pub fn polynomial_regression_extrapolation_into_slice(
510    dst: &mut [f64],
511    input: &PolynomialRegressionExtrapolationInput,
512    kernel: Kernel,
513) -> Result<(), PolynomialRegressionExtrapolationError> {
514    let prepared = polynomial_regression_extrapolation_prepare(input, kernel)?;
515    if dst.len() != prepared.data.len() {
516        return Err(
517            PolynomialRegressionExtrapolationError::OutputLengthMismatch {
518                expected: prepared.data.len(),
519                got: dst.len(),
520            },
521        );
522    }
523    let warmup = prepared.first + prepared.length - 1;
524    for value in &mut dst[..warmup] {
525        *value = f64::NAN;
526    }
527    polynomial_regression_extrapolation_compute_into(&prepared, dst);
528    Ok(())
529}
530
531#[derive(Debug, Clone)]
532pub struct PolynomialRegressionExtrapolationStream {
533    weights: Vec<f64>,
534    buffer: Vec<f64>,
535    head: usize,
536    count: usize,
537    valid_count: usize,
538}
539
540impl PolynomialRegressionExtrapolationStream {
541    pub fn try_new(
542        params: PolynomialRegressionExtrapolationParams,
543    ) -> Result<Self, PolynomialRegressionExtrapolationError> {
544        let length = params.length.unwrap_or(DEFAULT_LENGTH);
545        let extrapolate = params.extrapolate.unwrap_or(DEFAULT_EXTRAPOLATE);
546        let degree = params.degree.unwrap_or(DEFAULT_DEGREE);
547        let weights = build_forecast_weights(length, extrapolate, degree)?;
548        Ok(Self {
549            weights,
550            buffer: vec![f64::NAN; length],
551            head: 0,
552            count: 0,
553            valid_count: 0,
554        })
555    }
556
557    #[inline(always)]
558    pub fn update(&mut self, value: f64) -> Option<f64> {
559        let len = self.buffer.len();
560        if self.count == len {
561            let old = self.buffer[self.head];
562            if !old.is_nan() {
563                self.valid_count = self.valid_count.saturating_sub(1);
564            }
565        } else {
566            self.count += 1;
567        }
568
569        self.buffer[self.head] = value;
570        if !value.is_nan() {
571            self.valid_count += 1;
572        }
573        self.head += 1;
574        if self.head == len {
575            self.head = 0;
576        }
577
578        if self.count < len {
579            return None;
580        }
581        if self.valid_count < len {
582            return Some(f64::NAN);
583        }
584
585        let mut idx = if self.head == 0 {
586            len - 1
587        } else {
588            self.head - 1
589        };
590        let mut acc = 0.0;
591        for weight in &self.weights {
592            acc += *weight * self.buffer[idx];
593            if idx == 0 {
594                idx = len - 1;
595            } else {
596                idx -= 1;
597            }
598        }
599        Some(acc)
600    }
601}
602
603#[derive(Clone, Debug)]
604pub struct PolynomialRegressionExtrapolationBatchRange {
605    pub length: (usize, usize, usize),
606    pub extrapolate: (usize, usize, usize),
607    pub degree: (usize, usize, usize),
608}
609
610impl Default for PolynomialRegressionExtrapolationBatchRange {
611    fn default() -> Self {
612        Self {
613            length: (DEFAULT_LENGTH, DEFAULT_LENGTH, 0),
614            extrapolate: (DEFAULT_EXTRAPOLATE, DEFAULT_EXTRAPOLATE, 0),
615            degree: (DEFAULT_DEGREE, DEFAULT_DEGREE, 0),
616        }
617    }
618}
619
620#[derive(Clone, Debug, Default)]
621pub struct PolynomialRegressionExtrapolationBatchBuilder {
622    range: PolynomialRegressionExtrapolationBatchRange,
623    kernel: Kernel,
624}
625
626impl PolynomialRegressionExtrapolationBatchBuilder {
627    pub fn new() -> Self {
628        Self::default()
629    }
630
631    pub fn kernel(mut self, kernel: Kernel) -> Self {
632        self.kernel = kernel;
633        self
634    }
635
636    pub fn length_range(mut self, start: usize, end: usize, step: usize) -> Self {
637        self.range.length = (start, end, step);
638        self
639    }
640
641    pub fn extrapolate_range(mut self, start: usize, end: usize, step: usize) -> Self {
642        self.range.extrapolate = (start, end, step);
643        self
644    }
645
646    pub fn degree_range(mut self, start: usize, end: usize, step: usize) -> Self {
647        self.range.degree = (start, end, step);
648        self
649    }
650
651    pub fn length_static(mut self, value: usize) -> Self {
652        self.range.length = (value, value, 0);
653        self
654    }
655
656    pub fn extrapolate_static(mut self, value: usize) -> Self {
657        self.range.extrapolate = (value, value, 0);
658        self
659    }
660
661    pub fn degree_static(mut self, value: usize) -> Self {
662        self.range.degree = (value, value, 0);
663        self
664    }
665
666    pub fn apply_slice(
667        self,
668        data: &[f64],
669    ) -> Result<PolynomialRegressionExtrapolationBatchOutput, PolynomialRegressionExtrapolationError>
670    {
671        polynomial_regression_extrapolation_batch_with_kernel(data, &self.range, self.kernel)
672    }
673
674    pub fn apply_candles(
675        self,
676        candles: &Candles,
677        source: &str,
678    ) -> Result<PolynomialRegressionExtrapolationBatchOutput, PolynomialRegressionExtrapolationError>
679    {
680        self.apply_slice(source_type(candles, source))
681    }
682}
683
684#[derive(Clone, Debug)]
685pub struct PolynomialRegressionExtrapolationBatchOutput {
686    pub values: Vec<f64>,
687    pub combos: Vec<PolynomialRegressionExtrapolationParams>,
688    pub rows: usize,
689    pub cols: usize,
690}
691
692impl PolynomialRegressionExtrapolationBatchOutput {
693    pub fn row_for_params(
694        &self,
695        params: &PolynomialRegressionExtrapolationParams,
696    ) -> Option<usize> {
697        self.combos.iter().position(|combo| combo == params)
698    }
699
700    pub fn values_for(&self, params: &PolynomialRegressionExtrapolationParams) -> Option<&[f64]> {
701        self.row_for_params(params).and_then(|row| {
702            let start = row.checked_mul(self.cols)?;
703            let end = start.checked_add(self.cols)?;
704            self.values.get(start..end)
705        })
706    }
707}
708
709fn axis_values(
710    axis: &'static str,
711    range: (usize, usize, usize),
712) -> Result<Vec<usize>, PolynomialRegressionExtrapolationError> {
713    let (start, end, step) = range;
714    if step == 0 || start == end {
715        return Ok(vec![start]);
716    }
717
718    let mut out = Vec::new();
719    if start < end {
720        let mut current = start;
721        while current <= end {
722            out.push(current);
723            match current.checked_add(step) {
724                Some(next) if next > current => current = next,
725                _ => break,
726            }
727        }
728    } else {
729        let mut current = start;
730        while current >= end {
731            out.push(current);
732            if current == end {
733                break;
734            }
735            match current.checked_sub(step) {
736                Some(next) if next < current => current = next,
737                _ => break,
738            }
739        }
740    }
741
742    if out.is_empty() || !out.last().is_some_and(|value| *value == end) {
743        return Err(PolynomialRegressionExtrapolationError::InvalidRange {
744            axis,
745            start,
746            end,
747            step,
748        });
749    }
750    Ok(out)
751}
752
753pub(crate) fn expand_grid(
754    range: &PolynomialRegressionExtrapolationBatchRange,
755) -> Result<Vec<PolynomialRegressionExtrapolationParams>, PolynomialRegressionExtrapolationError> {
756    let lengths = axis_values("length", range.length)?;
757    let extrapolates = axis_values("extrapolate", range.extrapolate)?;
758    let degrees = axis_values("degree", range.degree)?;
759
760    let total = lengths
761        .len()
762        .checked_mul(extrapolates.len())
763        .and_then(|value| value.checked_mul(degrees.len()))
764        .ok_or(PolynomialRegressionExtrapolationError::InvalidRange {
765            axis: "grid",
766            start: lengths.len(),
767            end: extrapolates.len(),
768            step: degrees.len(),
769        })?;
770
771    let mut out = Vec::with_capacity(total);
772    for &length in &lengths {
773        for &extrapolate in &extrapolates {
774            for &degree in &degrees {
775                out.push(PolynomialRegressionExtrapolationParams {
776                    length: Some(length),
777                    extrapolate: Some(extrapolate),
778                    degree: Some(degree),
779                });
780            }
781        }
782    }
783    Ok(out)
784}
785
786fn prepare_batch_specs(
787    data: &[f64],
788    sweep: &PolynomialRegressionExtrapolationBatchRange,
789) -> Result<(usize, Vec<BatchRowSpec>), PolynomialRegressionExtrapolationError> {
790    if data.is_empty() {
791        return Err(PolynomialRegressionExtrapolationError::EmptyInputData);
792    }
793
794    let first = data
795        .iter()
796        .position(|value| !value.is_nan())
797        .ok_or(PolynomialRegressionExtrapolationError::AllValuesNaN)?;
798    let valid = data.len() - first;
799
800    let combos = expand_grid(sweep)?;
801    let mut max_length = 0usize;
802    let mut specs = Vec::with_capacity(combos.len());
803    for params in combos {
804        let length = params.length.unwrap_or(DEFAULT_LENGTH);
805        if length == 0 || length > data.len() {
806            return Err(PolynomialRegressionExtrapolationError::InvalidLength {
807                length,
808                data_len: data.len(),
809            });
810        }
811        if valid < length {
812            return Err(PolynomialRegressionExtrapolationError::NotEnoughValidData {
813                needed: length,
814                valid,
815            });
816        }
817        let weights = build_forecast_weights(
818            length,
819            params.extrapolate.unwrap_or(DEFAULT_EXTRAPOLATE),
820            params.degree.unwrap_or(DEFAULT_DEGREE),
821        )?;
822        max_length = max_length.max(length);
823        specs.push(BatchRowSpec {
824            params,
825            length,
826            weights,
827        });
828    }
829
830    if valid < max_length {
831        return Err(PolynomialRegressionExtrapolationError::NotEnoughValidData {
832            needed: max_length,
833            valid,
834        });
835    }
836
837    Ok((first, specs))
838}
839
840pub fn polynomial_regression_extrapolation_batch_with_kernel(
841    data: &[f64],
842    sweep: &PolynomialRegressionExtrapolationBatchRange,
843    kernel: Kernel,
844) -> Result<PolynomialRegressionExtrapolationBatchOutput, PolynomialRegressionExtrapolationError> {
845    let kernel = match kernel {
846        Kernel::Auto => detect_best_batch_kernel(),
847        other if other.is_batch() => other,
848        other => return Err(PolynomialRegressionExtrapolationError::InvalidKernelForBatch(other)),
849    };
850
851    let simd = match kernel {
852        Kernel::ScalarBatch | Kernel::Avx2Batch | Kernel::Avx512Batch => Kernel::Scalar,
853        _ => unreachable!(),
854    };
855    polynomial_regression_extrapolation_batch_par_slice(data, sweep, simd)
856}
857
858#[inline(always)]
859pub fn polynomial_regression_extrapolation_batch_slice(
860    data: &[f64],
861    sweep: &PolynomialRegressionExtrapolationBatchRange,
862    kernel: Kernel,
863) -> Result<PolynomialRegressionExtrapolationBatchOutput, PolynomialRegressionExtrapolationError> {
864    polynomial_regression_extrapolation_batch_inner(data, sweep, kernel, false)
865}
866
867#[inline(always)]
868pub fn polynomial_regression_extrapolation_batch_par_slice(
869    data: &[f64],
870    sweep: &PolynomialRegressionExtrapolationBatchRange,
871    kernel: Kernel,
872) -> Result<PolynomialRegressionExtrapolationBatchOutput, PolynomialRegressionExtrapolationError> {
873    polynomial_regression_extrapolation_batch_inner(data, sweep, kernel, true)
874}
875
876fn polynomial_regression_extrapolation_batch_inner(
877    data: &[f64],
878    sweep: &PolynomialRegressionExtrapolationBatchRange,
879    _kernel: Kernel,
880    parallel: bool,
881) -> Result<PolynomialRegressionExtrapolationBatchOutput, PolynomialRegressionExtrapolationError> {
882    let (first, specs) = prepare_batch_specs(data, sweep)?;
883    let rows = specs.len();
884    let cols = data.len();
885    let mut buf_mu = make_uninit_matrix(rows, cols);
886    let warm_prefixes: Vec<usize> = specs.iter().map(|spec| first + spec.length - 1).collect();
887    init_matrix_prefixes(&mut buf_mu, cols, &warm_prefixes);
888
889    let mut buf_guard = ManuallyDrop::new(buf_mu);
890    let out: &mut [f64] = unsafe {
891        core::slice::from_raw_parts_mut(buf_guard.as_mut_ptr() as *mut f64, buf_guard.len())
892    };
893
894    let do_row = |row: usize, out_row: &mut [f64]| {
895        let spec = &specs[row];
896        polynomial_regression_extrapolation_scalar(
897            data,
898            first,
899            spec.length,
900            &spec.weights,
901            out_row,
902        );
903    };
904
905    if parallel {
906        #[cfg(not(target_arch = "wasm32"))]
907        {
908            out.par_chunks_mut(cols)
909                .enumerate()
910                .for_each(|(row, chunk)| do_row(row, chunk));
911        }
912        #[cfg(target_arch = "wasm32")]
913        {
914            for (row, chunk) in out.chunks_mut(cols).enumerate() {
915                do_row(row, chunk);
916            }
917        }
918    } else {
919        for (row, chunk) in out.chunks_mut(cols).enumerate() {
920            do_row(row, chunk);
921        }
922    }
923
924    let values = unsafe {
925        Vec::from_raw_parts(
926            buf_guard.as_mut_ptr() as *mut f64,
927            buf_guard.len(),
928            buf_guard.capacity(),
929        )
930    };
931    let combos = specs.into_iter().map(|spec| spec.params).collect();
932    Ok(PolynomialRegressionExtrapolationBatchOutput {
933        values,
934        combos,
935        rows,
936        cols,
937    })
938}
939
940fn polynomial_regression_extrapolation_batch_inner_into(
941    data: &[f64],
942    sweep: &PolynomialRegressionExtrapolationBatchRange,
943    kernel: Kernel,
944    parallel: bool,
945    out: &mut [f64],
946) -> Result<Vec<PolynomialRegressionExtrapolationParams>, PolynomialRegressionExtrapolationError> {
947    let (first, specs) = prepare_batch_specs(data, sweep)?;
948    let rows = specs.len();
949    let cols = data.len();
950    let expected = rows.checked_mul(cols).ok_or(
951        PolynomialRegressionExtrapolationError::OutputLengthMismatch {
952            expected: usize::MAX,
953            got: out.len(),
954        },
955    )?;
956    if out.len() != expected {
957        return Err(
958            PolynomialRegressionExtrapolationError::OutputLengthMismatch {
959                expected,
960                got: out.len(),
961            },
962        );
963    }
964
965    let out_mu: &mut [MaybeUninit<f64>] = unsafe {
966        core::slice::from_raw_parts_mut(out.as_mut_ptr() as *mut MaybeUninit<f64>, out.len())
967    };
968    let warm_prefixes: Vec<usize> = specs.iter().map(|spec| first + spec.length - 1).collect();
969    init_matrix_prefixes(out_mu, cols, &warm_prefixes);
970
971    let do_row = |row: usize, row_mu: &mut [MaybeUninit<f64>]| {
972        let spec = &specs[row];
973        let dst: &mut [f64] = unsafe {
974            core::slice::from_raw_parts_mut(row_mu.as_mut_ptr() as *mut f64, row_mu.len())
975        };
976        match kernel {
977            Kernel::Scalar => polynomial_regression_extrapolation_scalar(
978                data,
979                first,
980                spec.length,
981                &spec.weights,
982                dst,
983            ),
984            _ => unreachable!(),
985        }
986    };
987
988    if parallel {
989        #[cfg(not(target_arch = "wasm32"))]
990        {
991            out_mu
992                .par_chunks_mut(cols)
993                .enumerate()
994                .for_each(|(row, row_mu)| do_row(row, row_mu));
995        }
996        #[cfg(target_arch = "wasm32")]
997        {
998            for (row, row_mu) in out_mu.chunks_mut(cols).enumerate() {
999                do_row(row, row_mu);
1000            }
1001        }
1002    } else {
1003        for (row, row_mu) in out_mu.chunks_mut(cols).enumerate() {
1004            do_row(row, row_mu);
1005        }
1006    }
1007
1008    Ok(specs.into_iter().map(|spec| spec.params).collect())
1009}
1010
1011#[cfg(feature = "python")]
1012#[pyfunction(name = "polynomial_regression_extrapolation")]
1013#[pyo3(signature = (data, length=DEFAULT_LENGTH, extrapolate=DEFAULT_EXTRAPOLATE, degree=DEFAULT_DEGREE, kernel=None))]
1014pub fn polynomial_regression_extrapolation_py<'py>(
1015    py: Python<'py>,
1016    data: PyReadonlyArray1<'py, f64>,
1017    length: usize,
1018    extrapolate: usize,
1019    degree: usize,
1020    kernel: Option<&str>,
1021) -> PyResult<Bound<'py, PyArray1<f64>>> {
1022    let slice_in = data.as_slice()?;
1023    let kern = validate_kernel(kernel, false)?;
1024    let input = PolynomialRegressionExtrapolationInput::from_slice(
1025        slice_in,
1026        PolynomialRegressionExtrapolationParams {
1027            length: Some(length),
1028            extrapolate: Some(extrapolate),
1029            degree: Some(degree),
1030        },
1031    );
1032    let values = py
1033        .allow_threads(|| {
1034            polynomial_regression_extrapolation_with_kernel(&input, kern)
1035                .map(|output| output.values)
1036        })
1037        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1038    Ok(values.into_pyarray(py))
1039}
1040
1041#[cfg(feature = "python")]
1042#[pyfunction(name = "polynomial_regression_extrapolation_batch")]
1043#[pyo3(signature = (data, length_range=(DEFAULT_LENGTH, DEFAULT_LENGTH, 0), extrapolate_range=(DEFAULT_EXTRAPOLATE, DEFAULT_EXTRAPOLATE, 0), degree_range=(DEFAULT_DEGREE, DEFAULT_DEGREE, 0), kernel=None))]
1044pub fn polynomial_regression_extrapolation_batch_py<'py>(
1045    py: Python<'py>,
1046    data: PyReadonlyArray1<'py, f64>,
1047    length_range: (usize, usize, usize),
1048    extrapolate_range: (usize, usize, usize),
1049    degree_range: (usize, usize, usize),
1050    kernel: Option<&str>,
1051) -> PyResult<Bound<'py, PyDict>> {
1052    let slice_in = data.as_slice()?;
1053    let kern = validate_kernel(kernel, true)?;
1054    let sweep = PolynomialRegressionExtrapolationBatchRange {
1055        length: length_range,
1056        extrapolate: extrapolate_range,
1057        degree: degree_range,
1058    };
1059    let combos = expand_grid(&sweep).map_err(|e| PyValueError::new_err(e.to_string()))?;
1060    let rows = combos.len();
1061    let cols = slice_in.len();
1062    let total = rows
1063        .checked_mul(cols)
1064        .ok_or_else(|| PyValueError::new_err("rows*cols overflow"))?;
1065
1066    let out_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
1067    let slice_out = unsafe { out_arr.as_slice_mut()? };
1068    let combos = py
1069        .allow_threads(|| {
1070            let batch_kernel = match kern {
1071                Kernel::Auto => detect_best_batch_kernel(),
1072                other => other,
1073            };
1074            let simd = match batch_kernel {
1075                Kernel::ScalarBatch | Kernel::Avx2Batch | Kernel::Avx512Batch => Kernel::Scalar,
1076                _ => unreachable!(),
1077            };
1078            polynomial_regression_extrapolation_batch_inner_into(
1079                slice_in, &sweep, simd, true, slice_out,
1080            )
1081        })
1082        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1083
1084    let dict = PyDict::new(py);
1085    dict.set_item("values", out_arr.reshape((rows, cols))?)?;
1086    dict.set_item(
1087        "lengths",
1088        combos
1089            .iter()
1090            .map(|params| params.length.unwrap_or(DEFAULT_LENGTH) as u64)
1091            .collect::<Vec<_>>()
1092            .into_pyarray(py),
1093    )?;
1094    dict.set_item(
1095        "extrapolates",
1096        combos
1097            .iter()
1098            .map(|params| params.extrapolate.unwrap_or(DEFAULT_EXTRAPOLATE) as u64)
1099            .collect::<Vec<_>>()
1100            .into_pyarray(py),
1101    )?;
1102    dict.set_item(
1103        "degrees",
1104        combos
1105            .iter()
1106            .map(|params| params.degree.unwrap_or(DEFAULT_DEGREE) as u64)
1107            .collect::<Vec<_>>()
1108            .into_pyarray(py),
1109    )?;
1110    dict.set_item("rows", rows)?;
1111    dict.set_item("cols", cols)?;
1112    Ok(dict)
1113}
1114
1115#[cfg(feature = "python")]
1116#[pyclass(name = "PolynomialRegressionExtrapolationStream")]
1117pub struct PolynomialRegressionExtrapolationStreamPy {
1118    inner: PolynomialRegressionExtrapolationStream,
1119}
1120
1121#[cfg(feature = "python")]
1122#[pymethods]
1123impl PolynomialRegressionExtrapolationStreamPy {
1124    #[new]
1125    #[pyo3(signature = (length=DEFAULT_LENGTH, extrapolate=DEFAULT_EXTRAPOLATE, degree=DEFAULT_DEGREE))]
1126    pub fn new(length: usize, extrapolate: usize, degree: usize) -> PyResult<Self> {
1127        let inner = PolynomialRegressionExtrapolationStream::try_new(
1128            PolynomialRegressionExtrapolationParams {
1129                length: Some(length),
1130                extrapolate: Some(extrapolate),
1131                degree: Some(degree),
1132            },
1133        )
1134        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1135        Ok(Self { inner })
1136    }
1137
1138    pub fn update(&mut self, value: f64) -> Option<f64> {
1139        self.inner.update(value)
1140    }
1141}
1142
1143#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1144#[derive(Serialize, Deserialize)]
1145pub struct PolynomialRegressionExtrapolationBatchConfig {
1146    pub length_range: (usize, usize, usize),
1147    pub extrapolate_range: (usize, usize, usize),
1148    pub degree_range: (usize, usize, usize),
1149}
1150
1151#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1152#[derive(Serialize, Deserialize)]
1153pub struct PolynomialRegressionExtrapolationBatchJsOutput {
1154    pub values: Vec<f64>,
1155    pub combos: Vec<PolynomialRegressionExtrapolationParams>,
1156    pub rows: usize,
1157    pub cols: usize,
1158}
1159
1160#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1161#[wasm_bindgen]
1162pub fn polynomial_regression_extrapolation_js(
1163    data: &[f64],
1164    length: usize,
1165    extrapolate: usize,
1166    degree: usize,
1167) -> Result<Vec<f64>, JsValue> {
1168    let input = PolynomialRegressionExtrapolationInput::from_slice(
1169        data,
1170        PolynomialRegressionExtrapolationParams {
1171            length: Some(length),
1172            extrapolate: Some(extrapolate),
1173            degree: Some(degree),
1174        },
1175    );
1176    let mut out = vec![0.0; data.len()];
1177    polynomial_regression_extrapolation_into_slice(&mut out, &input, Kernel::Auto)
1178        .map_err(|e| JsValue::from_str(&e.to_string()))?;
1179    Ok(out)
1180}
1181
1182#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1183#[wasm_bindgen]
1184pub fn polynomial_regression_extrapolation_alloc(len: usize) -> *mut f64 {
1185    let mut vec = Vec::<f64>::with_capacity(len);
1186    let ptr = vec.as_mut_ptr();
1187    std::mem::forget(vec);
1188    ptr
1189}
1190
1191#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1192#[wasm_bindgen]
1193pub fn polynomial_regression_extrapolation_free(ptr: *mut f64, len: usize) {
1194    if !ptr.is_null() {
1195        unsafe {
1196            let _ = Vec::from_raw_parts(ptr, len, len);
1197        }
1198    }
1199}
1200
1201#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1202#[wasm_bindgen]
1203pub fn polynomial_regression_extrapolation_into(
1204    in_ptr: *const f64,
1205    out_ptr: *mut f64,
1206    len: usize,
1207    length: usize,
1208    extrapolate: usize,
1209    degree: usize,
1210) -> Result<(), JsValue> {
1211    if in_ptr.is_null() || out_ptr.is_null() {
1212        return Err(JsValue::from_str("Null pointer provided"));
1213    }
1214
1215    unsafe {
1216        let data = std::slice::from_raw_parts(in_ptr, len);
1217        let input = PolynomialRegressionExtrapolationInput::from_slice(
1218            data,
1219            PolynomialRegressionExtrapolationParams {
1220                length: Some(length),
1221                extrapolate: Some(extrapolate),
1222                degree: Some(degree),
1223            },
1224        );
1225        if in_ptr == out_ptr {
1226            let mut temp = vec![0.0; len];
1227            polynomial_regression_extrapolation_into_slice(&mut temp, &input, Kernel::Auto)
1228                .map_err(|e| JsValue::from_str(&e.to_string()))?;
1229            let out = std::slice::from_raw_parts_mut(out_ptr, len);
1230            out.copy_from_slice(&temp);
1231        } else {
1232            let out = std::slice::from_raw_parts_mut(out_ptr, len);
1233            polynomial_regression_extrapolation_into_slice(out, &input, Kernel::Auto)
1234                .map_err(|e| JsValue::from_str(&e.to_string()))?;
1235        }
1236    }
1237    Ok(())
1238}
1239
1240#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1241#[wasm_bindgen(js_name = polynomial_regression_extrapolation_batch)]
1242pub fn polynomial_regression_extrapolation_batch_unified_js(
1243    data: &[f64],
1244    config: JsValue,
1245) -> Result<JsValue, JsValue> {
1246    let config: PolynomialRegressionExtrapolationBatchConfig =
1247        serde_wasm_bindgen::from_value(config)
1248            .map_err(|e| JsValue::from_str(&format!("Invalid config: {}", e)))?;
1249    let sweep = PolynomialRegressionExtrapolationBatchRange {
1250        length: config.length_range,
1251        extrapolate: config.extrapolate_range,
1252        degree: config.degree_range,
1253    };
1254    let output = polynomial_regression_extrapolation_batch_with_kernel(data, &sweep, Kernel::Auto)
1255        .map_err(|e| JsValue::from_str(&e.to_string()))?;
1256    let js_output = PolynomialRegressionExtrapolationBatchJsOutput {
1257        values: output.values,
1258        combos: output.combos,
1259        rows: output.rows,
1260        cols: output.cols,
1261    };
1262    serde_wasm_bindgen::to_value(&js_output)
1263        .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
1264}
1265
1266#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1267#[wasm_bindgen]
1268pub fn polynomial_regression_extrapolation_batch_into(
1269    in_ptr: *const f64,
1270    out_ptr: *mut f64,
1271    len: usize,
1272    length_start: usize,
1273    length_end: usize,
1274    length_step: usize,
1275    extrapolate_start: usize,
1276    extrapolate_end: usize,
1277    extrapolate_step: usize,
1278    degree_start: usize,
1279    degree_end: usize,
1280    degree_step: usize,
1281) -> Result<usize, JsValue> {
1282    if in_ptr.is_null() || out_ptr.is_null() {
1283        return Err(JsValue::from_str("Null pointer provided"));
1284    }
1285
1286    let sweep = PolynomialRegressionExtrapolationBatchRange {
1287        length: (length_start, length_end, length_step),
1288        extrapolate: (extrapolate_start, extrapolate_end, extrapolate_step),
1289        degree: (degree_start, degree_end, degree_step),
1290    };
1291    let combos = expand_grid(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
1292    let rows = combos.len();
1293    let total = rows
1294        .checked_mul(len)
1295        .ok_or_else(|| JsValue::from_str("rows*len overflow"))?;
1296
1297    unsafe {
1298        let data = std::slice::from_raw_parts(in_ptr, len);
1299        let out = std::slice::from_raw_parts_mut(out_ptr, total);
1300        polynomial_regression_extrapolation_batch_inner_into(
1301            data,
1302            &sweep,
1303            Kernel::Scalar,
1304            false,
1305            out,
1306        )
1307        .map_err(|e| JsValue::from_str(&e.to_string()))?;
1308    }
1309    Ok(rows)
1310}
1311
1312#[cfg(test)]
1313mod tests {
1314    use super::*;
1315    use crate::utilities::data_loader::read_candles_from_csv;
1316    use paste::paste;
1317
1318    fn assert_series_close(actual: &[f64], expected: &[f64], tol: f64) {
1319        assert_eq!(actual.len(), expected.len());
1320        for (idx, (&a, &e)) in actual.iter().zip(expected.iter()).enumerate() {
1321            if a.is_nan() || e.is_nan() {
1322                assert!(
1323                    a.is_nan() && e.is_nan(),
1324                    "NaN mismatch at idx {}: actual={} expected={}",
1325                    idx,
1326                    a,
1327                    e
1328                );
1329            } else {
1330                assert!(
1331                    (a - e).abs() <= tol,
1332                    "value mismatch at idx {}: actual={} expected={} tol={}",
1333                    idx,
1334                    a,
1335                    e,
1336                    tol
1337                );
1338            }
1339        }
1340    }
1341
1342    fn quadratic_data(size: usize) -> Vec<f64> {
1343        (0..size).map(|idx| (idx * idx) as f64).collect()
1344    }
1345
1346    fn cubic_data(size: usize) -> Vec<f64> {
1347        (0..size)
1348            .map(|idx| {
1349                let x = idx as f64;
1350                x * x * x - 2.0 * x * x + 3.0 * x + 1.0
1351            })
1352            .collect()
1353    }
1354
1355    fn check_quadratic_exactness(_name: &str, kernel: Kernel) -> Result<(), Box<dyn StdError>> {
1356        let data = quadratic_data(24);
1357        let input = PolynomialRegressionExtrapolationInput::from_slice(
1358            &data,
1359            PolynomialRegressionExtrapolationParams {
1360                length: Some(5),
1361                extrapolate: Some(2),
1362                degree: Some(2),
1363            },
1364        );
1365        let output = polynomial_regression_extrapolation_with_kernel(&input, kernel)?;
1366        let mut expected = vec![f64::NAN; data.len()];
1367        for idx in 4..data.len() {
1368            let x = idx as f64 + 2.0;
1369            expected[idx] = x * x;
1370        }
1371        assert_series_close(&output.values, &expected, 1e-9);
1372        Ok(())
1373    }
1374
1375    fn check_cubic_exactness(_name: &str, kernel: Kernel) -> Result<(), Box<dyn StdError>> {
1376        let data = cubic_data(30);
1377        let input = PolynomialRegressionExtrapolationInput::from_slice(
1378            &data,
1379            PolynomialRegressionExtrapolationParams {
1380                length: Some(6),
1381                extrapolate: Some(3),
1382                degree: Some(3),
1383            },
1384        );
1385        let output = polynomial_regression_extrapolation_with_kernel(&input, kernel)?;
1386        let mut expected = vec![f64::NAN; data.len()];
1387        for idx in 5..data.len() {
1388            let x = idx as f64 + 3.0;
1389            expected[idx] = x * x * x - 2.0 * x * x + 3.0 * x + 1.0;
1390        }
1391        assert_series_close(&output.values, &expected, 1e-8);
1392        Ok(())
1393    }
1394
1395    fn check_constant_degree_zero(_name: &str, kernel: Kernel) -> Result<(), Box<dyn StdError>> {
1396        let data = vec![42.0; 18];
1397        let input = PolynomialRegressionExtrapolationInput::from_slice(
1398            &data,
1399            PolynomialRegressionExtrapolationParams {
1400                length: Some(4),
1401                extrapolate: Some(7),
1402                degree: Some(0),
1403            },
1404        );
1405        let output = polynomial_regression_extrapolation_with_kernel(&input, kernel)?;
1406        assert!(output.values[..3].iter().all(|value| value.is_nan()));
1407        for value in &output.values[3..] {
1408            assert!((*value - 42.0).abs() <= 1e-12);
1409        }
1410        Ok(())
1411    }
1412
1413    fn check_nan_gap_semantics(_name: &str, kernel: Kernel) -> Result<(), Box<dyn StdError>> {
1414        let data = vec![
1415            f64::NAN,
1416            1.0,
1417            4.0,
1418            9.0,
1419            16.0,
1420            25.0,
1421            f64::NAN,
1422            64.0,
1423            81.0,
1424            100.0,
1425            121.0,
1426            144.0,
1427        ];
1428        let input = PolynomialRegressionExtrapolationInput::from_slice(
1429            &data,
1430            PolynomialRegressionExtrapolationParams {
1431                length: Some(3),
1432                extrapolate: Some(1),
1433                degree: Some(2),
1434            },
1435        );
1436        let output = polynomial_regression_extrapolation_with_kernel(&input, kernel)?;
1437        assert!(output.values[..3].iter().all(|value| value.is_nan()));
1438        assert!((output.values[3] - 16.0).abs() <= 1e-9);
1439        assert!((output.values[4] - 25.0).abs() <= 1e-9);
1440        assert!((output.values[5] - 36.0).abs() <= 1e-9);
1441        assert!(output.values[6].is_nan());
1442        assert!(output.values[7].is_nan());
1443        assert!(output.values[8].is_nan());
1444        assert!((output.values[9] - 121.0).abs() <= 1e-9);
1445        assert!((output.values[10] - 144.0).abs() <= 1e-9);
1446        assert!((output.values[11] - 169.0).abs() <= 1e-9);
1447        Ok(())
1448    }
1449
1450    fn check_into_matches_api(_name: &str, kernel: Kernel) -> Result<(), Box<dyn StdError>> {
1451        let data = quadratic_data(20);
1452        let input = PolynomialRegressionExtrapolationInput::from_slice(
1453            &data,
1454            PolynomialRegressionExtrapolationParams {
1455                length: Some(5),
1456                extrapolate: Some(2),
1457                degree: Some(2),
1458            },
1459        );
1460        let baseline = polynomial_regression_extrapolation_with_kernel(&input, kernel)?.values;
1461        let mut out = vec![0.0; data.len()];
1462        polynomial_regression_extrapolation_into_slice(&mut out, &input, kernel)?;
1463        assert_series_close(&out, &baseline, 1e-12);
1464        Ok(())
1465    }
1466
1467    fn check_stream_matches_batch(_name: &str, kernel: Kernel) -> Result<(), Box<dyn StdError>> {
1468        let data = cubic_data(24);
1469        let input = PolynomialRegressionExtrapolationInput::from_slice(
1470            &data,
1471            PolynomialRegressionExtrapolationParams {
1472                length: Some(6),
1473                extrapolate: Some(2),
1474                degree: Some(3),
1475            },
1476        );
1477        let batch = polynomial_regression_extrapolation_with_kernel(&input, kernel)?.values;
1478        let mut stream = PolynomialRegressionExtrapolationStream::try_new(
1479            PolynomialRegressionExtrapolationParams {
1480                length: Some(6),
1481                extrapolate: Some(2),
1482                degree: Some(3),
1483            },
1484        )?;
1485        let streamed: Vec<f64> = data
1486            .iter()
1487            .map(|&value| stream.update(value).unwrap_or(f64::NAN))
1488            .collect();
1489        assert_series_close(&streamed, &batch, 1e-12);
1490        Ok(())
1491    }
1492
1493    fn check_batch_matches_single(_name: &str, _kernel: Kernel) -> Result<(), Box<dyn StdError>> {
1494        let data = quadratic_data(22);
1495        let batch = PolynomialRegressionExtrapolationBatchBuilder::new()
1496            .length_range(4, 5, 1)
1497            .extrapolate_range(1, 2, 1)
1498            .degree_range(1, 2, 1)
1499            .kernel(Kernel::ScalarBatch)
1500            .apply_slice(&data)?;
1501        assert_eq!(batch.rows, 8);
1502        assert_eq!(batch.cols, data.len());
1503
1504        for length in [4usize, 5] {
1505            for extrapolate in [1usize, 2] {
1506                for degree in [1usize, 2] {
1507                    let params = PolynomialRegressionExtrapolationParams {
1508                        length: Some(length),
1509                        extrapolate: Some(extrapolate),
1510                        degree: Some(degree),
1511                    };
1512                    let input =
1513                        PolynomialRegressionExtrapolationInput::from_slice(&data, params.clone());
1514                    let single = polynomial_regression_extrapolation(&input)?.values;
1515                    let row = batch.values_for(&params).unwrap();
1516                    assert_series_close(row, &single, 1e-12);
1517                }
1518            }
1519        }
1520        Ok(())
1521    }
1522
1523    fn check_invalid_degree(_name: &str, kernel: Kernel) -> Result<(), Box<dyn StdError>> {
1524        let data = quadratic_data(12);
1525        let input = PolynomialRegressionExtrapolationInput::from_slice(
1526            &data,
1527            PolynomialRegressionExtrapolationParams {
1528                length: Some(4),
1529                extrapolate: Some(1),
1530                degree: Some(9),
1531            },
1532        );
1533        let err = polynomial_regression_extrapolation_with_kernel(&input, kernel).unwrap_err();
1534        assert!(matches!(
1535            err,
1536            PolynomialRegressionExtrapolationError::InvalidDegree { .. }
1537        ));
1538        Ok(())
1539    }
1540
1541    fn check_degree_exceeds_length(_name: &str, kernel: Kernel) -> Result<(), Box<dyn StdError>> {
1542        let data = quadratic_data(12);
1543        let input = PolynomialRegressionExtrapolationInput::from_slice(
1544            &data,
1545            PolynomialRegressionExtrapolationParams {
1546                length: Some(3),
1547                extrapolate: Some(1),
1548                degree: Some(3),
1549            },
1550        );
1551        let err = polynomial_regression_extrapolation_with_kernel(&input, kernel).unwrap_err();
1552        assert!(matches!(
1553            err,
1554            PolynomialRegressionExtrapolationError::DegreeExceedsLength { .. }
1555        ));
1556        Ok(())
1557    }
1558
1559    fn check_all_nan(_name: &str, kernel: Kernel) -> Result<(), Box<dyn StdError>> {
1560        let data = vec![f64::NAN; 16];
1561        let input = PolynomialRegressionExtrapolationInput::from_slice(
1562            &data,
1563            PolynomialRegressionExtrapolationParams {
1564                length: Some(4),
1565                extrapolate: Some(1),
1566                degree: Some(2),
1567            },
1568        );
1569        let err = polynomial_regression_extrapolation_with_kernel(&input, kernel).unwrap_err();
1570        assert!(matches!(
1571            err,
1572            PolynomialRegressionExtrapolationError::AllValuesNaN
1573        ));
1574        Ok(())
1575    }
1576
1577    macro_rules! generate_pre_tests {
1578        ($($name:ident),* $(,)?) => {
1579            $(
1580                paste! {
1581                    #[test]
1582                    fn [<polynomial_regression_extrapolation_ $name _scalar>]() -> Result<(), Box<dyn StdError>> {
1583                        $name("scalar", Kernel::Scalar)
1584                    }
1585
1586                    #[test]
1587                    fn [<polynomial_regression_extrapolation_ $name _auto>]() -> Result<(), Box<dyn StdError>> {
1588                        $name("auto", Kernel::Auto)
1589                    }
1590                }
1591            )*
1592        };
1593    }
1594
1595    generate_pre_tests!(
1596        check_quadratic_exactness,
1597        check_cubic_exactness,
1598        check_constant_degree_zero,
1599        check_nan_gap_semantics,
1600        check_into_matches_api,
1601        check_stream_matches_batch,
1602        check_batch_matches_single,
1603        check_invalid_degree,
1604        check_degree_exceeds_length,
1605        check_all_nan,
1606    );
1607
1608    #[test]
1609    fn polynomial_regression_extrapolation_default_candles_smoke() -> Result<(), Box<dyn StdError>>
1610    {
1611        let candles = read_candles_from_csv("src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv")?;
1612        let input = PolynomialRegressionExtrapolationInput::with_default_candles(&candles);
1613        let output = polynomial_regression_extrapolation(&input)?;
1614        assert_eq!(output.values.len(), candles.close.len());
1615        Ok(())
1616    }
1617}