Skip to main content

vector_ta/indicators/
linearreg_slope.rs

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, &params)
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}