1#[cfg(feature = "python")]
2use numpy::{IntoPyArray, PyArray1};
3#[cfg(feature = "python")]
4use pyo3::exceptions::PyValueError;
5#[cfg(feature = "python")]
6use pyo3::prelude::*;
7#[cfg(feature = "python")]
8use pyo3::types::PyDict;
9#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
10use serde::{Deserialize, Serialize};
11#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
12use wasm_bindgen::prelude::*;
13
14use crate::utilities::data_loader::{source_type, Candles};
15use crate::utilities::enums::Kernel;
16use crate::utilities::helpers::{
17 alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
18 make_uninit_matrix,
19};
20#[cfg(feature = "python")]
21use crate::utilities::kernel_validation::validate_kernel;
22use aligned_vec::{AVec, CACHELINE_ALIGN};
23#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
24use core::arch::x86_64::*;
25#[cfg(not(target_arch = "wasm32"))]
26use rayon::prelude::*;
27use std::convert::AsRef;
28use std::error::Error;
29use std::mem::MaybeUninit;
30use thiserror::Error;
31
32impl<'a> AsRef<[f64]> for LinearRegSlopeInput<'a> {
33 #[inline(always)]
34 fn as_ref(&self) -> &[f64] {
35 match &self.data {
36 LinearRegSlopeData::Slice(slice) => slice,
37 LinearRegSlopeData::Candles { candles, source } => source_type(candles, source),
38 }
39 }
40}
41
42#[derive(Debug, Clone)]
43pub enum LinearRegSlopeData<'a> {
44 Candles {
45 candles: &'a Candles,
46 source: &'a str,
47 },
48 Slice(&'a [f64]),
49}
50
51#[derive(Debug, Clone)]
52pub struct LinearRegSlopeOutput {
53 pub values: Vec<f64>,
54}
55
56#[derive(Debug, Clone)]
57#[cfg_attr(
58 all(target_arch = "wasm32", feature = "wasm"),
59 derive(Serialize, Deserialize)
60)]
61pub struct LinearRegSlopeParams {
62 pub period: Option<usize>,
63}
64
65impl Default for LinearRegSlopeParams {
66 fn default() -> Self {
67 Self { period: Some(14) }
68 }
69}
70
71#[derive(Debug, Clone)]
72pub struct LinearRegSlopeInput<'a> {
73 pub data: LinearRegSlopeData<'a>,
74 pub params: LinearRegSlopeParams,
75}
76
77impl<'a> LinearRegSlopeInput<'a> {
78 #[inline]
79 pub fn from_candles(c: &'a Candles, s: &'a str, p: LinearRegSlopeParams) -> Self {
80 Self {
81 data: LinearRegSlopeData::Candles {
82 candles: c,
83 source: s,
84 },
85 params: p,
86 }
87 }
88 #[inline]
89 pub fn from_slice(sl: &'a [f64], p: LinearRegSlopeParams) -> Self {
90 Self {
91 data: LinearRegSlopeData::Slice(sl),
92 params: p,
93 }
94 }
95 #[inline]
96 pub fn with_default_candles(c: &'a Candles) -> Self {
97 Self::from_candles(c, "close", LinearRegSlopeParams::default())
98 }
99 #[inline]
100 pub fn get_period(&self) -> usize {
101 self.params.period.unwrap_or(14)
102 }
103}
104
105#[derive(Copy, Clone, Debug)]
106pub struct LinearRegSlopeBuilder {
107 period: Option<usize>,
108 kernel: Kernel,
109}
110
111impl Default for LinearRegSlopeBuilder {
112 fn default() -> Self {
113 Self {
114 period: None,
115 kernel: Kernel::Auto,
116 }
117 }
118}
119
120impl LinearRegSlopeBuilder {
121 #[inline(always)]
122 pub fn new() -> Self {
123 Self::default()
124 }
125 #[inline(always)]
126 pub fn period(mut self, n: usize) -> Self {
127 self.period = Some(n);
128 self
129 }
130 #[inline(always)]
131 pub fn kernel(mut self, k: Kernel) -> Self {
132 self.kernel = k;
133 self
134 }
135 #[inline(always)]
136 pub fn apply(self, c: &Candles) -> Result<LinearRegSlopeOutput, LinearRegSlopeError> {
137 let p = LinearRegSlopeParams {
138 period: self.period,
139 };
140 let i = LinearRegSlopeInput::from_candles(c, "close", p);
141 linearreg_slope_with_kernel(&i, self.kernel)
142 }
143 #[inline(always)]
144 pub fn apply_slice(self, d: &[f64]) -> Result<LinearRegSlopeOutput, LinearRegSlopeError> {
145 let p = LinearRegSlopeParams {
146 period: self.period,
147 };
148 let i = LinearRegSlopeInput::from_slice(d, p);
149 linearreg_slope_with_kernel(&i, self.kernel)
150 }
151 #[inline(always)]
152 pub fn into_stream(self) -> Result<LinearRegSlopeStream, LinearRegSlopeError> {
153 let p = LinearRegSlopeParams {
154 period: self.period,
155 };
156 LinearRegSlopeStream::try_new(p)
157 }
158}
159
160#[derive(Debug, Error)]
161pub enum LinearRegSlopeError {
162 #[error("linearreg_slope: Empty data provided.")]
163 EmptyInputData,
164 #[error("linearreg_slope: All values are NaN.")]
165 AllValuesNaN,
166 #[error("linearreg_slope: Invalid period: period = {period}, data length = {data_len}")]
167 InvalidPeriod { period: usize, data_len: usize },
168 #[error("linearreg_slope: Not enough valid data: needed = {needed}, valid = {valid}")]
169 NotEnoughValidData { needed: usize, valid: usize },
170 #[error("linearreg_slope: Output length mismatch: expected = {expected}, got = {got}")]
171 OutputLengthMismatch { expected: usize, got: usize },
172 #[error("linearreg_slope: invalid range: start={start}, end={end}, step={step}")]
173 InvalidRange {
174 start: usize,
175 end: usize,
176 step: usize,
177 },
178 #[error("linearreg_slope: invalid kernel for batch: {0:?}")]
179 InvalidKernelForBatch(Kernel),
180}
181
182#[inline]
183pub fn linearreg_slope(
184 input: &LinearRegSlopeInput,
185) -> Result<LinearRegSlopeOutput, LinearRegSlopeError> {
186 linearreg_slope_with_kernel(input, Kernel::Auto)
187}
188
189pub fn linearreg_slope_with_kernel(
190 input: &LinearRegSlopeInput,
191 kernel: Kernel,
192) -> Result<LinearRegSlopeOutput, LinearRegSlopeError> {
193 let data: &[f64] = input.as_ref();
194 if data.is_empty() {
195 return Err(LinearRegSlopeError::EmptyInputData);
196 }
197 let period = input.get_period();
198
199 if period < 2 || period > data.len() {
200 return Err(LinearRegSlopeError::InvalidPeriod {
201 period,
202 data_len: data.len(),
203 });
204 }
205 let first_valid_idx = match data.iter().position(|&x| !x.is_nan()) {
206 Some(idx) => idx,
207 None => return Err(LinearRegSlopeError::AllValuesNaN),
208 };
209 if (data.len() - first_valid_idx) < period {
210 return Err(LinearRegSlopeError::NotEnoughValidData {
211 needed: period,
212 valid: data.len() - first_valid_idx,
213 });
214 }
215 let mut out = alloc_with_nan_prefix(data.len(), first_valid_idx + period - 1);
216 let chosen = match kernel {
217 Kernel::Auto => Kernel::Scalar,
218 other => other,
219 };
220 unsafe {
221 match chosen {
222 Kernel::Scalar | Kernel::ScalarBatch => {
223 linearreg_slope_scalar(data, period, first_valid_idx, &mut out)
224 }
225 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
226 Kernel::Avx2 | Kernel::Avx2Batch => {
227 linearreg_slope_avx2(data, period, first_valid_idx, &mut out)
228 }
229 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
230 Kernel::Avx512 | Kernel::Avx512Batch => {
231 linearreg_slope_avx512(data, period, first_valid_idx, &mut out)
232 }
233 _ => unreachable!(),
234 }
235 }
236 Ok(LinearRegSlopeOutput { values: out })
237}
238
239#[inline]
240pub fn linearreg_slope_scalar(data: &[f64], period: usize, first: usize, out: &mut [f64]) {
241 let len = data.len();
242 if len == 0 {
243 return;
244 }
245
246 let p = period as f64;
247 let base = first;
248 let mut i = base + period - 1;
249 if i >= len {
250 return;
251 }
252
253 let x = 0.5 * p * (p + 1.0);
254 let x2 = (p * (p + 1.0) * (2.0 * p + 1.0)) / 6.0;
255 let denom = p * x2 - x * x;
256 if denom.abs() < f64::EPSILON {
257 for out_i in i..len {
258 out[out_i] = f64::NAN;
259 }
260 return;
261 }
262 let bd = 1.0 / denom;
263 let p_bd = p * bd;
264 let x_bd = x * bd;
265
266 #[inline(always)]
267 fn kahan_add(sum: &mut f64, c: &mut f64, x: f64) {
268 let y = x - *c;
269 let t = *sum + y;
270 *c = (t - *sum) - y;
271 *sum = t;
272 }
273
274 unsafe {
275 let dp = data.as_ptr();
276
277 let mut y = 0.0f64;
278 let mut y_c = 0.0f64;
279 let mut xy = 0.0f64;
280 let mut xy_c = 0.0f64;
281 for j in 0..(period - 1) {
282 let v = *dp.add(base + j);
283 kahan_add(&mut y, &mut y_c, v);
284 kahan_add(&mut xy, &mut xy_c, v * (j + 1) as f64);
285 }
286
287 let mut in_new = dp.add(base + period - 1);
288 let mut in_old = dp.add(base);
289 let end = dp.add(len);
290 let mut out_ptr = out.as_mut_ptr().add(base + period - 1);
291
292 while in_new.add(1) < end {
293 let v0 = *in_new;
294 kahan_add(&mut y, &mut y_c, v0);
295 kahan_add(&mut xy, &mut xy_c, v0 * p);
296 let b0 = xy * p_bd - y * x_bd;
297 *out_ptr = if b0.abs() <= 1.1e-8 { 0.0 } else { b0 };
298 kahan_add(&mut xy, &mut xy_c, -y);
299 kahan_add(&mut y, &mut y_c, -*in_old);
300
301 let v1 = *in_new.add(1);
302 kahan_add(&mut y, &mut y_c, v1);
303 kahan_add(&mut xy, &mut xy_c, v1 * p);
304 let b1 = xy * p_bd - y * x_bd;
305 *out_ptr.add(1) = if b1.abs() <= 1.1e-8 { 0.0 } else { b1 };
306 kahan_add(&mut xy, &mut xy_c, -y);
307 kahan_add(&mut y, &mut y_c, -*in_old.add(1));
308
309 in_new = in_new.add(2);
310 in_old = in_old.add(2);
311 out_ptr = out_ptr.add(2);
312 }
313
314 if in_new < end {
315 let v = *in_new;
316 kahan_add(&mut y, &mut y_c, v);
317 kahan_add(&mut xy, &mut xy_c, v * p);
318 let b = xy * p_bd - y * x_bd;
319 *out_ptr = if b.abs() <= 1.1e-8 { 0.0 } else { b };
320 }
321 }
322}
323
324#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
325#[inline]
326pub fn linearreg_slope_avx512(data: &[f64], period: usize, first_valid: usize, out: &mut [f64]) {
327 linearreg_slope_scalar(data, period, first_valid, out)
328}
329
330#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
331#[inline]
332pub fn linearreg_slope_avx2(data: &[f64], period: usize, first_valid: usize, out: &mut [f64]) {
333 linearreg_slope_scalar(data, period, first_valid, out)
334}
335
336#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
337#[inline]
338pub fn linearreg_slope_avx512_short(
339 data: &[f64],
340 period: usize,
341 first_valid: usize,
342 out: &mut [f64],
343) {
344 linearreg_slope_scalar(data, period, first_valid, out)
345}
346
347#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
348#[inline]
349pub fn linearreg_slope_avx512_long(
350 data: &[f64],
351 period: usize,
352 first_valid: usize,
353 out: &mut [f64],
354) {
355 linearreg_slope_scalar(data, period, first_valid, out)
356}
357
358pub fn linearreg_slope_batch_with_kernel(
359 data: &[f64],
360 sweep: &LinearRegSlopeBatchRange,
361 kernel: Kernel,
362) -> Result<LinearRegSlopeBatchOutput, LinearRegSlopeError> {
363 let k = match kernel {
364 Kernel::Auto => detect_best_batch_kernel(),
365 other if other.is_batch() => other,
366 _ => return Err(LinearRegSlopeError::InvalidKernelForBatch(kernel)),
367 };
368 let simd = match k {
369 Kernel::Avx512Batch => Kernel::Avx512,
370 Kernel::Avx2Batch => Kernel::Avx2,
371 Kernel::ScalarBatch => Kernel::Scalar,
372 _ => unreachable!(),
373 };
374 linearreg_slope_batch_par_slice(data, sweep, simd)
375}
376
377#[derive(Clone, Debug)]
378pub struct LinearRegSlopeBatchRange {
379 pub period: (usize, usize, usize),
380}
381
382impl Default for LinearRegSlopeBatchRange {
383 fn default() -> Self {
384 Self {
385 period: (14, 263, 1),
386 }
387 }
388}
389
390#[derive(Clone, Debug, Default)]
391pub struct LinearRegSlopeBatchBuilder {
392 range: LinearRegSlopeBatchRange,
393 kernel: Kernel,
394}
395
396impl LinearRegSlopeBatchBuilder {
397 pub fn new() -> Self {
398 Self::default()
399 }
400 pub fn kernel(mut self, k: Kernel) -> Self {
401 self.kernel = k;
402 self
403 }
404 #[inline]
405 pub fn period_range(mut self, start: usize, end: usize, step: usize) -> Self {
406 self.range.period = (start, end, step);
407 self
408 }
409 #[inline]
410 pub fn period_static(mut self, p: usize) -> Self {
411 self.range.period = (p, p, 0);
412 self
413 }
414 pub fn apply_slice(
415 self,
416 data: &[f64],
417 ) -> Result<LinearRegSlopeBatchOutput, LinearRegSlopeError> {
418 linearreg_slope_batch_with_kernel(data, &self.range, self.kernel)
419 }
420 pub fn with_default_slice(
421 data: &[f64],
422 k: Kernel,
423 ) -> Result<LinearRegSlopeBatchOutput, LinearRegSlopeError> {
424 LinearRegSlopeBatchBuilder::new()
425 .kernel(k)
426 .apply_slice(data)
427 }
428 pub fn apply_candles(
429 self,
430 c: &Candles,
431 src: &str,
432 ) -> Result<LinearRegSlopeBatchOutput, LinearRegSlopeError> {
433 let slice = source_type(c, src);
434 self.apply_slice(slice)
435 }
436 pub fn with_default_candles(
437 c: &Candles,
438 ) -> Result<LinearRegSlopeBatchOutput, LinearRegSlopeError> {
439 LinearRegSlopeBatchBuilder::new()
440 .kernel(Kernel::Auto)
441 .apply_candles(c, "close")
442 }
443}
444
445#[derive(Clone, Debug)]
446pub struct LinearRegSlopeBatchOutput {
447 pub values: Vec<f64>,
448 pub combos: Vec<LinearRegSlopeParams>,
449 pub rows: usize,
450 pub cols: usize,
451}
452impl LinearRegSlopeBatchOutput {
453 pub fn row_for_params(&self, p: &LinearRegSlopeParams) -> Option<usize> {
454 self.combos
455 .iter()
456 .position(|c| c.period.unwrap_or(14) == p.period.unwrap_or(14))
457 }
458 pub fn values_for(&self, p: &LinearRegSlopeParams) -> Option<&[f64]> {
459 self.row_for_params(p).map(|row| {
460 let start = row * self.cols;
461 &self.values[start..start + self.cols]
462 })
463 }
464}
465
466#[inline(always)]
467fn expand_grid(r: &LinearRegSlopeBatchRange) -> Vec<LinearRegSlopeParams> {
468 fn axis_usize(
469 (start, end, step): (usize, usize, usize),
470 ) -> Result<Vec<usize>, LinearRegSlopeError> {
471 if step == 0 || start == end {
472 return Ok(vec![start]);
473 }
474 if start < end {
475 let mut v = Vec::new();
476 let st = step.max(1);
477 let mut x = start;
478 while x <= end {
479 v.push(x);
480 match x.checked_add(st) {
481 Some(next) => x = next,
482 None => break,
483 }
484 }
485 if v.is_empty() {
486 return Err(LinearRegSlopeError::InvalidRange { start, end, step });
487 }
488 return Ok(v);
489 }
490
491 let mut v = Vec::new();
492 let st = step.max(1) as isize;
493 let mut x = start as isize;
494 let end_i = end as isize;
495 while x >= end_i {
496 v.push(x as usize);
497 x -= st;
498 }
499 if v.is_empty() {
500 return Err(LinearRegSlopeError::InvalidRange { start, end, step });
501 }
502 Ok(v)
503 }
504 let periods = axis_usize(r.period).unwrap_or_else(|_| Vec::new());
505 let mut out = Vec::with_capacity(periods.len());
506 for p in periods {
507 out.push(LinearRegSlopeParams { period: Some(p) });
508 }
509 out
510}
511
512#[inline(always)]
513pub fn linearreg_slope_batch_slice(
514 data: &[f64],
515 sweep: &LinearRegSlopeBatchRange,
516 kern: Kernel,
517) -> Result<LinearRegSlopeBatchOutput, LinearRegSlopeError> {
518 linearreg_slope_batch_inner(data, sweep, kern, false)
519}
520
521#[inline(always)]
522pub fn linearreg_slope_batch_par_slice(
523 data: &[f64],
524 sweep: &LinearRegSlopeBatchRange,
525 kern: Kernel,
526) -> Result<LinearRegSlopeBatchOutput, LinearRegSlopeError> {
527 linearreg_slope_batch_inner(data, sweep, kern, true)
528}
529
530#[inline(always)]
531fn linearreg_slope_batch_inner(
532 data: &[f64],
533 sweep: &LinearRegSlopeBatchRange,
534 kern: Kernel,
535 parallel: bool,
536) -> Result<LinearRegSlopeBatchOutput, LinearRegSlopeError> {
537 let combos = expand_grid(sweep);
538 if combos.is_empty() {
539 return Err(LinearRegSlopeError::InvalidRange {
540 start: sweep.period.0,
541 end: sweep.period.1,
542 step: sweep.period.2,
543 });
544 }
545
546 for combo in &combos {
547 let period = combo.period.unwrap();
548 if period < 2 {
549 return Err(LinearRegSlopeError::InvalidPeriod {
550 period,
551 data_len: data.len(),
552 });
553 }
554 }
555
556 let first = data
557 .iter()
558 .position(|x| !x.is_nan())
559 .ok_or(LinearRegSlopeError::AllValuesNaN)?;
560 let max_p = combos.iter().map(|c| c.period.unwrap()).max().unwrap();
561 if data.len() - first < max_p {
562 return Err(LinearRegSlopeError::NotEnoughValidData {
563 needed: max_p,
564 valid: data.len() - first,
565 });
566 }
567 let rows = combos.len();
568 let cols = data.len();
569
570 let _total = rows
571 .checked_mul(cols)
572 .ok_or(LinearRegSlopeError::InvalidRange {
573 start: sweep.period.0,
574 end: sweep.period.1,
575 step: sweep.period.2,
576 })?;
577
578 let mut buf_mu = make_uninit_matrix(rows, cols);
579 let warmup_periods: Vec<usize> = combos
580 .iter()
581 .map(|c| first + c.period.unwrap() - 1)
582 .collect();
583 init_matrix_prefixes(&mut buf_mu, cols, &warmup_periods);
584
585 let mut buf_guard = core::mem::ManuallyDrop::new(buf_mu);
586 let out: &mut [f64] = unsafe {
587 core::slice::from_raw_parts_mut(buf_guard.as_mut_ptr() as *mut f64, buf_guard.len())
588 };
589
590 linearreg_slope_batch_inner_into(data, sweep, kern, parallel, out)?;
591
592 let values = unsafe {
593 Vec::from_raw_parts(
594 buf_guard.as_mut_ptr() as *mut f64,
595 buf_guard.len(),
596 buf_guard.capacity(),
597 )
598 };
599
600 Ok(LinearRegSlopeBatchOutput {
601 values,
602 combos,
603 rows,
604 cols,
605 })
606}
607
608#[inline(always)]
609fn linearreg_slope_batch_inner_into(
610 data: &[f64],
611 sweep: &LinearRegSlopeBatchRange,
612 kern: Kernel,
613 parallel: bool,
614 out: &mut [f64],
615) -> Result<Vec<LinearRegSlopeParams>, LinearRegSlopeError> {
616 let combos = expand_grid(sweep);
617 if combos.is_empty() {
618 return Err(LinearRegSlopeError::InvalidRange {
619 start: sweep.period.0,
620 end: sweep.period.1,
621 step: sweep.period.2,
622 });
623 }
624
625 for combo in &combos {
626 let period = combo.period.unwrap();
627 if period < 2 {
628 return Err(LinearRegSlopeError::InvalidPeriod {
629 period,
630 data_len: data.len(),
631 });
632 }
633 }
634
635 let first = data
636 .iter()
637 .position(|x| !x.is_nan())
638 .ok_or(LinearRegSlopeError::AllValuesNaN)?;
639 let max_p = combos.iter().map(|c| c.period.unwrap()).max().unwrap();
640 if data.len() - first < max_p {
641 return Err(LinearRegSlopeError::NotEnoughValidData {
642 needed: max_p,
643 valid: data.len() - first,
644 });
645 }
646
647 let rows = combos.len();
648 let cols = data.len();
649
650 let _total = rows
651 .checked_mul(cols)
652 .ok_or(LinearRegSlopeError::InvalidRange {
653 start: sweep.period.0,
654 end: sweep.period.1,
655 step: sweep.period.2,
656 })?;
657
658 for (row, combo) in combos.iter().enumerate() {
659 let warmup = first + combo.period.unwrap() - 1;
660 let row_start = row * cols;
661 for i in 0..warmup.min(cols) {
662 out[row_start + i] = f64::NAN;
663 }
664 }
665
666 if rows <= 1 {
667 let out_uninit = unsafe {
668 std::slice::from_raw_parts_mut(out.as_mut_ptr() as *mut MaybeUninit<f64>, out.len())
669 };
670 let do_row_scalar = |row: usize, dst_mu: &mut [MaybeUninit<f64>]| unsafe {
671 let period = combos[row].period.unwrap();
672 let dst =
673 core::slice::from_raw_parts_mut(dst_mu.as_mut_ptr() as *mut f64, dst_mu.len());
674 match kern {
675 Kernel::Scalar => linearreg_slope_row_scalar(data, first, period, dst),
676 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
677 Kernel::Avx2 => linearreg_slope_row_avx2(data, first, period, dst),
678 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
679 Kernel::Avx512 => linearreg_slope_row_avx512(data, first, period, dst),
680 _ => unreachable!(),
681 }
682 };
683 if parallel {
684 #[cfg(not(target_arch = "wasm32"))]
685 {
686 out_uninit
687 .par_chunks_mut(cols)
688 .enumerate()
689 .for_each(|(row, slice)| do_row_scalar(row, slice));
690 }
691 #[cfg(target_arch = "wasm32")]
692 {
693 for (row, slice) in out_uninit.chunks_mut(cols).enumerate() {
694 do_row_scalar(row, slice);
695 }
696 }
697 } else {
698 for (row, slice) in out_uninit.chunks_mut(cols).enumerate() {
699 do_row_scalar(row, slice);
700 }
701 }
702 } else {
703 let mut py = Vec::with_capacity(data.len() + 1);
704 let mut pky = Vec::with_capacity(data.len() + 1);
705 py.push(0.0);
706 pky.push(0.0);
707 if first > 0 {
708 py.resize(first + 1, 0.0);
709 pky.resize(first + 1, 0.0);
710 }
711 for i in first..data.len() {
712 let y = unsafe { *data.get_unchecked(i) };
713 let prev_y = unsafe { *py.get_unchecked(i) };
714 let prev_ky = unsafe { *pky.get_unchecked(i) };
715 py.push(prev_y + y);
716 pky.push(prev_ky + (i as f64) * y);
717 }
718
719 let out_uninit = unsafe {
720 std::slice::from_raw_parts_mut(out.as_mut_ptr() as *mut MaybeUninit<f64>, out.len())
721 };
722 let do_row_prefix = |row: usize, dst_mu: &mut [MaybeUninit<f64>]| {
723 let period = combos[row].period.unwrap();
724 let n = period as f64;
725 let m = (period - 1) as f64;
726 let sum_x = 0.5 * m * n;
727 let sum_x2 = (m * n) * (2.0 * m + 1.0) / 6.0;
728 let denom = n * sum_x2 - sum_x * sum_x;
729 if denom.abs() < f64::EPSILON {
730 return;
731 }
732 let dst = unsafe {
733 core::slice::from_raw_parts_mut(dst_mu.as_mut_ptr() as *mut f64, dst_mu.len())
734 };
735 let start_i = first + period - 1;
736 for i in start_i..cols {
737 let s = i + 1 - period;
738 let sy = unsafe { *py.get_unchecked(i + 1) - *py.get_unchecked(s) };
739 let sxy = unsafe {
740 (*pky.get_unchecked(i + 1) - *pky.get_unchecked(s)) - (s as f64) * sy
741 };
742 let num = n.mul_add(sxy, -sum_x * sy);
743 dst[i] = num / denom;
744 }
745 };
746
747 if parallel {
748 #[cfg(not(target_arch = "wasm32"))]
749 {
750 out_uninit
751 .par_chunks_mut(cols)
752 .enumerate()
753 .for_each(|(row, slice)| do_row_prefix(row, slice));
754 }
755 #[cfg(target_arch = "wasm32")]
756 {
757 for (row, slice) in out_uninit.chunks_mut(cols).enumerate() {
758 do_row_prefix(row, slice);
759 }
760 }
761 } else {
762 for (row, slice) in out_uninit.chunks_mut(cols).enumerate() {
763 do_row_prefix(row, slice);
764 }
765 }
766 }
767
768 Ok(combos)
769}
770
771#[inline(always)]
772unsafe fn linearreg_slope_row_scalar(data: &[f64], first: usize, period: usize, out: &mut [f64]) {
773 linearreg_slope_scalar(data, period, first, out)
774}
775
776#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
777#[inline(always)]
778unsafe fn linearreg_slope_row_avx2(data: &[f64], first: usize, period: usize, out: &mut [f64]) {
779 linearreg_slope_scalar(data, period, first, out)
780}
781
782#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
783#[inline(always)]
784unsafe fn linearreg_slope_row_avx512(data: &[f64], first: usize, period: usize, out: &mut [f64]) {
785 if period <= 32 {
786 linearreg_slope_row_avx512_short(data, first, period, out);
787 } else {
788 linearreg_slope_row_avx512_long(data, first, period, out);
789 }
790}
791
792#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
793#[inline(always)]
794unsafe fn linearreg_slope_row_avx512_short(
795 data: &[f64],
796 first: usize,
797 period: usize,
798 out: &mut [f64],
799) {
800 linearreg_slope_scalar(data, period, first, out)
801}
802
803#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
804#[inline(always)]
805unsafe fn linearreg_slope_row_avx512_long(
806 data: &[f64],
807 first: usize,
808 period: usize,
809 out: &mut [f64],
810) {
811 linearreg_slope_scalar(data, period, first, out)
812}
813
814#[derive(Debug, Clone)]
815pub struct LinearRegSlopeStream {
816 period: usize,
817 buffer: Vec<f64>,
818 head: usize,
819 filled: bool,
820 warm_count: usize,
821
822 n: f64,
823 m: f64,
824 sum_x: f64,
825 sum_x2: f64,
826 denom: f64,
827 inv_denom: f64,
828
829 sum_y: f64,
830 sum_y_c: f64,
831 sum_xy: f64,
832 sum_xy_c: f64,
833
834 step: usize,
835 recalc_mask: usize,
836}
837
838impl LinearRegSlopeStream {
839 #[inline]
840 pub fn try_new(params: LinearRegSlopeParams) -> Result<Self, LinearRegSlopeError> {
841 let period = params.period.unwrap_or(14);
842 if period < 2 {
843 return Err(LinearRegSlopeError::InvalidPeriod {
844 period,
845 data_len: 0,
846 });
847 }
848
849 let n = period as f64;
850 let m = (period - 1) as f64;
851
852 let sum_x = 0.5 * m * n;
853
854 let sum_x2 = (m * n) * (2.0 * m + 1.0) / 6.0;
855
856 let denom = n * sum_x2 - sum_x * sum_x;
857
858 let inv_denom = if denom.abs() > f64::EPSILON {
859 1.0 / denom
860 } else {
861 f64::NAN
862 };
863
864 Ok(Self {
865 period,
866 buffer: vec![0.0; period],
867 head: 0,
868 filled: false,
869 warm_count: 0,
870
871 n,
872 m,
873 sum_x,
874 sum_x2,
875 denom,
876 inv_denom,
877
878 sum_y: 0.0,
879 sum_y_c: 0.0,
880 sum_xy: 0.0,
881 sum_xy_c: 0.0,
882
883 step: 0,
884 recalc_mask: 255,
885 })
886 }
887
888 #[inline(always)]
889 pub fn update(&mut self, value: f64) -> Option<f64> {
890 if !value.is_finite() {
891 self.reset_state();
892 return None;
893 }
894
895 if !self.filled {
896 let j = self.warm_count as f64;
897
898 self.buffer[self.head] = value;
899 self.head = (self.head + 1) % self.period;
900
901 let y0 = value - self.sum_y_c;
902 let t0 = self.sum_y + y0;
903 self.sum_y_c = (t0 - self.sum_y) - y0;
904 self.sum_y = t0;
905
906 let jy = j * value;
907 let y1 = jy - self.sum_xy_c;
908 let t1 = self.sum_xy + y1;
909 self.sum_xy_c = (t1 - self.sum_xy) - y1;
910 self.sum_xy = t1;
911
912 self.warm_count += 1;
913 if self.warm_count < self.period {
914 return None;
915 }
916
917 self.filled = true;
918
919 return self.emit_slope();
920 }
921
922 let y_old = self.buffer[self.head];
923 self.buffer[self.head] = value;
924 self.head = (self.head + 1) % self.period;
925
926 let delta0 = value - y_old;
927 let yk0 = delta0 - self.sum_y_c;
928 let t0 = self.sum_y + yk0;
929 self.sum_y_c = (t0 - self.sum_y) - yk0;
930 self.sum_y = t0;
931
932 let delta1 = -self.sum_y + self.n * value;
933 let yk1 = delta1 - self.sum_xy_c;
934 let t1 = self.sum_xy + yk1;
935 self.sum_xy_c = (t1 - self.sum_xy) - yk1;
936 self.sum_xy = t1;
937
938 self.step = self.step.wrapping_add(1);
939 if (self.step & self.recalc_mask) == 0 {
940 self.recompute_exact();
941 }
942
943 self.emit_slope()
944 }
945
946 #[inline(always)]
947 fn emit_slope(&self) -> Option<f64> {
948 if !self.filled || !(self.denom.is_finite()) {
949 return None;
950 }
951
952 if self.m > 0.0 {
953 let first = self.buffer[self.head];
954 let last = self.buffer[(self.head + self.period - 1) % self.period];
955 let a2 = (last - first) / self.m;
956
957 let s0_model = a2.mul_add(self.sum_x, first * self.n);
958 let s1_model = a2.mul_add(self.sum_x2, first * self.sum_x);
959
960 let tol0 = 1e-12_f64 * 1.0_f64.max(self.sum_y.abs()).max(s0_model.abs());
961 let tol1 = 1e-12_f64 * 1.0_f64.max(self.sum_xy.abs()).max(s1_model.abs());
962 if (self.sum_y - s0_model).abs() <= tol0 && (self.sum_xy - s1_model).abs() <= tol1 {
963 return Some(a2);
964 }
965 }
966
967 let num = self.n.mul_add(self.sum_xy, -self.sum_x * self.sum_y);
968 Some(num * self.inv_denom)
969 }
970
971 #[inline(always)]
972 fn recompute_exact(&mut self) {
973 let mut sy = 0.0;
974 let mut sxy = 0.0;
975 let mut idx = self.head;
976 for j in 0..self.period {
977 let y = self.buffer[idx];
978 sy += y;
979 sxy = (j as f64).mul_add(y, sxy);
980 idx += 1;
981 if idx == self.period {
982 idx = 0;
983 }
984 }
985 self.sum_y = sy;
986 self.sum_y_c = 0.0;
987 self.sum_xy = sxy;
988 self.sum_xy_c = 0.0;
989 }
990
991 #[inline(always)]
992 fn reset_state(&mut self) {
993 self.head = 0;
994 self.filled = false;
995 self.warm_count = 0;
996 self.sum_y = 0.0;
997 self.sum_y_c = 0.0;
998 self.sum_xy = 0.0;
999 self.sum_xy_c = 0.0;
1000 }
1001}
1002
1003#[inline(always)]
1004fn expand_grid_stream(_r: &LinearRegSlopeBatchRange) -> Vec<LinearRegSlopeParams> {
1005 vec![LinearRegSlopeParams::default()]
1006}
1007
1008#[cfg(feature = "python")]
1009#[pyfunction(name = "linearreg_slope")]
1010#[pyo3(signature = (data, period, kernel=None))]
1011pub fn linearreg_slope_py<'py>(
1012 py: Python<'py>,
1013 data: numpy::PyReadonlyArray1<'py, f64>,
1014 period: usize,
1015 kernel: Option<&str>,
1016) -> PyResult<Bound<'py, numpy::PyArray1<f64>>> {
1017 use numpy::{IntoPyArray, PyArray1, PyArrayMethods};
1018
1019 let slice_in = data.as_slice()?;
1020 let kern = validate_kernel(kernel, false)?;
1021 let params = LinearRegSlopeParams {
1022 period: Some(period),
1023 };
1024 let linearreg_slope_in = LinearRegSlopeInput::from_slice(slice_in, params);
1025
1026 let result_vec: Vec<f64> = py
1027 .allow_threads(|| linearreg_slope_with_kernel(&linearreg_slope_in, kern).map(|o| o.values))
1028 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1029
1030 Ok(result_vec.into_pyarray(py))
1031}
1032
1033#[cfg(feature = "python")]
1034#[pyfunction(name = "linearreg_slope_batch")]
1035#[pyo3(signature = (data, period_range, kernel=None))]
1036pub fn linearreg_slope_batch_py<'py>(
1037 py: Python<'py>,
1038 data: numpy::PyReadonlyArray1<'py, f64>,
1039 period_range: (usize, usize, usize),
1040 kernel: Option<&str>,
1041) -> PyResult<Bound<'py, pyo3::types::PyDict>> {
1042 use numpy::{IntoPyArray, PyArray1, PyArrayMethods};
1043 use pyo3::types::PyDict;
1044 use std::mem::MaybeUninit;
1045
1046 let slice_in = data.as_slice()?;
1047 let sweep = LinearRegSlopeBatchRange {
1048 period: period_range,
1049 };
1050
1051 let combos = expand_grid(&sweep);
1052 let rows = combos.len();
1053 if rows == 0 {
1054 return Err(PyValueError::new_err(
1055 "linearreg_slope: invalid period range (empty expansion)",
1056 ));
1057 }
1058 let cols = slice_in.len();
1059 let total = rows
1060 .checked_mul(cols)
1061 .ok_or_else(|| PyValueError::new_err("linearreg_slope: rows*cols overflow"))?;
1062
1063 let out_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
1064 let slice_out = unsafe { out_arr.as_slice_mut()? };
1065
1066 let first = slice_in
1067 .iter()
1068 .position(|x| !x.is_nan())
1069 .ok_or_else(|| PyValueError::new_err("All values are NaN"))?;
1070 let warm: Vec<usize> = combos
1071 .iter()
1072 .map(|c| first + c.period.unwrap() - 1)
1073 .collect();
1074 unsafe {
1075 let out_mu: &mut [MaybeUninit<f64>] = core::slice::from_raw_parts_mut(
1076 slice_out.as_mut_ptr() as *mut MaybeUninit<f64>,
1077 slice_out.len(),
1078 );
1079 init_matrix_prefixes(out_mu, cols, &warm);
1080 }
1081
1082 let kern = validate_kernel(kernel, true)?;
1083 py.allow_threads(|| {
1084 let k = match kern {
1085 Kernel::Auto => detect_best_batch_kernel(),
1086 k => k,
1087 };
1088 let simd = match k {
1089 Kernel::Avx512Batch => Kernel::Avx512,
1090 Kernel::Avx2Batch => Kernel::Avx2,
1091 Kernel::ScalarBatch => Kernel::Scalar,
1092 _ => unreachable!(),
1093 };
1094 linearreg_slope_batch_inner_into(slice_in, &sweep, simd, true, slice_out)
1095 })
1096 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1097
1098 let dict = PyDict::new(py);
1099 dict.set_item("values", out_arr.reshape((rows, cols))?)?;
1100 dict.set_item(
1101 "periods",
1102 combos
1103 .iter()
1104 .map(|p| p.period.unwrap() as u64)
1105 .collect::<Vec<_>>()
1106 .into_pyarray(py),
1107 )?;
1108 Ok(dict)
1109}
1110
1111#[cfg(all(feature = "python", feature = "cuda"))]
1112use crate::cuda::moving_averages::CudaLinearregSlope;
1113#[cfg(all(feature = "python", feature = "cuda"))]
1114use crate::indicators::moving_averages::alma::DeviceArrayF32Py;
1115
1116#[cfg(all(feature = "python", feature = "cuda"))]
1117#[pyfunction(name = "linearreg_slope_cuda_batch_dev")]
1118#[pyo3(signature = (data_f32, period_range, device_id=0))]
1119pub fn linearreg_slope_cuda_batch_dev_py<'py>(
1120 py: Python<'py>,
1121 data_f32: numpy::PyReadonlyArray1<'py, f32>,
1122 period_range: (usize, usize, usize),
1123 device_id: usize,
1124) -> PyResult<(DeviceArrayF32Py, Bound<'py, PyDict>)> {
1125 use crate::cuda::cuda_available;
1126 use numpy::IntoPyArray;
1127 use pyo3::types::PyDict;
1128
1129 if !cuda_available() {
1130 return Err(PyValueError::new_err("CUDA not available"));
1131 }
1132
1133 let slice_in = data_f32.as_slice()?;
1134 let sweep = LinearRegSlopeBatchRange {
1135 period: period_range,
1136 };
1137
1138 let (inner, combos, ctx, dev_id) = py.allow_threads(|| {
1139 let cuda =
1140 CudaLinearregSlope::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
1141 let ctx = cuda.context_arc();
1142 let dev = cuda.device_id();
1143 cuda.linearreg_slope_batch_dev(slice_in, &sweep)
1144 .map(|(inner, combos)| (inner, combos, ctx, dev))
1145 .map_err(|e| PyValueError::new_err(e.to_string()))
1146 })?;
1147
1148 let dict = PyDict::new(py);
1149 let periods: Vec<u64> = combos.iter().map(|c| c.period.unwrap() as u64).collect();
1150 dict.set_item("periods", periods.into_pyarray(py))?;
1151
1152 Ok((
1153 DeviceArrayF32Py {
1154 inner,
1155 _ctx: Some(ctx),
1156 device_id: Some(dev_id),
1157 },
1158 dict,
1159 ))
1160}
1161
1162#[cfg(all(feature = "python", feature = "cuda"))]
1163#[pyfunction(name = "linearreg_slope_cuda_many_series_one_param_dev")]
1164#[pyo3(signature = (data_tm_f32, period, device_id=0))]
1165pub fn linearreg_slope_cuda_many_series_one_param_dev_py(
1166 py: Python<'_>,
1167 data_tm_f32: numpy::PyReadonlyArray2<'_, f32>,
1168 period: usize,
1169 device_id: usize,
1170) -> PyResult<DeviceArrayF32Py> {
1171 use crate::cuda::cuda_available;
1172 use numpy::PyUntypedArrayMethods;
1173
1174 if !cuda_available() {
1175 return Err(PyValueError::new_err("CUDA not available"));
1176 }
1177
1178 let flat_in = data_tm_f32.as_slice()?;
1179 let rows = data_tm_f32.shape()[0];
1180 let cols = data_tm_f32.shape()[1];
1181 let params = LinearRegSlopeParams {
1182 period: Some(period),
1183 };
1184
1185 let (inner, ctx, dev_id) = py.allow_threads(|| {
1186 let cuda =
1187 CudaLinearregSlope::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
1188 let ctx = cuda.context_arc();
1189 let dev = cuda.device_id();
1190 cuda.linearreg_slope_many_series_one_param_time_major_dev(flat_in, cols, rows, ¶ms)
1191 .map(|inner| (inner, ctx, dev))
1192 .map_err(|e| PyValueError::new_err(e.to_string()))
1193 })?;
1194
1195 Ok(DeviceArrayF32Py {
1196 inner,
1197 _ctx: Some(ctx),
1198 device_id: Some(dev_id),
1199 })
1200}
1201
1202#[cfg(feature = "python")]
1203#[pyclass(name = "LinearRegSlopeStream")]
1204pub struct LinearRegSlopeStreamPy {
1205 stream: LinearRegSlopeStream,
1206}
1207
1208#[cfg(feature = "python")]
1209#[pymethods]
1210impl LinearRegSlopeStreamPy {
1211 #[new]
1212 pub fn new(period: usize) -> PyResult<Self> {
1213 let params = LinearRegSlopeParams {
1214 period: Some(period),
1215 };
1216 let stream = LinearRegSlopeStream::try_new(params)
1217 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1218 Ok(Self { stream })
1219 }
1220
1221 pub fn update(&mut self, value: f64) -> Option<f64> {
1222 self.stream.update(value)
1223 }
1224}
1225
1226pub fn linearreg_slope_into_slice(
1227 dst: &mut [f64],
1228 input: &LinearRegSlopeInput,
1229 kern: Kernel,
1230) -> Result<(), LinearRegSlopeError> {
1231 let data: &[f64] = input.as_ref();
1232 if data.is_empty() {
1233 return Err(LinearRegSlopeError::EmptyInputData);
1234 }
1235 let period = input.get_period();
1236
1237 if period < 2 || period > data.len() {
1238 return Err(LinearRegSlopeError::InvalidPeriod {
1239 period,
1240 data_len: data.len(),
1241 });
1242 }
1243 if dst.len() != data.len() {
1244 return Err(LinearRegSlopeError::OutputLengthMismatch {
1245 expected: data.len(),
1246 got: dst.len(),
1247 });
1248 }
1249
1250 let first_valid_idx = match data.iter().position(|&x| !x.is_nan()) {
1251 Some(idx) => idx,
1252 None => return Err(LinearRegSlopeError::AllValuesNaN),
1253 };
1254 if (data.len() - first_valid_idx) < period {
1255 return Err(LinearRegSlopeError::NotEnoughValidData {
1256 needed: period,
1257 valid: data.len() - first_valid_idx,
1258 });
1259 }
1260
1261 let chosen = match kern {
1262 Kernel::Auto => Kernel::Scalar,
1263 other => other,
1264 };
1265
1266 unsafe {
1267 match chosen {
1268 Kernel::Scalar | Kernel::ScalarBatch => {
1269 linearreg_slope_scalar(data, period, first_valid_idx, dst)
1270 }
1271 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1272 Kernel::Avx2 | Kernel::Avx2Batch => {
1273 linearreg_slope_avx2(data, period, first_valid_idx, dst)
1274 }
1275 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1276 Kernel::Avx512 | Kernel::Avx512Batch => {
1277 linearreg_slope_avx512(data, period, first_valid_idx, dst)
1278 }
1279 _ => unreachable!(),
1280 }
1281 }
1282
1283 let warmup_end = first_valid_idx + period - 1;
1284 for v in &mut dst[..warmup_end] {
1285 *v = f64::NAN;
1286 }
1287
1288 Ok(())
1289}
1290
1291#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1292#[wasm_bindgen]
1293pub fn linearreg_slope_js(data: &[f64], period: usize) -> Result<Vec<f64>, JsValue> {
1294 let params = LinearRegSlopeParams {
1295 period: Some(period),
1296 };
1297 let input = LinearRegSlopeInput::from_slice(data, params);
1298
1299 let mut output = vec![0.0; data.len()];
1300 linearreg_slope_into_slice(&mut output, &input, Kernel::Auto)
1301 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1302
1303 Ok(output)
1304}
1305
1306#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
1307#[inline]
1308pub fn linearreg_slope_into(
1309 input: &LinearRegSlopeInput,
1310 out: &mut [f64],
1311) -> Result<(), LinearRegSlopeError> {
1312 linearreg_slope_into_slice(out, input, Kernel::Auto)
1313}
1314
1315#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1316#[wasm_bindgen]
1317pub fn linearreg_slope_into(
1318 in_ptr: *const f64,
1319 out_ptr: *mut f64,
1320 len: usize,
1321 period: usize,
1322) -> Result<(), JsValue> {
1323 if in_ptr.is_null() || out_ptr.is_null() {
1324 return Err(JsValue::from_str("Null pointer provided"));
1325 }
1326
1327 unsafe {
1328 let data = std::slice::from_raw_parts(in_ptr, len);
1329 let params = LinearRegSlopeParams {
1330 period: Some(period),
1331 };
1332 let input = LinearRegSlopeInput::from_slice(data, params);
1333
1334 if in_ptr == out_ptr {
1335 let mut temp = vec![0.0; len];
1336 linearreg_slope_into_slice(&mut temp, &input, detect_best_kernel())
1337 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1338 let out = std::slice::from_raw_parts_mut(out_ptr, len);
1339 out.copy_from_slice(&temp);
1340 } else {
1341 let out = std::slice::from_raw_parts_mut(out_ptr, len);
1342 linearreg_slope_into_slice(out, &input, detect_best_kernel())
1343 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1344 }
1345 Ok(())
1346 }
1347}
1348
1349#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1350#[wasm_bindgen]
1351pub fn linearreg_slope_alloc(len: usize) -> *mut f64 {
1352 let mut vec = Vec::<f64>::with_capacity(len);
1353 let ptr = vec.as_mut_ptr();
1354 std::mem::forget(vec);
1355 ptr
1356}
1357
1358#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1359#[wasm_bindgen]
1360pub fn linearreg_slope_free(ptr: *mut f64, len: usize) {
1361 if !ptr.is_null() {
1362 unsafe {
1363 let _ = Vec::from_raw_parts(ptr, len, len);
1364 }
1365 }
1366}
1367
1368#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1369#[derive(Serialize, Deserialize)]
1370pub struct LinearRegSlopeBatchConfig {
1371 pub period_range: (usize, usize, usize),
1372}
1373
1374#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1375#[derive(Serialize, Deserialize)]
1376pub struct LinearRegSlopeBatchJsOutput {
1377 pub values: Vec<f64>,
1378 pub combos: Vec<LinearRegSlopeParams>,
1379 pub rows: usize,
1380 pub cols: usize,
1381}
1382
1383#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1384#[wasm_bindgen(js_name = linearreg_slope_batch)]
1385pub fn linearreg_slope_batch_js(data: &[f64], config: JsValue) -> Result<JsValue, JsValue> {
1386 let config: LinearRegSlopeBatchConfig = serde_wasm_bindgen::from_value(config)
1387 .map_err(|e| JsValue::from_str(&format!("Invalid config: {}", e)))?;
1388
1389 let sweep = LinearRegSlopeBatchRange {
1390 period: config.period_range,
1391 };
1392
1393 let output = linearreg_slope_batch_inner(data, &sweep, Kernel::Scalar, false)
1394 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1395
1396 let js_output = LinearRegSlopeBatchJsOutput {
1397 values: output.values,
1398 combos: output.combos,
1399 rows: output.rows,
1400 cols: output.cols,
1401 };
1402
1403 serde_wasm_bindgen::to_value(&js_output)
1404 .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
1405}
1406
1407#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1408#[wasm_bindgen]
1409pub fn linearreg_slope_batch_into(
1410 in_ptr: *const f64,
1411 out_ptr: *mut f64,
1412 len: usize,
1413 period_start: usize,
1414 period_end: usize,
1415 period_step: usize,
1416) -> Result<usize, JsValue> {
1417 if in_ptr.is_null() || out_ptr.is_null() {
1418 return Err(JsValue::from_str(
1419 "null pointer passed to linearreg_slope_batch_into",
1420 ));
1421 }
1422 unsafe {
1423 let data = core::slice::from_raw_parts(in_ptr, len);
1424 let sweep = LinearRegSlopeBatchRange {
1425 period: (period_start, period_end, period_step),
1426 };
1427 let combos = expand_grid(&sweep);
1428 let rows = combos.len();
1429 if rows == 0 {
1430 return Err(JsValue::from_str(
1431 "linearreg_slope: invalid period range (empty expansion)",
1432 ));
1433 }
1434 let cols = len;
1435 let total = rows
1436 .checked_mul(cols)
1437 .ok_or_else(|| JsValue::from_str("linearreg_slope: rows*cols overflow"))?;
1438
1439 let out = core::slice::from_raw_parts_mut(out_ptr, total);
1440
1441 let first = data
1442 .iter()
1443 .position(|x| !x.is_nan())
1444 .ok_or_else(|| JsValue::from_str("All values are NaN"))?;
1445 let warm: Vec<usize> = combos
1446 .iter()
1447 .map(|c| first + c.period.unwrap() - 1)
1448 .collect();
1449 let out_mu =
1450 core::slice::from_raw_parts_mut(out.as_mut_ptr() as *mut MaybeUninit<f64>, out.len());
1451 init_matrix_prefixes(out_mu, cols, &warm);
1452
1453 linearreg_slope_batch_inner_into(data, &sweep, detect_best_kernel(), false, out)
1454 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1455
1456 Ok(rows)
1457 }
1458}
1459
1460#[cfg(test)]
1461mod tests {
1462 use super::*;
1463 use crate::skip_if_unsupported;
1464 use crate::utilities::data_loader::read_candles_from_csv;
1465
1466 fn check_linearreg_slope_partial_params(
1467 test_name: &str,
1468 kernel: Kernel,
1469 ) -> Result<(), Box<dyn Error>> {
1470 skip_if_unsupported!(kernel, test_name);
1471 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1472 let candles = read_candles_from_csv(file_path)?;
1473
1474 let default_params = LinearRegSlopeParams { period: None };
1475 let input = LinearRegSlopeInput::from_candles(&candles, "close", default_params);
1476 let output = linearreg_slope_with_kernel(&input, kernel)?;
1477 assert_eq!(output.values.len(), candles.close.len());
1478
1479 Ok(())
1480 }
1481
1482 fn check_linearreg_slope_accuracy(
1483 test_name: &str,
1484 kernel: Kernel,
1485 ) -> Result<(), Box<dyn Error>> {
1486 skip_if_unsupported!(kernel, test_name);
1487 let input_data = [100.0, 98.0, 95.0, 90.0, 85.0, 80.0, 78.0, 77.0, 79.0, 81.0];
1488 let params = LinearRegSlopeParams { period: Some(5) };
1489 let input = LinearRegSlopeInput::from_slice(&input_data, params);
1490 let result = linearreg_slope_with_kernel(&input, kernel)?;
1491 assert_eq!(result.values.len(), input_data.len());
1492 for val in &result.values[4..] {
1493 assert!(
1494 !val.is_nan(),
1495 "Expected valid slope values after period-1 index"
1496 );
1497 }
1498 Ok(())
1499 }
1500
1501 fn check_linearreg_slope_zero_period(
1502 test_name: &str,
1503 kernel: Kernel,
1504 ) -> Result<(), Box<dyn Error>> {
1505 skip_if_unsupported!(kernel, test_name);
1506 let input_data = [10.0, 20.0, 30.0];
1507 let params = LinearRegSlopeParams { period: Some(0) };
1508 let input = LinearRegSlopeInput::from_slice(&input_data, params);
1509 let res = linearreg_slope_with_kernel(&input, kernel);
1510 assert!(
1511 res.is_err(),
1512 "[{}] linearreg_slope should fail with zero period",
1513 test_name
1514 );
1515 Ok(())
1516 }
1517
1518 fn check_linearreg_slope_period_one(
1519 test_name: &str,
1520 kernel: Kernel,
1521 ) -> Result<(), Box<dyn Error>> {
1522 skip_if_unsupported!(kernel, test_name);
1523 let input_data = [10.0, 20.0, 30.0, 40.0, 50.0];
1524 let params = LinearRegSlopeParams { period: Some(1) };
1525 let input = LinearRegSlopeInput::from_slice(&input_data, params);
1526 let res = linearreg_slope_with_kernel(&input, kernel);
1527 assert!(
1528 res.is_err(),
1529 "[{}] linearreg_slope should fail with period=1 (needs at least 2 points for slope)",
1530 test_name
1531 );
1532
1533 if let Err(e) = res {
1534 let msg = e.to_string();
1535 assert!(
1536 msg.contains("Invalid period"),
1537 "[{}] Expected 'Invalid period' error, got: {}",
1538 test_name,
1539 msg
1540 );
1541 }
1542 Ok(())
1543 }
1544
1545 fn check_linearreg_slope_period_exceeds_length(
1546 test_name: &str,
1547 kernel: Kernel,
1548 ) -> Result<(), Box<dyn Error>> {
1549 skip_if_unsupported!(kernel, test_name);
1550 let data_small = [10.0, 20.0, 30.0];
1551 let params = LinearRegSlopeParams { period: Some(10) };
1552 let input = LinearRegSlopeInput::from_slice(&data_small, params);
1553 let res = linearreg_slope_with_kernel(&input, kernel);
1554 assert!(
1555 res.is_err(),
1556 "[{}] linearreg_slope should fail with period exceeding length",
1557 test_name
1558 );
1559 Ok(())
1560 }
1561
1562 fn check_linearreg_slope_very_small_dataset(
1563 test_name: &str,
1564 kernel: Kernel,
1565 ) -> Result<(), Box<dyn Error>> {
1566 skip_if_unsupported!(kernel, test_name);
1567 let single_point = [42.0];
1568 let params = LinearRegSlopeParams { period: Some(14) };
1569 let input = LinearRegSlopeInput::from_slice(&single_point, params);
1570 let res = linearreg_slope_with_kernel(&input, kernel);
1571 assert!(
1572 res.is_err(),
1573 "[{}] linearreg_slope should fail with insufficient data",
1574 test_name
1575 );
1576 Ok(())
1577 }
1578
1579 fn check_linearreg_slope_reinput(
1580 test_name: &str,
1581 kernel: Kernel,
1582 ) -> Result<(), Box<dyn Error>> {
1583 skip_if_unsupported!(kernel, test_name);
1584 let input_data = [10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0];
1585 let first_params = LinearRegSlopeParams { period: Some(3) };
1586 let first_input = LinearRegSlopeInput::from_slice(&input_data, first_params);
1587 let first_result = linearreg_slope_with_kernel(&first_input, kernel)?;
1588 let second_params = LinearRegSlopeParams { period: Some(3) };
1589 let second_input = LinearRegSlopeInput::from_slice(&first_result.values, second_params);
1590 let second_result = linearreg_slope_with_kernel(&second_input, kernel)?;
1591 assert_eq!(second_result.values.len(), first_result.values.len());
1592 Ok(())
1593 }
1594
1595 fn check_linearreg_slope_nan_handling(
1596 test_name: &str,
1597 kernel: Kernel,
1598 ) -> Result<(), Box<dyn Error>> {
1599 skip_if_unsupported!(kernel, test_name);
1600 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1601 let candles = read_candles_from_csv(file_path)?;
1602 let input = LinearRegSlopeInput::from_candles(
1603 &candles,
1604 "close",
1605 LinearRegSlopeParams { period: Some(14) },
1606 );
1607 let res = linearreg_slope_with_kernel(&input, kernel)?;
1608 assert_eq!(res.values.len(), candles.close.len());
1609 if res.values.len() > 240 {
1610 for (i, &val) in res.values[240..].iter().enumerate() {
1611 assert!(
1612 !val.is_nan(),
1613 "[{}] Found unexpected NaN at out-index {}",
1614 test_name,
1615 240 + i
1616 );
1617 }
1618 }
1619 Ok(())
1620 }
1621
1622 #[cfg(debug_assertions)]
1623 fn check_linearreg_slope_no_poison(
1624 test_name: &str,
1625 kernel: Kernel,
1626 ) -> Result<(), Box<dyn Error>> {
1627 skip_if_unsupported!(kernel, test_name);
1628
1629 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1630 let candles = read_candles_from_csv(file_path)?;
1631
1632 let test_params = vec![
1633 LinearRegSlopeParams::default(),
1634 LinearRegSlopeParams { period: Some(2) },
1635 LinearRegSlopeParams { period: Some(3) },
1636 LinearRegSlopeParams { period: Some(5) },
1637 LinearRegSlopeParams { period: Some(7) },
1638 LinearRegSlopeParams { period: Some(10) },
1639 LinearRegSlopeParams { period: Some(14) },
1640 LinearRegSlopeParams { period: Some(20) },
1641 LinearRegSlopeParams { period: Some(21) },
1642 LinearRegSlopeParams { period: Some(30) },
1643 LinearRegSlopeParams { period: Some(50) },
1644 LinearRegSlopeParams { period: Some(100) },
1645 LinearRegSlopeParams { period: Some(200) },
1646 ];
1647
1648 for (param_idx, params) in test_params.iter().enumerate() {
1649 let input = LinearRegSlopeInput::from_candles(&candles, "close", params.clone());
1650 let output = linearreg_slope_with_kernel(&input, kernel)?;
1651
1652 for (i, &val) in output.values.iter().enumerate() {
1653 if val.is_nan() {
1654 continue;
1655 }
1656
1657 let bits = val.to_bits();
1658
1659 if bits == 0x11111111_11111111 {
1660 panic!(
1661 "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at index {} \
1662 with params: period={} (param set {})",
1663 test_name,
1664 val,
1665 bits,
1666 i,
1667 params.period.unwrap_or(14),
1668 param_idx
1669 );
1670 }
1671
1672 if bits == 0x22222222_22222222 {
1673 panic!(
1674 "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at index {} \
1675 with params: period={} (param set {})",
1676 test_name,
1677 val,
1678 bits,
1679 i,
1680 params.period.unwrap_or(14),
1681 param_idx
1682 );
1683 }
1684
1685 if bits == 0x33333333_33333333 {
1686 panic!(
1687 "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at index {} \
1688 with params: period={} (param set {})",
1689 test_name,
1690 val,
1691 bits,
1692 i,
1693 params.period.unwrap_or(14),
1694 param_idx
1695 );
1696 }
1697 }
1698 }
1699
1700 Ok(())
1701 }
1702
1703 #[cfg(not(debug_assertions))]
1704 fn check_linearreg_slope_no_poison(
1705 _test_name: &str,
1706 _kernel: Kernel,
1707 ) -> Result<(), Box<dyn Error>> {
1708 Ok(())
1709 }
1710
1711 macro_rules! generate_all_linearreg_slope_tests {
1712 ($($test_fn:ident),*) => {
1713 paste::paste! {
1714 $(
1715 #[test]
1716 fn [<$test_fn _scalar_f64>]() {
1717 let _ = $test_fn(stringify!([<$test_fn _scalar_f64>]), Kernel::Scalar);
1718 }
1719 )*
1720 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1721 $(
1722 #[test]
1723 fn [<$test_fn _avx2_f64>]() {
1724 let _ = $test_fn(stringify!([<$test_fn _avx2_f64>]), Kernel::Avx2);
1725 }
1726 #[test]
1727 fn [<$test_fn _avx512_f64>]() {
1728 let _ = $test_fn(stringify!([<$test_fn _avx512_f64>]), Kernel::Avx512);
1729 }
1730 )*
1731 }
1732 }
1733 }
1734 generate_all_linearreg_slope_tests!(
1735 check_linearreg_slope_partial_params,
1736 check_linearreg_slope_accuracy,
1737 check_linearreg_slope_zero_period,
1738 check_linearreg_slope_period_one,
1739 check_linearreg_slope_period_exceeds_length,
1740 check_linearreg_slope_very_small_dataset,
1741 check_linearreg_slope_reinput,
1742 check_linearreg_slope_nan_handling,
1743 check_linearreg_slope_no_poison
1744 );
1745
1746 #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
1747 #[test]
1748 fn test_linearreg_slope_into_matches_api() -> Result<(), Box<dyn Error>> {
1749 let n = 512usize;
1750 let mut data = vec![0.0f64; n];
1751 for i in 0..n {
1752 let t = i as f64;
1753 data[i] = 1.0 + 0.01 * t + (t * 0.2).sin() * 0.5;
1754 }
1755
1756 let input = LinearRegSlopeInput::from_slice(&data, LinearRegSlopeParams::default());
1757
1758 let base = linearreg_slope(&input)?.values;
1759
1760 let mut into_out = vec![0.0f64; n];
1761 linearreg_slope_into(&input, &mut into_out)?;
1762
1763 #[inline]
1764 fn eq_or_both_nan(a: f64, b: f64) -> bool {
1765 (a.is_nan() && b.is_nan()) || (a == b) || ((a - b).abs() <= 1e-12)
1766 }
1767
1768 assert_eq!(base.len(), into_out.len());
1769 for i in 0..n {
1770 assert!(
1771 eq_or_both_nan(base[i], into_out[i]),
1772 "linearreg_slope_into mismatch at {}: base={}, into={}",
1773 i,
1774 base[i],
1775 into_out[i]
1776 );
1777 }
1778
1779 Ok(())
1780 }
1781
1782 fn check_batch_default_row(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1783 skip_if_unsupported!(kernel, test);
1784 let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1785 let c = read_candles_from_csv(file)?;
1786 let output = LinearRegSlopeBatchBuilder::new()
1787 .kernel(kernel)
1788 .apply_candles(&c, "close")?;
1789 let def = LinearRegSlopeParams::default();
1790 let row = output.values_for(&def).expect("default row missing");
1791 assert_eq!(row.len(), c.close.len());
1792 Ok(())
1793 }
1794
1795 #[cfg(debug_assertions)]
1796 fn check_batch_no_poison(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1797 skip_if_unsupported!(kernel, test);
1798
1799 let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1800 let c = read_candles_from_csv(file)?;
1801
1802 let test_configs = vec![
1803 (2, 10, 2),
1804 (5, 25, 5),
1805 (30, 60, 15),
1806 (2, 5, 1),
1807 (10, 30, 10),
1808 (14, 21, 7),
1809 (14, 14, 0),
1810 (50, 150, 25),
1811 (3, 15, 3),
1812 ];
1813
1814 for (cfg_idx, &(p_start, p_end, p_step)) in test_configs.iter().enumerate() {
1815 let output = LinearRegSlopeBatchBuilder::new()
1816 .kernel(kernel)
1817 .period_range(p_start, p_end, p_step)
1818 .apply_candles(&c, "close")?;
1819
1820 for (idx, &val) in output.values.iter().enumerate() {
1821 if val.is_nan() {
1822 continue;
1823 }
1824
1825 let bits = val.to_bits();
1826 let row = idx / output.cols;
1827 let col = idx % output.cols;
1828 let combo = &output.combos[row];
1829
1830 if bits == 0x11111111_11111111 {
1831 panic!(
1832 "[{}] Config {}: Found alloc_with_nan_prefix poison value {} (0x{:016X}) \
1833 at row {} col {} (flat index {}) with params: period={}",
1834 test,
1835 cfg_idx,
1836 val,
1837 bits,
1838 row,
1839 col,
1840 idx,
1841 combo.period.unwrap_or(14)
1842 );
1843 }
1844
1845 if bits == 0x22222222_22222222 {
1846 panic!(
1847 "[{}] Config {}: Found init_matrix_prefixes poison value {} (0x{:016X}) \
1848 at row {} col {} (flat index {}) with params: period={}",
1849 test,
1850 cfg_idx,
1851 val,
1852 bits,
1853 row,
1854 col,
1855 idx,
1856 combo.period.unwrap_or(14)
1857 );
1858 }
1859
1860 if bits == 0x33333333_33333333 {
1861 panic!(
1862 "[{}] Config {}: Found make_uninit_matrix poison value {} (0x{:016X}) \
1863 at row {} col {} (flat index {}) with params: period={}",
1864 test,
1865 cfg_idx,
1866 val,
1867 bits,
1868 row,
1869 col,
1870 idx,
1871 combo.period.unwrap_or(14)
1872 );
1873 }
1874 }
1875 }
1876
1877 Ok(())
1878 }
1879
1880 #[cfg(not(debug_assertions))]
1881 fn check_batch_no_poison(_test: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
1882 Ok(())
1883 }
1884
1885 macro_rules! gen_batch_tests {
1886 ($fn_name:ident) => {
1887 paste::paste! {
1888 #[test] fn [<$fn_name _scalar>]() {
1889 let _ = $fn_name(stringify!([<$fn_name _scalar>]), Kernel::ScalarBatch);
1890 }
1891 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1892 #[test] fn [<$fn_name _avx2>]() {
1893 let _ = $fn_name(stringify!([<$fn_name _avx2>]), Kernel::Avx2Batch);
1894 }
1895 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1896 #[test] fn [<$fn_name _avx512>]() {
1897 let _ = $fn_name(stringify!([<$fn_name _avx512>]), Kernel::Avx512Batch);
1898 }
1899 #[test] fn [<$fn_name _auto_detect>]() {
1900 let _ = $fn_name(stringify!([<$fn_name _auto_detect>]), Kernel::Auto);
1901 }
1902 }
1903 };
1904 }
1905 gen_batch_tests!(check_batch_default_row);
1906 gen_batch_tests!(check_batch_no_poison);
1907
1908 #[cfg(test)]
1909 fn check_linearreg_slope_property(
1910 test_name: &str,
1911 kernel: Kernel,
1912 ) -> Result<(), Box<dyn std::error::Error>> {
1913 use proptest::prelude::*;
1914 skip_if_unsupported!(kernel, test_name);
1915
1916 let strat = (2usize..=100)
1917 .prop_flat_map(|period| {
1918 (
1919 prop::collection::vec(
1920 (-1e6f64..1e6f64).prop_filter("finite", |x| x.is_finite()),
1921 period..=500,
1922 ),
1923 Just(period),
1924 0usize..=5,
1925 )
1926 })
1927 .prop_map(|(mut data, period, scenario)| {
1928 match scenario {
1929 0 => {}
1930 1 => {
1931 let constant = data.get(0).copied().unwrap_or(100.0);
1932 data.iter_mut().for_each(|x| *x = constant);
1933 }
1934 2 => {
1935 for (i, val) in data.iter_mut().enumerate() {
1936 *val = 2.0 * i as f64 + 10.0;
1937 }
1938 }
1939 3 => {
1940 let mut base = 100.0;
1941 for val in data.iter_mut() {
1942 *val = base;
1943 base += (0.1 + (*val).abs() * 1e-6);
1944 }
1945 }
1946 4 => {
1947 let mut base = 1000.0;
1948 for val in data.iter_mut() {
1949 *val = base;
1950 base -= (0.1 + (*val).abs() * 1e-6);
1951 }
1952 }
1953 5 => {
1954 for (i, val) in data.iter_mut().enumerate() {
1955 *val = if i % 20 == 0 {
1956 1000.0 * (if i % 40 == 0 { 1.0 } else { -1.0 })
1957 } else {
1958 10.0 + i as f64 * 0.5
1959 };
1960 }
1961 }
1962 _ => unreachable!(),
1963 }
1964 (data, period)
1965 });
1966
1967 proptest::test_runner::TestRunner::default()
1968 .run(&strat, |(data, period)| {
1969 let params = LinearRegSlopeParams {
1970 period: Some(period),
1971 };
1972 let input = LinearRegSlopeInput::from_slice(&data, params);
1973
1974 let LinearRegSlopeOutput { values: out } =
1975 linearreg_slope_with_kernel(&input, kernel).unwrap();
1976
1977 let LinearRegSlopeOutput { values: ref_out } =
1978 linearreg_slope_with_kernel(&input, Kernel::Scalar).unwrap();
1979
1980 for i in 0..(period - 1).min(data.len()) {
1981 prop_assert!(
1982 out[i].is_nan(),
1983 "Expected NaN during warmup at index {}, got {}",
1984 i,
1985 out[i]
1986 );
1987 }
1988
1989 for i in (period - 1)..data.len() {
1990 let window = &data[i + 1 - period..=i];
1991 let y = out[i];
1992 let r = ref_out[i];
1993
1994 if y.is_finite() && r.is_finite() {
1995 let y_bits = y.to_bits();
1996 let r_bits = r.to_bits();
1997 let ulp_diff: u64 = y_bits.abs_diff(r_bits);
1998
1999 prop_assert!(
2000 (y - r).abs() <= 1e-9 || ulp_diff <= 8,
2001 "Kernel mismatch at idx {}: {} vs {} (ULP={})",
2002 i,
2003 y,
2004 r,
2005 ulp_diff
2006 );
2007 } else {
2008 prop_assert_eq!(
2009 y.is_nan(),
2010 r.is_nan(),
2011 "NaN mismatch at idx {}: {} vs {}",
2012 i,
2013 y,
2014 r
2015 );
2016 }
2017
2018 if window
2019 .windows(2)
2020 .all(|w| (w[0] - w[1]).abs() < f64::EPSILON)
2021 {
2022 prop_assert!(
2023 y.abs() <= 1e-8,
2024 "Expected slope ~0 for constant data at idx {}, got {}",
2025 i,
2026 y
2027 );
2028 }
2029
2030 let is_linear = {
2031 if period >= 3 {
2032 let x1 = 0.0;
2033 let y1 = window[0];
2034 let x2 = (period - 1) as f64;
2035 let y2 = window[period - 1];
2036 let expected_slope = (y2 - y1) / (x2 - x1);
2037
2038 let mut is_linear = true;
2039 for (j, &val) in window.iter().enumerate() {
2040 let expected = y1 + expected_slope * j as f64;
2041 if (val - expected).abs() > 1e-9 {
2042 is_linear = false;
2043 break;
2044 }
2045 }
2046
2047 if is_linear {
2048 prop_assert!(
2049 (y - expected_slope).abs() <= 1e-9,
2050 "Linear data slope mismatch at idx {}: {} vs expected {}",
2051 i,
2052 y,
2053 expected_slope
2054 );
2055 }
2056 is_linear
2057 } else {
2058 false
2059 }
2060 };
2061
2062 let is_increasing = window.windows(2).all(|w| w[1] > w[0]);
2063 if is_increasing && !is_linear {
2064 prop_assert!(
2065 y > 1e-8,
2066 "Expected positive slope for increasing data at idx {}, got {}",
2067 i,
2068 y
2069 );
2070 }
2071
2072 let is_decreasing = window.windows(2).all(|w| w[1] < w[0]);
2073 if is_decreasing && !is_linear {
2074 prop_assert!(
2075 y < -1e-8,
2076 "Expected negative slope for decreasing data at idx {}, got {}",
2077 i,
2078 y
2079 );
2080 }
2081
2082 if y.is_finite() {
2083 let data_range = window.iter().cloned().fold(f64::NEG_INFINITY, f64::max)
2084 - window.iter().cloned().fold(f64::INFINITY, f64::min);
2085
2086 if data_range < 1e-9 {
2087 prop_assert!(
2088 y.abs() <= 1e-6,
2089 "Expected near-zero slope for constant data at idx {}, got {}",
2090 i,
2091 y
2092 );
2093 } else {
2094 let max_slope = data_range / (period as f64 * 0.5);
2095
2096 prop_assert!(
2097 y.abs() <= max_slope * 5.0,
2098 "Slope magnitude too large at idx {}: {} (max expected ~{})",
2099 i,
2100 y.abs(),
2101 max_slope
2102 );
2103 }
2104 }
2105
2106 prop_assert!(!y.is_infinite(), "Found infinite value at idx {}: {}", i, y);
2107 }
2108
2109 Ok(())
2110 })
2111 .unwrap();
2112
2113 Ok(())
2114 }
2115
2116 #[cfg(test)]
2117 generate_all_linearreg_slope_tests!(check_linearreg_slope_property);
2118}