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 °ree in °rees {
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(¶ms).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}