vector_ta/indicators/
linearreg_angle.rs

1#[cfg(all(feature = "python", feature = "cuda"))]
2use crate::cuda::{cuda_available, CudaLinearregAngle};
3#[cfg(all(feature = "python", feature = "cuda"))]
4use crate::utilities::dlpack_cuda::export_f32_cuda_dlpack_2d;
5#[cfg(all(feature = "python", feature = "cuda"))]
6use cust::context::Context;
7#[cfg(all(feature = "python", feature = "cuda"))]
8use cust::memory::DeviceBuffer;
9#[cfg(feature = "python")]
10use numpy::{IntoPyArray, PyArray1};
11#[cfg(feature = "python")]
12use pyo3::exceptions::PyValueError;
13#[cfg(feature = "python")]
14use pyo3::prelude::*;
15#[cfg(feature = "python")]
16use pyo3::types::{PyDict, PyList};
17#[cfg(all(feature = "python", feature = "cuda"))]
18use std::sync::Arc;
19
20#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
21use serde::{Deserialize, Serialize};
22#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
23use wasm_bindgen::prelude::*;
24
25use crate::utilities::data_loader::{source_type, Candles};
26use crate::utilities::enums::Kernel;
27use crate::utilities::helpers::{
28    alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
29    make_uninit_matrix,
30};
31#[cfg(feature = "python")]
32use crate::utilities::kernel_validation::validate_kernel;
33#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
34use core::arch::x86_64::*;
35#[cfg(not(target_arch = "wasm32"))]
36use rayon::prelude::*;
37use std::convert::AsRef;
38use std::error::Error;
39use std::f64::consts::PI;
40use thiserror::Error;
41
42#[derive(Debug, Clone)]
43pub enum Linearreg_angleData<'a> {
44    Candles {
45        candles: &'a Candles,
46        source: &'a str,
47    },
48    Slice(&'a [f64]),
49}
50
51impl<'a> AsRef<[f64]> for Linearreg_angleInput<'a> {
52    #[inline(always)]
53    fn as_ref(&self) -> &[f64] {
54        match &self.data {
55            Linearreg_angleData::Slice(slice) => slice,
56            Linearreg_angleData::Candles { candles, source } => source_type(candles, source),
57        }
58    }
59}
60
61#[derive(Debug, Clone)]
62pub struct Linearreg_angleOutput {
63    pub values: Vec<f64>,
64}
65
66#[derive(Debug, Clone)]
67#[cfg_attr(
68    all(target_arch = "wasm32", feature = "wasm"),
69    derive(Serialize, Deserialize)
70)]
71pub struct Linearreg_angleParams {
72    pub period: Option<usize>,
73}
74
75impl Default for Linearreg_angleParams {
76    fn default() -> Self {
77        Self { period: Some(14) }
78    }
79}
80
81#[derive(Debug, Clone)]
82pub struct Linearreg_angleInput<'a> {
83    pub data: Linearreg_angleData<'a>,
84    pub params: Linearreg_angleParams,
85}
86
87impl<'a> Linearreg_angleInput<'a> {
88    #[inline]
89    pub fn from_candles(c: &'a Candles, s: &'a str, p: Linearreg_angleParams) -> Self {
90        Self {
91            data: Linearreg_angleData::Candles {
92                candles: c,
93                source: s,
94            },
95            params: p,
96        }
97    }
98    #[inline]
99    pub fn from_slice(sl: &'a [f64], p: Linearreg_angleParams) -> Self {
100        Self {
101            data: Linearreg_angleData::Slice(sl),
102            params: p,
103        }
104    }
105    #[inline]
106    pub fn with_default_candles(c: &'a Candles) -> Self {
107        Self::from_candles(c, "close", Linearreg_angleParams::default())
108    }
109    #[inline]
110    pub fn get_period(&self) -> usize {
111        self.params.period.unwrap_or(14)
112    }
113}
114
115#[derive(Copy, Clone, Debug)]
116pub struct Linearreg_angleBuilder {
117    period: Option<usize>,
118    kernel: Kernel,
119}
120
121impl Default for Linearreg_angleBuilder {
122    fn default() -> Self {
123        Self {
124            period: None,
125            kernel: Kernel::Auto,
126        }
127    }
128}
129
130impl Linearreg_angleBuilder {
131    #[inline(always)]
132    pub fn new() -> Self {
133        Self::default()
134    }
135    #[inline(always)]
136    pub fn period(mut self, n: usize) -> Self {
137        self.period = Some(n);
138        self
139    }
140    #[inline(always)]
141    pub fn kernel(mut self, k: Kernel) -> Self {
142        self.kernel = k;
143        self
144    }
145    #[inline(always)]
146    pub fn apply(self, c: &Candles) -> Result<Linearreg_angleOutput, Linearreg_angleError> {
147        let p = Linearreg_angleParams {
148            period: self.period,
149        };
150        let i = Linearreg_angleInput::from_candles(c, "close", p);
151        linearreg_angle_with_kernel(&i, self.kernel)
152    }
153    #[inline(always)]
154    pub fn apply_slice(self, d: &[f64]) -> Result<Linearreg_angleOutput, Linearreg_angleError> {
155        let p = Linearreg_angleParams {
156            period: self.period,
157        };
158        let i = Linearreg_angleInput::from_slice(d, p);
159        linearreg_angle_with_kernel(&i, self.kernel)
160    }
161    #[inline(always)]
162    pub fn into_stream(self) -> Result<Linearreg_angleStream, Linearreg_angleError> {
163        let p = Linearreg_angleParams {
164            period: self.period,
165        };
166        Linearreg_angleStream::try_new(p)
167    }
168}
169
170#[derive(Debug, Error)]
171pub enum Linearreg_angleError {
172    #[error("linearreg_angle: Empty data slice.")]
173    EmptyInputData,
174    #[error("linearreg_angle: All values are NaN.")]
175    AllValuesNaN,
176    #[error("linearreg_angle: Invalid period: period = {period}, data length = {data_len}")]
177    InvalidPeriod { period: usize, data_len: usize },
178    #[error("linearreg_angle: Not enough valid data: needed = {needed}, valid = {valid}")]
179    NotEnoughValidData { needed: usize, valid: usize },
180    #[error("linearreg_angle: Output length mismatch: expected = {expected}, actual = {got}")]
181    OutputLengthMismatch { expected: usize, got: usize },
182    #[error("linearreg_angle: Invalid range: start={start}, end={end}, step={step}")]
183    InvalidRange {
184        start: usize,
185        end: usize,
186        step: usize,
187    },
188    #[error("linearreg_angle: Invalid kernel type for batch operation: {0:?}")]
189    InvalidKernelForBatch(Kernel),
190}
191
192#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
193impl From<Linearreg_angleError> for JsValue {
194    fn from(err: Linearreg_angleError) -> Self {
195        JsValue::from_str(&err.to_string())
196    }
197}
198
199#[inline]
200pub fn linearreg_angle(
201    input: &Linearreg_angleInput,
202) -> Result<Linearreg_angleOutput, Linearreg_angleError> {
203    linearreg_angle_with_kernel(input, Kernel::Auto)
204}
205
206#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
207pub fn linearreg_angle_into(
208    input: &Linearreg_angleInput,
209    out: &mut [f64],
210) -> Result<(), Linearreg_angleError> {
211    linearreg_angle_into_slice(out, input, Kernel::Auto)
212}
213
214pub fn linearreg_angle_with_kernel(
215    input: &Linearreg_angleInput,
216    kernel: Kernel,
217) -> Result<Linearreg_angleOutput, Linearreg_angleError> {
218    let data: &[f64] = input.as_ref();
219    if data.is_empty() {
220        return Err(Linearreg_angleError::EmptyInputData);
221    }
222
223    let first = data
224        .iter()
225        .position(|x| !x.is_nan())
226        .ok_or(Linearreg_angleError::AllValuesNaN)?;
227    let len = data.len();
228    let period = input.get_period();
229
230    if period < 2 || period > len {
231        return Err(Linearreg_angleError::InvalidPeriod {
232            period,
233            data_len: len,
234        });
235    }
236    if (len - first) < period {
237        return Err(Linearreg_angleError::NotEnoughValidData {
238            needed: period,
239            valid: len - first,
240        });
241    }
242
243    let chosen = match kernel {
244        Kernel::Auto => Kernel::Scalar,
245        other => other,
246    };
247
248    let mut out = alloc_with_nan_prefix(len, first + period - 1);
249
250    unsafe {
251        match chosen {
252            Kernel::Scalar | Kernel::ScalarBatch => {
253                linearreg_angle_scalar(data, period, first, &mut out)
254            }
255            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
256            Kernel::Avx2 | Kernel::Avx2Batch => linearreg_angle_avx2(data, period, first, &mut out),
257            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
258            Kernel::Avx512 | Kernel::Avx512Batch => {
259                linearreg_angle_avx512(data, period, first, &mut out)
260            }
261            _ => unreachable!(),
262        }
263    }
264
265    Ok(Linearreg_angleOutput { values: out })
266}
267
268#[inline]
269pub fn linearreg_angle_scalar(data: &[f64], period: usize, first_valid: usize, out: &mut [f64]) {
270    let p = period as f64;
271    let sum_x = (period * (period - 1)) as f64 * 0.5;
272    let sum_x_sqr = (period * (period - 1) * (2 * period - 1)) as f64 / 6.0;
273    let divisor = sum_x * sum_x - p * sum_x_sqr;
274    let inv_div = 1.0 / divisor;
275    let rad2deg = 180.0 / PI;
276
277    let n = data.len();
278    let mut i = first_valid + period - 1;
279    if i >= n {
280        return;
281    }
282
283    let mut start = i + 1 - period;
284    let mut sum_y = 0.0;
285    let mut sum_kd = 0.0;
286
287    let has_nan = data[first_valid..].iter().any(|v| v.is_nan());
288
289    unsafe {
290        let mut j = start;
291        let end = i + 1;
292        while j + 3 < end {
293            let y0 = *data.get_unchecked(j);
294            let y1 = *data.get_unchecked(j + 1);
295            let y2 = *data.get_unchecked(j + 2);
296            let y3 = *data.get_unchecked(j + 3);
297
298            sum_y += y0 + y1 + y2 + y3;
299            let jf = j as f64;
300            sum_kd += jf * y0 + (jf + 1.0) * y1 + (jf + 2.0) * y2 + (jf + 3.0) * y3;
301
302            j += 4;
303        }
304        while j < end {
305            let y = *data.get_unchecked(j);
306            sum_y += y;
307            sum_kd += (j as f64) * y;
308            j += 1;
309        }
310
311        if !has_nan {
312            loop {
313                let i_f = i as f64;
314                let sum_xy = i_f * sum_y - sum_kd;
315                let num = p.mul_add(sum_xy, -sum_x * sum_y);
316                let slope = num * inv_div;
317                *out.get_unchecked_mut(i) = slope.atan() * rad2deg;
318
319                i += 1;
320                if i >= n {
321                    break;
322                }
323
324                let enter = *data.get_unchecked(i);
325                let leave = *data.get_unchecked(start);
326                start += 1;
327
328                sum_y += enter - leave;
329                sum_kd += (i as f64) * enter - ((i - period) as f64) * leave;
330            }
331        } else {
332            loop {
333                let i_f = i as f64;
334                let sum_xy = i_f * sum_y - sum_kd;
335                let num = p.mul_add(sum_xy, -sum_x * sum_y);
336                let slope = num * inv_div;
337                *out.get_unchecked_mut(i) = slope.atan() * rad2deg;
338
339                i += 1;
340                if i >= n {
341                    break;
342                }
343
344                let enter = *data.get_unchecked(i);
345                let leave = *data.get_unchecked(start);
346                start += 1;
347
348                if enter.is_nan() | leave.is_nan() {
349                    sum_y = 0.0;
350                    sum_kd = 0.0;
351                    let ws = i + 1 - period;
352                    let mut jj = ws;
353                    let ee = i + 1;
354                    while jj + 3 < ee {
355                        let y0 = *data.get_unchecked(jj);
356                        let y1 = *data.get_unchecked(jj + 1);
357                        let y2 = *data.get_unchecked(jj + 2);
358                        let y3 = *data.get_unchecked(jj + 3);
359
360                        sum_y += y0 + y1 + y2 + y3;
361                        let jf = jj as f64;
362                        sum_kd += jf * y0 + (jf + 1.0) * y1 + (jf + 2.0) * y2 + (jf + 3.0) * y3;
363                        jj += 4;
364                    }
365                    while jj < ee {
366                        let y = *data.get_unchecked(jj);
367                        sum_y += y;
368                        sum_kd += (jj as f64) * y;
369                        jj += 1;
370                    }
371                } else {
372                    sum_y += enter - leave;
373                    sum_kd += (i as f64) * enter - ((i - period) as f64) * leave;
374                }
375            }
376        }
377    }
378}
379
380#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
381#[inline]
382pub fn linearreg_angle_avx512(data: &[f64], period: usize, first_valid: usize, out: &mut [f64]) {
383    if period <= 32 {
384        unsafe { linearreg_angle_avx512_short(data, period, first_valid, out) }
385    } else {
386        unsafe { linearreg_angle_avx512_long(data, period, first_valid, out) }
387    }
388}
389
390#[inline]
391pub fn linearreg_angle_avx2(data: &[f64], period: usize, first_valid: usize, out: &mut [f64]) {
392    linearreg_angle_scalar(data, period, first_valid, out)
393}
394
395#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
396#[inline]
397pub fn linearreg_angle_avx512_short(
398    data: &[f64],
399    period: usize,
400    first_valid: usize,
401    out: &mut [f64],
402) {
403    linearreg_angle_scalar(data, period, first_valid, out)
404}
405
406#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
407#[inline]
408pub fn linearreg_angle_avx512_long(
409    data: &[f64],
410    period: usize,
411    first_valid: usize,
412    out: &mut [f64],
413) {
414    linearreg_angle_scalar(data, period, first_valid, out)
415}
416
417#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
418#[target_feature(enable = "avx512f,avx512dq,fma")]
419unsafe fn linearreg_angle_avx512_impl(
420    data: &[f64],
421    period: usize,
422    first_valid: usize,
423    out: &mut [f64],
424) {
425    use core::arch::x86_64::*;
426
427    if data[first_valid..].iter().any(|v| v.is_nan()) {
428        return linearreg_angle_scalar(data, period, first_valid, out);
429    }
430
431    let n = data.len();
432    let start_i = first_valid + period - 1;
433    if start_i >= n {
434        return;
435    }
436
437    let p = period as f64;
438    let sum_x = (period * (period - 1)) as f64 * 0.5;
439    let sum_x_sqr = (period * (period - 1) * (2 * period - 1)) as f64 / 6.0;
440    let divisor = sum_x * sum_x - p * sum_x_sqr;
441    let inv_div = 1.0 / divisor;
442    let rad2deg = 180.0 / PI;
443
444    let mut s = vec![0.0f64; n + 1];
445    let mut k = vec![0.0f64; n + 1];
446    let mut acc_s = 0.0f64;
447    let mut acc_k = 0.0f64;
448
449    let mut idx = 0usize;
450    while idx + 3 < n {
451        let y0 = *data.get_unchecked(idx);
452        let y1 = *data.get_unchecked(idx + 1);
453        let y2 = *data.get_unchecked(idx + 2);
454        let y3 = *data.get_unchecked(idx + 3);
455
456        acc_s += y0;
457        s[idx + 1] = acc_s;
458        acc_k += (idx as f64) * y0;
459        k[idx + 1] = acc_k;
460        acc_s += y1;
461        s[idx + 2] = acc_s;
462        acc_k += ((idx + 1) as f64) * y1;
463        k[idx + 2] = acc_k;
464        acc_s += y2;
465        s[idx + 3] = acc_s;
466        acc_k += ((idx + 2) as f64) * y2;
467        k[idx + 3] = acc_k;
468        acc_s += y3;
469        s[idx + 4] = acc_s;
470        acc_k += ((idx + 3) as f64) * y3;
471        k[idx + 4] = acc_k;
472
473        idx += 4;
474    }
475    while idx < n {
476        let y = *data.get_unchecked(idx);
477        acc_s += y;
478        s[idx + 1] = acc_s;
479        acc_k += (idx as f64) * y;
480        k[idx + 1] = acc_k;
481        idx += 1;
482    }
483
484    let v_p = _mm512_set1_pd(p);
485    let v_nsumx = _mm512_set1_pd(-sum_x);
486    let v_invdiv = _mm512_set1_pd(inv_div);
487
488    let mut i = start_i;
489    let width = 8usize;
490
491    while i + width <= n {
492        let s_hi = _mm512_loadu_pd(s.as_ptr().add(i + 1));
493        let s_lo = _mm512_loadu_pd(s.as_ptr().add(i + 1 - period));
494        let sum_y = _mm512_sub_pd(s_hi, s_lo);
495
496        let k_hi = _mm512_loadu_pd(k.as_ptr().add(i + 1));
497        let k_lo = _mm512_loadu_pd(k.as_ptr().add(i + 1 - period));
498        let sum_kd = _mm512_sub_pd(k_hi, k_lo);
499
500        let base = i as f64;
501        let v_i = _mm512_setr_pd(
502            base,
503            base + 1.0,
504            base + 2.0,
505            base + 3.0,
506            base + 4.0,
507            base + 5.0,
508            base + 6.0,
509            base + 7.0,
510        );
511
512        let sum_xy = _mm512_fnmadd_pd(v_i, sum_y, sum_kd);
513        let sum_xy = _mm512_sub_pd(_mm512_setzero_pd(), sum_xy);
514
515        let num = _mm512_fmadd_pd(v_p, sum_xy, _mm512_mul_pd(v_nsumx, sum_y));
516        let slope = _mm512_mul_pd(num, v_invdiv);
517
518        let mut tmp: [f64; 8] = core::mem::zeroed();
519        _mm512_storeu_pd(tmp.as_mut_ptr(), slope);
520
521        *out.get_unchecked_mut(i) = tmp[0].atan() * rad2deg;
522        *out.get_unchecked_mut(i + 1) = tmp[1].atan() * rad2deg;
523        *out.get_unchecked_mut(i + 2) = tmp[2].atan() * rad2deg;
524        *out.get_unchecked_mut(i + 3) = tmp[3].atan() * rad2deg;
525        *out.get_unchecked_mut(i + 4) = tmp[4].atan() * rad2deg;
526        *out.get_unchecked_mut(i + 5) = tmp[5].atan() * rad2deg;
527        *out.get_unchecked_mut(i + 6) = tmp[6].atan() * rad2deg;
528        *out.get_unchecked_mut(i + 7) = tmp[7].atan() * rad2deg;
529
530        i += width;
531    }
532
533    while i < n {
534        let sum_y = *s.get_unchecked(i + 1) - *s.get_unchecked(i + 1 - period);
535        let sum_kd = *k.get_unchecked(i + 1) - *k.get_unchecked(i + 1 - period);
536        let sum_xy = (i as f64) * sum_y - sum_kd;
537        let num = p.mul_add(sum_xy, -sum_x * sum_y);
538        let slope = num * (1.0 / divisor);
539        *out.get_unchecked_mut(i) = slope.atan() * rad2deg;
540        i += 1;
541    }
542}
543
544#[derive(Debug, Clone)]
545pub struct Linearreg_angleStream {
546    period: usize,
547
548    ring: Vec<f64>,
549    head: usize,
550    len: usize,
551
552    sum_y: f64,
553    sum_kd: f64,
554
555    idx: usize,
556
557    p: f64,
558    sum_x: f64,
559    inv_div: f64,
560    rad2deg: f64,
561
562    params: Linearreg_angleParams,
563}
564
565impl Linearreg_angleStream {
566    pub fn try_new(params: Linearreg_angleParams) -> Result<Self, Linearreg_angleError> {
567        let period = params.period.unwrap_or(14);
568        if period < 2 {
569            return Err(Linearreg_angleError::InvalidPeriod {
570                period,
571                data_len: 0,
572            });
573        }
574
575        let p = period as f64;
576        let sum_x = (period * (period - 1)) as f64 * 0.5;
577        let sum_x_sqr = (period * (period - 1) * (2 * period - 1)) as f64 / 6.0;
578        let divisor = sum_x * sum_x - p * sum_x_sqr;
579        let inv_div = 1.0 / divisor;
580
581        Ok(Self {
582            period,
583            ring: vec![f64::NAN; period],
584            head: 0,
585            len: 0,
586            sum_y: 0.0,
587            sum_kd: 0.0,
588            idx: 0,
589            p,
590            sum_x,
591            inv_div,
592            rad2deg: 180.0 / std::f64::consts::PI,
593            params,
594        })
595    }
596
597    #[inline(always)]
598    pub fn update(&mut self, value: f64) -> Option<f64> {
599        let i = self.idx;
600        let had_full = self.len == self.period;
601        let leave = if had_full { self.ring[self.head] } else { 0.0 };
602
603        self.ring[self.head] = value;
604        self.head += 1;
605        if self.head == self.period {
606            self.head = 0;
607        }
608
609        if self.len < self.period {
610            self.len += 1;
611            self.sum_y += value;
612            self.sum_kd += (i as f64) * value;
613        } else if value.is_nan() | leave.is_nan() {
614            self.rebuild_window_sums(i);
615        } else {
616            self.sum_y += value - leave;
617            self.sum_kd += (i as f64) * value - ((i - self.period) as f64) * leave;
618        }
619
620        let out = if self.len < self.period {
621            None
622        } else {
623            let sum_xy = (i as f64) * self.sum_y - self.sum_kd;
624            let num = self.p.mul_add(sum_xy, -self.sum_x * self.sum_y);
625            let slope = num * self.inv_div;
626            Some(slope.atan() * self.rad2deg)
627        };
628
629        self.idx = i + 1;
630        out
631    }
632
633    #[inline(always)]
634    fn rebuild_window_sums(&mut self, i: usize) {
635        let win_len = self.len;
636        let mut s_y = 0.0f64;
637        let mut s_kd = 0.0f64;
638
639        let start_abs = i + 1 - win_len;
640
641        for j in 0..win_len {
642            let pos = self.head + j;
643            let rix = if pos >= self.period {
644                pos - self.period
645            } else {
646                pos
647            };
648            let y = self.ring[rix];
649            let k = (start_abs + j) as f64;
650            s_y += y;
651            s_kd += k * y;
652        }
653        self.sum_y = s_y;
654        self.sum_kd = s_kd;
655    }
656}
657
658#[derive(Clone, Debug)]
659pub struct Linearreg_angleBatchRange {
660    pub period: (usize, usize, usize),
661}
662
663impl Default for Linearreg_angleBatchRange {
664    fn default() -> Self {
665        Self {
666            period: (14, 263, 1),
667        }
668    }
669}
670
671#[derive(Clone, Debug, Default)]
672pub struct Linearreg_angleBatchBuilder {
673    range: Linearreg_angleBatchRange,
674    kernel: Kernel,
675}
676
677impl Linearreg_angleBatchBuilder {
678    pub fn new() -> Self {
679        Self::default()
680    }
681    pub fn kernel(mut self, k: Kernel) -> Self {
682        self.kernel = k;
683        self
684    }
685    #[inline]
686    pub fn period_range(mut self, start: usize, end: usize, step: usize) -> Self {
687        self.range.period = (start, end, step);
688        self
689    }
690    #[inline]
691    pub fn period_static(mut self, p: usize) -> Self {
692        self.range.period = (p, p, 0);
693        self
694    }
695    pub fn apply_slice(
696        self,
697        data: &[f64],
698    ) -> Result<Linearreg_angleBatchOutput, Linearreg_angleError> {
699        linearreg_angle_batch_with_kernel(data, &self.range, self.kernel)
700    }
701    pub fn with_default_slice(
702        data: &[f64],
703        k: Kernel,
704    ) -> Result<Linearreg_angleBatchOutput, Linearreg_angleError> {
705        Linearreg_angleBatchBuilder::new()
706            .kernel(k)
707            .apply_slice(data)
708    }
709    pub fn apply_candles(
710        self,
711        c: &Candles,
712        src: &str,
713    ) -> Result<Linearreg_angleBatchOutput, Linearreg_angleError> {
714        let slice = source_type(c, src);
715        self.apply_slice(slice)
716    }
717    pub fn with_default_candles(
718        c: &Candles,
719    ) -> Result<Linearreg_angleBatchOutput, Linearreg_angleError> {
720        Linearreg_angleBatchBuilder::new()
721            .kernel(Kernel::Auto)
722            .apply_candles(c, "close")
723    }
724}
725
726pub fn linearreg_angle_batch_with_kernel(
727    data: &[f64],
728    sweep: &Linearreg_angleBatchRange,
729    k: Kernel,
730) -> Result<Linearreg_angleBatchOutput, Linearreg_angleError> {
731    let kernel = match k {
732        Kernel::Auto => detect_best_batch_kernel(),
733        other if other.is_batch() => other,
734        _ => return Err(Linearreg_angleError::InvalidKernelForBatch(k)),
735    };
736    let simd = match kernel {
737        Kernel::Avx512Batch => Kernel::Avx512,
738        Kernel::Avx2Batch => Kernel::Avx2,
739        Kernel::ScalarBatch => Kernel::Scalar,
740        _ => unreachable!(),
741    };
742    linearreg_angle_batch_par_slice(data, sweep, simd)
743}
744
745#[derive(Clone, Debug)]
746pub struct Linearreg_angleBatchOutput {
747    pub values: Vec<f64>,
748    pub combos: Vec<Linearreg_angleParams>,
749    pub rows: usize,
750    pub cols: usize,
751}
752
753impl Linearreg_angleBatchOutput {
754    pub fn row_for_params(&self, p: &Linearreg_angleParams) -> Option<usize> {
755        self.combos
756            .iter()
757            .position(|c| c.period.unwrap_or(14) == p.period.unwrap_or(14))
758    }
759    pub fn values_for(&self, p: &Linearreg_angleParams) -> Option<&[f64]> {
760        self.row_for_params(p).map(|row| {
761            let start = row * self.cols;
762            &self.values[start..start + self.cols]
763        })
764    }
765}
766
767#[inline(always)]
768fn expand_grid(r: &Linearreg_angleBatchRange) -> Vec<Linearreg_angleParams> {
769    fn axis_usize((start, end, step): (usize, usize, usize)) -> Vec<usize> {
770        if step == 0 || start == end {
771            return vec![start];
772        }
773        let mut vals = Vec::new();
774        if start < end {
775            let mut x = start;
776            while x <= end {
777                vals.push(x);
778                let next = x.saturating_add(step);
779                if next == x {
780                    break;
781                }
782                x = next;
783            }
784        } else {
785            let mut x = start;
786            loop {
787                vals.push(x);
788                if x <= end {
789                    break;
790                }
791                let next = x.saturating_sub(step);
792                if next >= x {
793                    break;
794                }
795                x = next;
796            }
797        }
798        vals
799    }
800    let periods = axis_usize(r.period);
801    let mut out = Vec::with_capacity(periods.len());
802    for &p in &periods {
803        out.push(Linearreg_angleParams { period: Some(p) });
804    }
805    out
806}
807
808#[inline(always)]
809pub fn linearreg_angle_batch_slice(
810    data: &[f64],
811    sweep: &Linearreg_angleBatchRange,
812    kern: Kernel,
813) -> Result<Linearreg_angleBatchOutput, Linearreg_angleError> {
814    linearreg_angle_batch_inner(data, sweep, kern, false)
815}
816
817#[inline(always)]
818pub fn linearreg_angle_batch_par_slice(
819    data: &[f64],
820    sweep: &Linearreg_angleBatchRange,
821    kern: Kernel,
822) -> Result<Linearreg_angleBatchOutput, Linearreg_angleError> {
823    linearreg_angle_batch_inner(data, sweep, kern, true)
824}
825
826#[inline(always)]
827fn linearreg_angle_batch_inner(
828    data: &[f64],
829    sweep: &Linearreg_angleBatchRange,
830    kern: Kernel,
831    parallel: bool,
832) -> Result<Linearreg_angleBatchOutput, Linearreg_angleError> {
833    let combos = expand_grid(sweep);
834    if combos.is_empty() {
835        return Err(Linearreg_angleError::InvalidRange {
836            start: sweep.period.0,
837            end: sweep.period.1,
838            step: sweep.period.2,
839        });
840    }
841
842    for combo in &combos {
843        let period = combo.period.unwrap();
844        if period < 2 {
845            return Err(Linearreg_angleError::InvalidPeriod {
846                period,
847                data_len: data.len(),
848            });
849        }
850    }
851    let first = data
852        .iter()
853        .position(|x| !x.is_nan())
854        .ok_or(Linearreg_angleError::AllValuesNaN)?;
855    let max_p = combos.iter().map(|c| c.period.unwrap()).max().unwrap();
856    let _ = combos
857        .len()
858        .checked_mul(max_p)
859        .ok_or(Linearreg_angleError::InvalidRange {
860            start: sweep.period.0,
861            end: sweep.period.1,
862            step: sweep.period.2,
863        })?;
864    if data.len() - first < max_p {
865        return Err(Linearreg_angleError::NotEnoughValidData {
866            needed: max_p,
867            valid: data.len() - first,
868        });
869    }
870    let rows = combos.len();
871    let cols = data.len();
872    let _ = rows
873        .checked_mul(cols)
874        .ok_or(Linearreg_angleError::InvalidRange {
875            start: sweep.period.0,
876            end: sweep.period.1,
877            step: sweep.period.2,
878        })?;
879
880    let mut buf_mu = make_uninit_matrix(rows, cols);
881
882    let warm: Vec<usize> = combos
883        .iter()
884        .map(|c| first + c.period.unwrap() - 1)
885        .collect();
886    init_matrix_prefixes(&mut buf_mu, cols, &warm);
887
888    let mut buf_guard = core::mem::ManuallyDrop::new(buf_mu);
889    let out: &mut [f64] = unsafe {
890        core::slice::from_raw_parts_mut(buf_guard.as_mut_ptr() as *mut f64, buf_guard.len())
891    };
892
893    let has_nan = data[first..].iter().any(|v| v.is_nan());
894
895    let (s_pref, k_pref): (Option<Vec<f64>>, Option<Vec<f64>>) = if !has_nan {
896        let n = data.len();
897        let mut s = vec![0.0f64; n + 1];
898        let mut k = vec![0.0f64; n + 1];
899        let mut acc_s = 0.0f64;
900        let mut acc_k = 0.0f64;
901        let mut idx = 0usize;
902        unsafe {
903            while idx + 3 < n {
904                let y0 = *data.get_unchecked(idx);
905                let y1 = *data.get_unchecked(idx + 1);
906                let y2 = *data.get_unchecked(idx + 2);
907                let y3 = *data.get_unchecked(idx + 3);
908
909                acc_s += y0;
910                s[idx + 1] = acc_s;
911                acc_k += (idx as f64) * y0;
912                k[idx + 1] = acc_k;
913                acc_s += y1;
914                s[idx + 2] = acc_s;
915                acc_k += ((idx + 1) as f64) * y1;
916                k[idx + 2] = acc_k;
917                acc_s += y2;
918                s[idx + 3] = acc_s;
919                acc_k += ((idx + 2) as f64) * y2;
920                k[idx + 3] = acc_k;
921                acc_s += y3;
922                s[idx + 4] = acc_s;
923                acc_k += ((idx + 3) as f64) * y3;
924                k[idx + 4] = acc_k;
925
926                idx += 4;
927            }
928            while idx < n {
929                let y = *data.get_unchecked(idx);
930                acc_s += y;
931                s[idx + 1] = acc_s;
932                acc_k += (idx as f64) * y;
933                k[idx + 1] = acc_k;
934                idx += 1;
935            }
936        }
937        (Some(s), Some(k))
938    } else {
939        (None, None)
940    };
941
942    let do_row = |row: usize, out_row: &mut [f64]| unsafe {
943        let period = combos[row].period.unwrap();
944        if has_nan {
945            linearreg_angle_row_scalar(data, first, period, out_row)
946        } else {
947            let p = period as f64;
948            let sum_x = (period * (period - 1)) as f64 * 0.5;
949            let sum_x_sqr = (period * (period - 1) * (2 * period - 1)) as f64 / 6.0;
950            let inv_div = 1.0 / (sum_x * sum_x - p * sum_x_sqr);
951            let rad2deg = 180.0 / PI;
952
953            match kern {
954                Kernel::Scalar => linearreg_angle_row_scalar_with_prefixes(
955                    data,
956                    first,
957                    out_row,
958                    s_pref.as_ref().unwrap(),
959                    k_pref.as_ref().unwrap(),
960                    p,
961                    sum_x,
962                    inv_div,
963                    rad2deg,
964                    period,
965                ),
966                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
967                Kernel::Avx2 => linearreg_angle_row_avx2_with_prefixes(
968                    data,
969                    first,
970                    out_row,
971                    s_pref.as_ref().unwrap(),
972                    k_pref.as_ref().unwrap(),
973                    p,
974                    sum_x,
975                    inv_div,
976                    rad2deg,
977                    period,
978                ),
979                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
980                Kernel::Avx512 => linearreg_angle_row_avx512_with_prefixes(
981                    data,
982                    first,
983                    out_row,
984                    s_pref.as_ref().unwrap(),
985                    k_pref.as_ref().unwrap(),
986                    p,
987                    sum_x,
988                    inv_div,
989                    rad2deg,
990                    period,
991                ),
992                _ => unreachable!(),
993            }
994        }
995    };
996    if parallel {
997        #[cfg(not(target_arch = "wasm32"))]
998        {
999            out.par_chunks_mut(cols)
1000                .enumerate()
1001                .for_each(|(row, slice)| do_row(row, slice));
1002        }
1003
1004        #[cfg(target_arch = "wasm32")]
1005        {
1006            for (row, slice) in out.chunks_mut(cols).enumerate() {
1007                do_row(row, slice);
1008            }
1009        }
1010    } else {
1011        for (row, slice) in out.chunks_mut(cols).enumerate() {
1012            do_row(row, slice);
1013        }
1014    }
1015
1016    let values = unsafe {
1017        Vec::from_raw_parts(
1018            buf_guard.as_mut_ptr() as *mut f64,
1019            buf_guard.len(),
1020            buf_guard.capacity(),
1021        )
1022    };
1023    core::mem::forget(buf_guard);
1024
1025    Ok(Linearreg_angleBatchOutput {
1026        values,
1027        combos,
1028        rows,
1029        cols,
1030    })
1031}
1032
1033#[inline(always)]
1034unsafe fn linearreg_angle_row_scalar(data: &[f64], first: usize, period: usize, out: &mut [f64]) {
1035    linearreg_angle_scalar(data, period, first, out)
1036}
1037
1038#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1039#[inline(always)]
1040unsafe fn linearreg_angle_row_avx2(data: &[f64], first: usize, period: usize, out: &mut [f64]) {
1041    linearreg_angle_row_scalar(data, first, period, out)
1042}
1043
1044#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1045#[inline(always)]
1046unsafe fn linearreg_angle_row_avx512(data: &[f64], first: usize, period: usize, out: &mut [f64]) {
1047    linearreg_angle_row_scalar(data, first, period, out)
1048}
1049
1050#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1051#[inline(always)]
1052unsafe fn linearreg_angle_row_avx512_short(
1053    data: &[f64],
1054    first: usize,
1055    period: usize,
1056    out: &mut [f64],
1057) {
1058    linearreg_angle_row_scalar(data, first, period, out)
1059}
1060
1061#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1062#[inline(always)]
1063unsafe fn linearreg_angle_row_avx512_long(
1064    data: &[f64],
1065    first: usize,
1066    period: usize,
1067    out: &mut [f64],
1068) {
1069    linearreg_angle_row_scalar(data, first, period, out)
1070}
1071
1072#[inline(always)]
1073unsafe fn linearreg_angle_row_scalar_with_prefixes(
1074    data: &[f64],
1075    first: usize,
1076    out: &mut [f64],
1077    s: &[f64],
1078    k: &[f64],
1079    p: f64,
1080    sum_x: f64,
1081    inv_div: f64,
1082    rad2deg: f64,
1083    period: usize,
1084) {
1085    let n = data.len();
1086    let start_i = first + period - 1;
1087    if start_i >= n {
1088        return;
1089    }
1090
1091    let mut i = start_i;
1092    while i < n {
1093        let sum_y = *s.get_unchecked(i + 1) - *s.get_unchecked(i + 1 - period);
1094        let sum_kd = *k.get_unchecked(i + 1) - *k.get_unchecked(i + 1 - period);
1095        let sum_xy = (i as f64) * sum_y - sum_kd;
1096        let num = p.mul_add(sum_xy, -sum_x * sum_y);
1097        let slope = num * inv_div;
1098        *out.get_unchecked_mut(i) = slope.atan() * rad2deg;
1099        i += 1;
1100    }
1101}
1102
1103#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1104#[inline(always)]
1105unsafe fn linearreg_angle_row_avx2_with_prefixes(
1106    data: &[f64],
1107    first: usize,
1108    out: &mut [f64],
1109    s: &[f64],
1110    k: &[f64],
1111    p: f64,
1112    sum_x: f64,
1113    inv_div: f64,
1114    rad2deg: f64,
1115    period: usize,
1116) {
1117    linearreg_angle_row_scalar_with_prefixes(
1118        data, first, out, s, k, p, sum_x, inv_div, rad2deg, period,
1119    )
1120}
1121
1122#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1123#[inline(always)]
1124unsafe fn linearreg_angle_row_avx512_with_prefixes(
1125    data: &[f64],
1126    first: usize,
1127    out: &mut [f64],
1128    s: &[f64],
1129    k: &[f64],
1130    p: f64,
1131    sum_x: f64,
1132    inv_div: f64,
1133    rad2deg: f64,
1134    period: usize,
1135) {
1136    linearreg_angle_row_scalar_with_prefixes(
1137        data, first, out, s, k, p, sum_x, inv_div, rad2deg, period,
1138    )
1139}
1140
1141#[cfg(test)]
1142mod tests {
1143    use super::*;
1144    use crate::skip_if_unsupported;
1145    use crate::utilities::data_loader::read_candles_from_csv;
1146    use crate::utilities::enums::Kernel;
1147
1148    fn check_lra_partial_params(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1149        skip_if_unsupported!(kernel, test_name);
1150        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1151        let candles = read_candles_from_csv(file_path)?;
1152
1153        let default_params = Linearreg_angleParams { period: None };
1154        let input = Linearreg_angleInput::from_candles(&candles, "close", default_params);
1155        let output = linearreg_angle_with_kernel(&input, kernel)?;
1156        assert_eq!(output.values.len(), candles.close.len());
1157
1158        Ok(())
1159    }
1160
1161    fn check_lra_accuracy(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1162        skip_if_unsupported!(kernel, test_name);
1163        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1164        let candles = read_candles_from_csv(file_path)?;
1165        let params = Linearreg_angleParams { period: Some(14) };
1166        let input = Linearreg_angleInput::from_candles(&candles, "close", params);
1167        let result = linearreg_angle_with_kernel(&input, kernel)?;
1168
1169        let expected_last_five = [
1170            -89.30491945492733,
1171            -89.28911257342405,
1172            -89.1088041965075,
1173            -86.58419429159467,
1174            -87.77085937059316,
1175        ];
1176        let start = result.values.len().saturating_sub(5);
1177        for (i, &val) in result.values[start..].iter().enumerate() {
1178            let diff = (val - expected_last_five[i]).abs();
1179            assert!(
1180                diff < 1e-5,
1181                "[{}] LRA {:?} mismatch at idx {}: got {}, expected {}",
1182                test_name,
1183                kernel,
1184                i,
1185                val,
1186                expected_last_five[i]
1187            );
1188        }
1189        Ok(())
1190    }
1191
1192    fn check_lra_zero_period(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1193        skip_if_unsupported!(kernel, test_name);
1194        let input_data = [10.0, 20.0, 30.0];
1195        let params = Linearreg_angleParams { period: Some(0) };
1196        let input = Linearreg_angleInput::from_slice(&input_data, params);
1197        let res = linearreg_angle_with_kernel(&input, kernel);
1198        assert!(
1199            res.is_err(),
1200            "[{}] LRA should fail with zero period",
1201            test_name
1202        );
1203        Ok(())
1204    }
1205
1206    fn check_lra_period_exceeds_length(
1207        test_name: &str,
1208        kernel: Kernel,
1209    ) -> Result<(), Box<dyn Error>> {
1210        skip_if_unsupported!(kernel, test_name);
1211        let data_small = [10.0, 20.0, 30.0];
1212        let params = Linearreg_angleParams { period: Some(10) };
1213        let input = Linearreg_angleInput::from_slice(&data_small, params);
1214        let res = linearreg_angle_with_kernel(&input, kernel);
1215        assert!(
1216            res.is_err(),
1217            "[{}] LRA should fail with period exceeding length",
1218            test_name
1219        );
1220        Ok(())
1221    }
1222
1223    fn check_lra_very_small_dataset(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1224        skip_if_unsupported!(kernel, test_name);
1225        let single_point = [42.0];
1226        let params = Linearreg_angleParams { period: Some(14) };
1227        let input = Linearreg_angleInput::from_slice(&single_point, params);
1228        let res = linearreg_angle_with_kernel(&input, kernel);
1229        assert!(
1230            res.is_err(),
1231            "[{}] LRA should fail with insufficient data",
1232            test_name
1233        );
1234        Ok(())
1235    }
1236
1237    fn check_lra_reinput(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1238        skip_if_unsupported!(kernel, test_name);
1239        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1240        let candles = read_candles_from_csv(file_path)?;
1241
1242        let first_params = Linearreg_angleParams { period: Some(14) };
1243        let first_input = Linearreg_angleInput::from_candles(&candles, "close", first_params);
1244        let first_result = linearreg_angle_with_kernel(&first_input, kernel)?;
1245
1246        let second_params = Linearreg_angleParams { period: Some(14) };
1247        let second_input = Linearreg_angleInput::from_slice(&first_result.values, second_params);
1248        let second_result = linearreg_angle_with_kernel(&second_input, kernel)?;
1249
1250        assert_eq!(second_result.values.len(), first_result.values.len());
1251        Ok(())
1252    }
1253
1254    #[cfg(debug_assertions)]
1255    fn check_lra_no_poison(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1256        skip_if_unsupported!(kernel, test_name);
1257
1258        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1259        let candles = read_candles_from_csv(file_path)?;
1260
1261        let test_params = vec![
1262            Linearreg_angleParams::default(),
1263            Linearreg_angleParams { period: Some(2) },
1264            Linearreg_angleParams { period: Some(3) },
1265            Linearreg_angleParams { period: Some(5) },
1266            Linearreg_angleParams { period: Some(7) },
1267            Linearreg_angleParams { period: Some(10) },
1268            Linearreg_angleParams { period: Some(14) },
1269            Linearreg_angleParams { period: Some(20) },
1270            Linearreg_angleParams { period: Some(30) },
1271            Linearreg_angleParams { period: Some(50) },
1272            Linearreg_angleParams { period: Some(100) },
1273            Linearreg_angleParams { period: Some(200) },
1274            Linearreg_angleParams { period: Some(500) },
1275        ];
1276
1277        for (param_idx, params) in test_params.iter().enumerate() {
1278            let input = Linearreg_angleInput::from_candles(&candles, "close", params.clone());
1279            let output = linearreg_angle_with_kernel(&input, kernel)?;
1280
1281            for (i, &val) in output.values.iter().enumerate() {
1282                if val.is_nan() {
1283                    continue;
1284                }
1285
1286                let bits = val.to_bits();
1287
1288                if bits == 0x11111111_11111111 {
1289                    panic!(
1290                        "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at index {} \
1291						 with params: period={} (param set {})",
1292                        test_name,
1293                        val,
1294                        bits,
1295                        i,
1296                        params.period.unwrap_or(14),
1297                        param_idx
1298                    );
1299                }
1300
1301                if bits == 0x22222222_22222222 {
1302                    panic!(
1303                        "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at index {} \
1304						 with params: period={} (param set {})",
1305                        test_name,
1306                        val,
1307                        bits,
1308                        i,
1309                        params.period.unwrap_or(14),
1310                        param_idx
1311                    );
1312                }
1313
1314                if bits == 0x33333333_33333333 {
1315                    panic!(
1316                        "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at index {} \
1317						 with params: period={} (param set {})",
1318                        test_name,
1319                        val,
1320                        bits,
1321                        i,
1322                        params.period.unwrap_or(14),
1323                        param_idx
1324                    );
1325                }
1326            }
1327        }
1328
1329        Ok(())
1330    }
1331
1332    #[cfg(not(debug_assertions))]
1333    fn check_lra_no_poison(_test_name: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
1334        Ok(())
1335    }
1336
1337    macro_rules! generate_all_lra_tests {
1338        ($($test_fn:ident),*) => {
1339            paste::paste! {
1340                $(
1341                    #[test]
1342                    fn [<$test_fn _scalar_f64>]() {
1343                        let _ = $test_fn(stringify!([<$test_fn _scalar_f64>]), Kernel::Scalar);
1344                    }
1345                )*
1346                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1347                $(
1348                    #[test]
1349                    fn [<$test_fn _avx2_f64>]() {
1350                        let _ = $test_fn(stringify!([<$test_fn _avx2_f64>]), Kernel::Avx2);
1351                    }
1352                    #[test]
1353                    fn [<$test_fn _avx512_f64>]() {
1354                        let _ = $test_fn(stringify!([<$test_fn _avx512_f64>]), Kernel::Avx512);
1355                    }
1356                )*
1357            }
1358        }
1359    }
1360
1361    #[test]
1362    fn test_linearreg_angle_into_matches_api() -> Result<(), Box<dyn Error>> {
1363        let len = 256usize;
1364        let mut data = Vec::with_capacity(len);
1365        for i in 0..len {
1366            let x = i as f64;
1367
1368            let v = (0.05 * x).sin() * 3.0 + 0.01 * x + (0.001 * x * x);
1369            data.push(v);
1370        }
1371
1372        data[0] = f64::NAN;
1373        data[1] = f64::NAN;
1374
1375        let params = Linearreg_angleParams::default();
1376        let input = Linearreg_angleInput::from_slice(&data, params);
1377
1378        let baseline = linearreg_angle(&input)?.values;
1379
1380        let mut into_out = vec![0.0f64; len];
1381        #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
1382        {
1383            linearreg_angle_into(&input, &mut into_out)?;
1384        }
1385        #[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1386        {
1387            linearreg_angle_into_slice(&mut into_out, &input, Kernel::Auto)?;
1388        }
1389
1390        assert_eq!(baseline.len(), into_out.len());
1391
1392        fn eq_or_both_nan(a: f64, b: f64) -> bool {
1393            (a.is_nan() && b.is_nan()) || (a == b)
1394        }
1395
1396        for i in 0..len {
1397            assert!(
1398                eq_or_both_nan(baseline[i], into_out[i]),
1399                "Mismatch at index {}: api={} into={}",
1400                i,
1401                baseline[i],
1402                into_out[i]
1403            );
1404        }
1405
1406        Ok(())
1407    }
1408    #[cfg(feature = "proptest")]
1409    #[allow(clippy::float_cmp)]
1410    fn check_linearreg_angle_property(
1411        test_name: &str,
1412        kernel: Kernel,
1413    ) -> Result<(), Box<dyn std::error::Error>> {
1414        use proptest::prelude::*;
1415        skip_if_unsupported!(kernel, test_name);
1416
1417        let strat = (2usize..=50).prop_flat_map(|period| {
1418            (
1419                (100f64..10000f64, 0.0001f64..0.1f64, period + 10..400)
1420                    .prop_flat_map(move |(base_price, volatility, data_len)| {
1421                        let price_changes = prop::collection::vec(
1422                            (-volatility..volatility).prop_filter("finite", |x| x.is_finite()),
1423                            data_len,
1424                        );
1425
1426                        let scenario = prop::strategy::Union::new(vec![
1427                            (0u8..60u8).boxed(),
1428                            (60u8..70u8).boxed(),
1429                            (70u8..80u8).boxed(),
1430                            (80u8..90u8).boxed(),
1431                            (90u8..95u8).boxed(),
1432                            (95u8..100u8).boxed(),
1433                        ]);
1434
1435                        (
1436                            Just(base_price),
1437                            Just(volatility),
1438                            Just(data_len),
1439                            price_changes,
1440                            scenario,
1441                        )
1442                    })
1443                    .prop_map(
1444                        move |(base_price, volatility, data_len, price_changes, scenario)| {
1445                            let mut data = Vec::with_capacity(data_len);
1446
1447                            match scenario {
1448                                0..=59 => {
1449                                    let mut current_price = base_price;
1450                                    for change in price_changes {
1451                                        current_price *= 1.0 + change;
1452                                        data.push(current_price);
1453                                    }
1454                                }
1455                                60..=69 => {
1456                                    data.resize(data_len, base_price);
1457                                }
1458                                70..=79 => {
1459                                    let slope = base_price * 0.001;
1460                                    for i in 0..data_len {
1461                                        data.push(base_price + slope * i as f64);
1462                                    }
1463                                }
1464                                80..=89 => {
1465                                    let slope = base_price * 0.001;
1466                                    for i in 0..data_len {
1467                                        data.push(base_price - slope * i as f64);
1468                                    }
1469                                }
1470                                90..=94 => {
1471                                    let slope = base_price * 0.00001;
1472                                    for i in 0..data_len {
1473                                        data.push(base_price + slope * i as f64);
1474                                    }
1475                                }
1476                                95..=99 => {
1477                                    let slope = base_price * 0.1;
1478                                    for i in 0..data_len {
1479                                        data.push(base_price + slope * i as f64);
1480                                    }
1481                                }
1482                                _ => unreachable!(),
1483                            }
1484                            data
1485                        },
1486                    ),
1487                Just(period),
1488            )
1489        });
1490
1491        proptest::test_runner::TestRunner::default().run(&strat, |(data, period)| {
1492            let params = Linearreg_angleParams {
1493                period: Some(period),
1494            };
1495            let input = Linearreg_angleInput::from_slice(&data, params);
1496
1497            let result = linearreg_angle_with_kernel(&input, kernel)?;
1498            let out = &result.values;
1499
1500            let ref_result = linearreg_angle_with_kernel(&input, Kernel::Scalar)?;
1501            let ref_out = &ref_result.values;
1502
1503            prop_assert_eq!(out.len(), data.len(), "Output length mismatch");
1504
1505            let warmup_end = period - 1;
1506            for i in 0..warmup_end {
1507                prop_assert!(
1508                    out[i].is_nan(),
1509                    "Expected NaN during warmup at index {}, got {}",
1510                    i,
1511                    out[i]
1512                );
1513            }
1514
1515            for (i, &val) in out.iter().enumerate().skip(warmup_end) {
1516                if !val.is_nan() {
1517                    prop_assert!(
1518                        val >= -90.0 && val <= 90.0,
1519                        "Angle out of bounds at index {}: {} degrees",
1520                        i,
1521                        val
1522                    );
1523                }
1524            }
1525
1526            if data.windows(2).all(|w| (w[0] - w[1]).abs() < 1e-10) {
1527                for (i, &val) in out.iter().enumerate().skip(warmup_end) {
1528                    if !val.is_nan() {
1529                        prop_assert!(
1530                            val.abs() < 1e-3,
1531                            "Expected ~0° for constant data at index {}, got {}°",
1532                            i,
1533                            val
1534                        );
1535                    }
1536                }
1537            }
1538
1539            let is_perfectly_linear = if data.len() >= 3 {
1540                let mut deltas = Vec::new();
1541                for i in 1..data.len() {
1542                    deltas.push(data[i] - data[i - 1]);
1543                }
1544
1545                deltas.windows(2).all(|w| (w[0] - w[1]).abs() < 1e-10)
1546            } else {
1547                false
1548            };
1549
1550            if is_perfectly_linear && data.len() > period {
1551                let valid_angles: Vec<f64> = out
1552                    .iter()
1553                    .skip(warmup_end)
1554                    .filter(|&&v| !v.is_nan())
1555                    .copied()
1556                    .collect();
1557
1558                if valid_angles.len() >= 2 {
1559                    let first_angle = valid_angles[0];
1560                    for (i, &angle) in valid_angles.iter().enumerate().skip(1) {
1561                        prop_assert!(
1562								(angle - first_angle).abs() < 1e-6,
1563								"Linear data should produce consistent angles. Index {} has {} vs first {}",
1564								i, angle, first_angle
1565							);
1566                    }
1567                }
1568            }
1569
1570            let is_linear_up = data.windows(2).all(|w| w[1] > w[0]);
1571            let is_linear_down = data.windows(2).all(|w| w[1] < w[0]);
1572
1573            if is_linear_up {
1574                for (i, &val) in out.iter().enumerate().skip(warmup_end) {
1575                    if !val.is_nan() {
1576                        prop_assert!(
1577                            val > 0.0,
1578                            "Expected positive angle for uptrend at index {}, got {}°",
1579                            i,
1580                            val
1581                        );
1582                    }
1583                }
1584            }
1585
1586            if is_linear_down {
1587                for (i, &val) in out.iter().enumerate().skip(warmup_end) {
1588                    if !val.is_nan() {
1589                        prop_assert!(
1590                            val < 0.0,
1591                            "Expected negative angle for downtrend at index {}, got {}°",
1592                            i,
1593                            val
1594                        );
1595                    }
1596                }
1597            }
1598
1599            if period <= 10 && data.len() >= period * 2 {
1600                let test_data: Vec<f64> = (0..period).map(|i| i as f64).collect();
1601                let test_params = Linearreg_angleParams {
1602                    period: Some(period),
1603                };
1604                let test_input = Linearreg_angleInput::from_slice(&test_data, test_params);
1605
1606                if let Ok(test_result) = linearreg_angle_with_kernel(&test_input, kernel) {
1607                    if test_result.values.len() >= period {
1608                        let test_angle = test_result.values[period - 1];
1609                        if !test_angle.is_nan() {
1610                            let expected_angle = 45.0;
1611                            prop_assert!(
1612                                (test_angle - expected_angle).abs() < 1.0,
1613                                "Mathematical test failed: expected ~45°, got {}°",
1614                                test_angle
1615                            );
1616                        }
1617                    }
1618                }
1619            }
1620
1621            let base_price = data[0];
1622            if data.windows(2).all(|w| {
1623                let delta = (w[1] - w[0]).abs();
1624                delta < base_price * 0.00001 && delta > 0.0
1625            }) {
1626                for &val in out.iter().skip(warmup_end) {
1627                    if !val.is_nan() {
1628                        prop_assert!(
1629                            val.abs() < 1.0,
1630                            "Near-horizontal data should produce small angle, got {}°",
1631                            val
1632                        );
1633                    }
1634                }
1635            }
1636
1637            for i in warmup_end..data.len() {
1638                let y = out[i];
1639                let r = ref_out[i];
1640
1641                if !y.is_finite() || !r.is_finite() {
1642                    prop_assert_eq!(
1643                        y.to_bits(),
1644                        r.to_bits(),
1645                        "NaN/infinity mismatch at index {}: {} vs {}",
1646                        i,
1647                        y,
1648                        r
1649                    );
1650                    continue;
1651                }
1652
1653                let y_bits = y.to_bits();
1654                let r_bits = r.to_bits();
1655                let ulp_diff = y_bits.abs_diff(r_bits);
1656
1657                prop_assert!(
1658                    (y - r).abs() <= 1e-9 || ulp_diff <= 5,
1659                    "Kernel mismatch at index {}: {} vs {} (ULP diff: {})",
1660                    i,
1661                    y,
1662                    r,
1663                    ulp_diff
1664                );
1665            }
1666
1667            Ok(())
1668        })?;
1669
1670        Ok(())
1671    }
1672    #[cfg(feature = "proptest")]
1673    generate_all_lra_tests!(check_linearreg_angle_property);
1674
1675    generate_all_lra_tests!(
1676        check_lra_partial_params,
1677        check_lra_accuracy,
1678        check_lra_zero_period,
1679        check_lra_period_exceeds_length,
1680        check_lra_very_small_dataset,
1681        check_lra_reinput,
1682        check_lra_no_poison
1683    );
1684    fn check_batch_default_row(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1685        skip_if_unsupported!(kernel, test);
1686
1687        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1688        let c = read_candles_from_csv(file)?;
1689
1690        let output = Linearreg_angleBatchBuilder::new()
1691            .kernel(kernel)
1692            .apply_candles(&c, "close")?;
1693
1694        let def = Linearreg_angleParams::default();
1695        let row = output.values_for(&def).expect("default row missing");
1696
1697        assert_eq!(row.len(), c.close.len());
1698
1699        let expected = [
1700            -89.30491945492733,
1701            -89.28911257342405,
1702            -89.1088041965075,
1703            -86.58419429159467,
1704            -87.77085937059316,
1705        ];
1706        let start = row.len() - 5;
1707        for (i, &v) in row[start..].iter().enumerate() {
1708            assert!(
1709                (v - expected[i]).abs() < 1e-5,
1710                "[{test}] default-row mismatch at idx {i}: {v} vs {expected:?}"
1711            );
1712        }
1713        Ok(())
1714    }
1715
1716    fn check_batch_grid_search(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1717        skip_if_unsupported!(kernel, test);
1718
1719        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1720        let c = read_candles_from_csv(file)?;
1721
1722        let batch = Linearreg_angleBatchBuilder::new()
1723            .kernel(kernel)
1724            .period_range(10, 16, 2)
1725            .apply_candles(&c, "close")?;
1726
1727        let periods = [10, 12, 14, 16];
1728        assert_eq!(batch.rows, 4);
1729
1730        for (ix, p) in periods.iter().enumerate() {
1731            let param = Linearreg_angleParams { period: Some(*p) };
1732            let row_idx = batch.row_for_params(&param);
1733            assert_eq!(row_idx, Some(ix), "Batch grid missing period {p}");
1734            let row = batch.values_for(&param).expect("Missing row for period");
1735            assert_eq!(row.len(), batch.cols, "Row len mismatch for period {p}");
1736        }
1737        Ok(())
1738    }
1739
1740    fn check_batch_period_static(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1741        skip_if_unsupported!(kernel, test);
1742
1743        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1744        let c = read_candles_from_csv(file)?;
1745
1746        let batch = Linearreg_angleBatchBuilder::new()
1747            .kernel(kernel)
1748            .period_static(14)
1749            .apply_candles(&c, "close")?;
1750
1751        assert_eq!(batch.rows, 1);
1752        let param = Linearreg_angleParams { period: Some(14) };
1753        let row = batch.values_for(&param).expect("Missing static row");
1754        assert_eq!(row.len(), batch.cols);
1755
1756        let last = *row.last().unwrap();
1757        let expected = -87.77085937059316;
1758        assert!(
1759            (last - expected).abs() < 1e-5,
1760            "Static period row last val mismatch: got {last}, want {expected}"
1761        );
1762
1763        Ok(())
1764    }
1765
1766    #[cfg(debug_assertions)]
1767    fn check_batch_no_poison(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1768        skip_if_unsupported!(kernel, test);
1769
1770        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1771        let c = read_candles_from_csv(file)?;
1772
1773        let test_configs = vec![
1774            (2, 10, 2),
1775            (5, 15, 1),
1776            (10, 50, 10),
1777            (20, 100, 20),
1778            (50, 200, 50),
1779            (14, 14, 0),
1780            (2, 5, 1),
1781            (100, 500, 100),
1782            (7, 21, 7),
1783            (30, 90, 30),
1784        ];
1785
1786        for (cfg_idx, &(p_start, p_end, p_step)) in test_configs.iter().enumerate() {
1787            let mut builder = Linearreg_angleBatchBuilder::new().kernel(kernel);
1788
1789            if p_step > 0 {
1790                builder = builder.period_range(p_start, p_end, p_step);
1791            } else {
1792                builder = builder.period_static(p_start);
1793            }
1794
1795            let output = builder.apply_candles(&c, "close")?;
1796
1797            for (idx, &val) in output.values.iter().enumerate() {
1798                if val.is_nan() {
1799                    continue;
1800                }
1801
1802                let bits = val.to_bits();
1803                let row = idx / output.cols;
1804                let col = idx % output.cols;
1805                let combo = &output.combos[row];
1806
1807                if bits == 0x11111111_11111111 {
1808                    panic!(
1809                        "[{}] Config {}: Found alloc_with_nan_prefix poison value {} (0x{:016X}) \
1810						 at row {} col {} (flat index {}) with params: period={}",
1811                        test,
1812                        cfg_idx,
1813                        val,
1814                        bits,
1815                        row,
1816                        col,
1817                        idx,
1818                        combo.period.unwrap_or(14)
1819                    );
1820                }
1821
1822                if bits == 0x22222222_22222222 {
1823                    panic!(
1824                        "[{}] Config {}: Found init_matrix_prefixes poison value {} (0x{:016X}) \
1825						 at row {} col {} (flat index {}) with params: period={}",
1826                        test,
1827                        cfg_idx,
1828                        val,
1829                        bits,
1830                        row,
1831                        col,
1832                        idx,
1833                        combo.period.unwrap_or(14)
1834                    );
1835                }
1836
1837                if bits == 0x33333333_33333333 {
1838                    panic!(
1839                        "[{}] Config {}: Found make_uninit_matrix poison value {} (0x{:016X}) \
1840						 at row {} col {} (flat index {}) with params: period={}",
1841                        test,
1842                        cfg_idx,
1843                        val,
1844                        bits,
1845                        row,
1846                        col,
1847                        idx,
1848                        combo.period.unwrap_or(14)
1849                    );
1850                }
1851            }
1852        }
1853
1854        Ok(())
1855    }
1856
1857    #[cfg(not(debug_assertions))]
1858    fn check_batch_no_poison(_test: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
1859        Ok(())
1860    }
1861
1862    macro_rules! gen_batch_tests {
1863        ($fn_name:ident) => {
1864            paste::paste! {
1865                #[test] fn [<$fn_name _scalar>]()      {
1866                    let _ = $fn_name(stringify!([<$fn_name _scalar>]), Kernel::ScalarBatch);
1867                }
1868                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1869                #[test] fn [<$fn_name _avx2>]()        {
1870                    let _ = $fn_name(stringify!([<$fn_name _avx2>]), Kernel::Avx2Batch);
1871                }
1872                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1873                #[test] fn [<$fn_name _avx512>]()      {
1874                    let _ = $fn_name(stringify!([<$fn_name _avx512>]), Kernel::Avx512Batch);
1875                }
1876                #[test] fn [<$fn_name _auto_detect>]() {
1877                    let _ = $fn_name(stringify!([<$fn_name _auto_detect>]), Kernel::Auto);
1878                }
1879            }
1880        };
1881    }
1882    gen_batch_tests!(check_batch_default_row);
1883    gen_batch_tests!(check_batch_grid_search);
1884    gen_batch_tests!(check_batch_period_static);
1885    gen_batch_tests!(check_batch_no_poison);
1886}
1887
1888pub fn linearreg_angle_into_slice(
1889    dst: &mut [f64],
1890    input: &Linearreg_angleInput,
1891    kern: Kernel,
1892) -> Result<(), Linearreg_angleError> {
1893    let data: &[f64] = input.as_ref();
1894    if data.is_empty() {
1895        return Err(Linearreg_angleError::EmptyInputData);
1896    }
1897
1898    let first = data
1899        .iter()
1900        .position(|x| !x.is_nan())
1901        .ok_or(Linearreg_angleError::AllValuesNaN)?;
1902    let len = data.len();
1903    let period = input.get_period();
1904
1905    if period < 2 || period > len {
1906        return Err(Linearreg_angleError::InvalidPeriod {
1907            period,
1908            data_len: len,
1909        });
1910    }
1911    if (len - first) < period {
1912        return Err(Linearreg_angleError::NotEnoughValidData {
1913            needed: period,
1914            valid: len - first,
1915        });
1916    }
1917
1918    if dst.len() != len {
1919        return Err(Linearreg_angleError::OutputLengthMismatch {
1920            expected: len,
1921            got: dst.len(),
1922        });
1923    }
1924
1925    let chosen = match kern {
1926        Kernel::Auto => Kernel::Scalar,
1927        other => other,
1928    };
1929
1930    let warmup_end = first + period - 1;
1931    for v in &mut dst[..warmup_end] {
1932        *v = f64::NAN;
1933    }
1934
1935    unsafe {
1936        match chosen {
1937            Kernel::Scalar | Kernel::ScalarBatch => {
1938                linearreg_angle_scalar(data, period, first, dst)
1939            }
1940            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1941            Kernel::Avx2 | Kernel::Avx2Batch => linearreg_angle_avx2(data, period, first, dst),
1942            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1943            Kernel::Avx512 | Kernel::Avx512Batch => {
1944                linearreg_angle_avx512(data, period, first, dst)
1945            }
1946            _ => unreachable!(),
1947        }
1948    }
1949
1950    Ok(())
1951}
1952
1953#[cfg(feature = "python")]
1954#[pyfunction(name = "linearreg_angle")]
1955#[pyo3(signature = (data, period, kernel=None))]
1956pub fn linearreg_angle_py<'py>(
1957    py: Python<'py>,
1958    data: numpy::PyReadonlyArray1<'py, f64>,
1959    period: usize,
1960    kernel: Option<&str>,
1961) -> PyResult<Bound<'py, numpy::PyArray1<f64>>> {
1962    use numpy::{IntoPyArray, PyArrayMethods};
1963
1964    let slice_in = data.as_slice()?;
1965    let kern = validate_kernel(kernel, false)?;
1966    let params = Linearreg_angleParams {
1967        period: Some(period),
1968    };
1969    let linearreg_angle_in = Linearreg_angleInput::from_slice(slice_in, params);
1970
1971    let result_vec: Vec<f64> = py
1972        .allow_threads(|| linearreg_angle_with_kernel(&linearreg_angle_in, kern).map(|o| o.values))
1973        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1974
1975    Ok(result_vec.into_pyarray(py))
1976}
1977
1978#[cfg(feature = "python")]
1979#[pyclass(name = "Linearreg_angleStream")]
1980pub struct Linearreg_angleStreamPy {
1981    stream: Linearreg_angleStream,
1982}
1983
1984#[cfg(feature = "python")]
1985#[pymethods]
1986impl Linearreg_angleStreamPy {
1987    #[new]
1988    fn new(period: usize) -> PyResult<Self> {
1989        let params = Linearreg_angleParams {
1990            period: Some(period),
1991        };
1992        let stream = Linearreg_angleStream::try_new(params)
1993            .map_err(|e| PyValueError::new_err(e.to_string()))?;
1994        Ok(Linearreg_angleStreamPy { stream })
1995    }
1996
1997    fn update(&mut self, value: f64) -> Option<f64> {
1998        self.stream.update(value)
1999    }
2000}
2001
2002#[cfg(feature = "python")]
2003#[pyfunction(name = "linearreg_angle_batch")]
2004#[pyo3(signature = (data, period_range, kernel=None))]
2005pub fn linearreg_angle_batch_py<'py>(
2006    py: Python<'py>,
2007    data: numpy::PyReadonlyArray1<'py, f64>,
2008    period_range: (usize, usize, usize),
2009    kernel: Option<&str>,
2010) -> PyResult<Bound<'py, pyo3::types::PyDict>> {
2011    use numpy::{IntoPyArray, PyArray1, PyArrayMethods};
2012    use pyo3::types::PyDict;
2013    use std::mem::MaybeUninit;
2014
2015    let slice_in = data.as_slice()?;
2016
2017    let sweep = Linearreg_angleBatchRange {
2018        period: period_range,
2019    };
2020    let combos = expand_grid(&sweep);
2021    if combos.is_empty() {
2022        return Err(PyValueError::new_err("linearreg_angle_batch: empty grid"));
2023    }
2024    let rows = combos.len();
2025    let cols = slice_in.len();
2026
2027    for combo in &combos {
2028        let period = combo.period.unwrap();
2029        if period < 2 {
2030            return Err(PyValueError::new_err(
2031                Linearreg_angleError::InvalidPeriod {
2032                    period,
2033                    data_len: cols,
2034                }
2035                .to_string(),
2036            ));
2037        }
2038    }
2039
2040    let total = rows.checked_mul(cols).ok_or_else(|| {
2041        PyValueError::new_err(
2042            Linearreg_angleError::InvalidRange {
2043                start: sweep.period.0,
2044                end: sweep.period.1,
2045                step: sweep.period.2,
2046            }
2047            .to_string(),
2048        )
2049    })?;
2050
2051    let out_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
2052    let slice_out = unsafe { out_arr.as_slice_mut()? };
2053
2054    let first = slice_in
2055        .iter()
2056        .position(|x| !x.is_nan())
2057        .ok_or_else(|| PyValueError::new_err("AllValuesNaN"))?;
2058    let warm: Vec<usize> = combos
2059        .iter()
2060        .map(|c| first + c.period.unwrap() - 1)
2061        .collect();
2062
2063    let mu: &mut [MaybeUninit<f64>] = unsafe {
2064        core::slice::from_raw_parts_mut(
2065            slice_out.as_mut_ptr() as *mut MaybeUninit<f64>,
2066            slice_out.len(),
2067        )
2068    };
2069    init_matrix_prefixes(mu, cols, &warm);
2070
2071    let kern = validate_kernel(kernel, true)?;
2072    let resolved = match kern {
2073        Kernel::Auto => detect_best_batch_kernel(),
2074        k => k,
2075    };
2076    let simd = match resolved {
2077        Kernel::Avx512Batch => Kernel::Avx512,
2078        Kernel::Avx2Batch => Kernel::Avx2,
2079        Kernel::ScalarBatch => Kernel::Scalar,
2080        _ => unreachable!(),
2081    };
2082
2083    py.allow_threads(|| linearreg_angle_batch_inner_into(slice_in, &sweep, simd, true, slice_out))
2084        .map_err(|e| PyValueError::new_err(e.to_string()))?;
2085
2086    let dict = PyDict::new(py);
2087    dict.set_item("values", out_arr.reshape((rows, cols))?)?;
2088    dict.set_item(
2089        "periods",
2090        combos
2091            .iter()
2092            .map(|p| p.period.unwrap() as u64)
2093            .collect::<Vec<_>>()
2094            .into_pyarray(py),
2095    )?;
2096    Ok(dict)
2097}
2098
2099#[cfg(all(feature = "python", feature = "cuda"))]
2100#[pyclass(module = "ta_indicators.cuda", unsendable)]
2101pub struct LinearregAngleDeviceArrayF32Py {
2102    pub(crate) buf: Option<DeviceBuffer<f32>>,
2103    pub(crate) rows: usize,
2104    pub(crate) cols: usize,
2105    pub(crate) _ctx: Arc<Context>,
2106    pub(crate) device_id: u32,
2107}
2108
2109#[cfg(all(feature = "python", feature = "cuda"))]
2110#[pymethods]
2111impl LinearregAngleDeviceArrayF32Py {
2112    #[getter]
2113    fn __cuda_array_interface__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
2114        let d = PyDict::new(py);
2115        d.set_item("shape", (self.rows, self.cols))?;
2116        d.set_item("typestr", "<f4")?;
2117        d.set_item(
2118            "strides",
2119            (
2120                self.cols * std::mem::size_of::<f32>(),
2121                std::mem::size_of::<f32>(),
2122            ),
2123        )?;
2124        let ptr = self
2125            .buf
2126            .as_ref()
2127            .ok_or_else(|| PyValueError::new_err("buffer already exported via __dlpack__"))?
2128            .as_device_ptr()
2129            .as_raw() as usize;
2130        d.set_item("data", (ptr, false))?;
2131
2132        d.set_item("version", 3)?;
2133        Ok(d)
2134    }
2135
2136    fn __dlpack_device__(&self) -> (i32, i32) {
2137        (2, self.device_id as i32)
2138    }
2139
2140    #[pyo3(signature = (stream=None, max_version=None, dl_device=None, copy=None))]
2141    fn __dlpack__<'py>(
2142        &mut self,
2143        py: Python<'py>,
2144        stream: Option<PyObject>,
2145        max_version: Option<PyObject>,
2146        dl_device: Option<PyObject>,
2147        copy: Option<PyObject>,
2148    ) -> PyResult<PyObject> {
2149        let (kdl, alloc_dev) = self.__dlpack_device__();
2150        if let Some(dev_obj) = dl_device.as_ref() {
2151            if let Ok((dev_ty, dev_id)) = dev_obj.extract::<(i32, i32)>(py) {
2152                if dev_ty != kdl || dev_id != alloc_dev {
2153                    let wants_copy = copy
2154                        .as_ref()
2155                        .and_then(|c| c.extract::<bool>(py).ok())
2156                        .unwrap_or(false);
2157                    if wants_copy {
2158                        return Err(PyValueError::new_err(
2159                            "device copy not implemented for __dlpack__",
2160                        ));
2161                    } else {
2162                        return Err(PyValueError::new_err("dl_device mismatch for __dlpack__"));
2163                    }
2164                }
2165            }
2166        }
2167        let _ = stream;
2168
2169        let buf = self
2170            .buf
2171            .take()
2172            .ok_or_else(|| PyValueError::new_err("__dlpack__ may only be called once"))?;
2173
2174        let rows = self.rows;
2175        let cols = self.cols;
2176
2177        let max_version_bound = max_version.map(|obj| obj.into_bound(py));
2178
2179        export_f32_cuda_dlpack_2d(py, buf, rows, cols, alloc_dev, max_version_bound)
2180    }
2181}
2182
2183#[cfg(all(feature = "python", feature = "cuda"))]
2184#[pyfunction(name = "linearreg_angle_cuda_batch_dev")]
2185#[pyo3(signature = (data_f32, period_range, device_id=0))]
2186pub fn linearreg_angle_cuda_batch_dev_py<'py>(
2187    py: Python<'py>,
2188    data_f32: numpy::PyReadonlyArray1<'py, f32>,
2189    period_range: (usize, usize, usize),
2190    device_id: usize,
2191) -> PyResult<LinearregAngleDeviceArrayF32Py> {
2192    if !cuda_available() {
2193        return Err(PyValueError::new_err("CUDA not available"));
2194    }
2195    let slice_in = data_f32.as_slice()?;
2196    let sweep = Linearreg_angleBatchRange {
2197        period: period_range,
2198    };
2199    let (buf, rows, cols, ctx, dev_id) = py.allow_threads(|| {
2200        let cuda =
2201            CudaLinearregAngle::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
2202        let out = cuda
2203            .linearreg_angle_batch_dev(slice_in, &sweep)
2204            .map_err(|e| PyValueError::new_err(e.to_string()))?;
2205        let crate::cuda::moving_averages::DeviceArrayF32 { buf, rows, cols } = out;
2206        let ctx = cuda.context_arc();
2207        Ok::<_, pyo3::PyErr>((buf, rows, cols, ctx, cuda.device_id()))
2208    })?;
2209    Ok(LinearregAngleDeviceArrayF32Py {
2210        buf: Some(buf),
2211        rows,
2212        cols,
2213        _ctx: ctx,
2214        device_id: dev_id,
2215    })
2216}
2217
2218#[cfg(all(feature = "python", feature = "cuda"))]
2219#[pyfunction(name = "linearreg_angle_cuda_many_series_one_param_dev")]
2220#[pyo3(signature = (data_tm_f32, cols, rows, period, device_id=0))]
2221pub fn linearreg_angle_cuda_many_series_one_param_dev_py<'py>(
2222    py: Python<'py>,
2223    data_tm_f32: numpy::PyReadonlyArray1<'py, f32>,
2224    cols: usize,
2225    rows: usize,
2226    period: usize,
2227    device_id: usize,
2228) -> PyResult<LinearregAngleDeviceArrayF32Py> {
2229    if !cuda_available() {
2230        return Err(PyValueError::new_err("CUDA not available"));
2231    }
2232    let params = Linearreg_angleParams {
2233        period: Some(period),
2234    };
2235    let slice_in = data_tm_f32.as_slice()?;
2236    let (buf, r_out, c_out, ctx, dev_id) = py.allow_threads(|| {
2237        let cuda =
2238            CudaLinearregAngle::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
2239        let out = cuda
2240            .linearreg_angle_many_series_one_param_time_major_dev(slice_in, cols, rows, &params)
2241            .map_err(|e| PyValueError::new_err(e.to_string()))?;
2242        let crate::cuda::moving_averages::DeviceArrayF32 { buf, rows, cols } = out;
2243        let ctx = cuda.context_arc();
2244        Ok::<_, pyo3::PyErr>((buf, rows, cols, ctx, cuda.device_id()))
2245    })?;
2246    Ok(LinearregAngleDeviceArrayF32Py {
2247        buf: Some(buf),
2248        rows: r_out,
2249        cols: c_out,
2250        _ctx: ctx,
2251        device_id: dev_id,
2252    })
2253}
2254
2255#[cfg(feature = "python")]
2256pub fn register_linearreg_angle_module(m: &Bound<'_, pyo3::types::PyModule>) -> PyResult<()> {
2257    m.add_function(wrap_pyfunction!(linearreg_angle_py, m)?)?;
2258    m.add_function(wrap_pyfunction!(linearreg_angle_batch_py, m)?)?;
2259    #[cfg(feature = "cuda")]
2260    {
2261        m.add_function(wrap_pyfunction!(linearreg_angle_cuda_batch_dev_py, m)?)?;
2262        m.add_function(wrap_pyfunction!(
2263            linearreg_angle_cuda_many_series_one_param_dev_py,
2264            m
2265        )?)?;
2266        m.add_class::<LinearregAngleDeviceArrayF32Py>()?;
2267        m.add_class::<crate::indicators::moving_averages::alma::DeviceArrayF32Py>()?;
2268    }
2269    Ok(())
2270}
2271
2272#[inline(always)]
2273fn linearreg_angle_batch_inner_into(
2274    data: &[f64],
2275    sweep: &Linearreg_angleBatchRange,
2276    kern: Kernel,
2277    parallel: bool,
2278    out: &mut [f64],
2279) -> Result<Vec<Linearreg_angleParams>, Linearreg_angleError> {
2280    let combos = expand_grid(sweep);
2281    if combos.is_empty() {
2282        return Err(Linearreg_angleError::InvalidRange {
2283            start: sweep.period.0,
2284            end: sweep.period.1,
2285            step: sweep.period.2,
2286        });
2287    }
2288
2289    for combo in &combos {
2290        let period = combo.period.unwrap();
2291        if period < 2 {
2292            return Err(Linearreg_angleError::InvalidPeriod {
2293                period,
2294                data_len: data.len(),
2295            });
2296        }
2297    }
2298
2299    let first = data
2300        .iter()
2301        .position(|x| !x.is_nan())
2302        .ok_or(Linearreg_angleError::AllValuesNaN)?;
2303    let max_p = combos.iter().map(|c| c.period.unwrap()).max().unwrap();
2304    let _ = combos
2305        .len()
2306        .checked_mul(max_p)
2307        .ok_or(Linearreg_angleError::InvalidRange {
2308            start: sweep.period.0,
2309            end: sweep.period.1,
2310            step: sweep.period.2,
2311        })?;
2312    if data.len() - first < max_p {
2313        return Err(Linearreg_angleError::NotEnoughValidData {
2314            needed: max_p,
2315            valid: data.len() - first,
2316        });
2317    }
2318
2319    let cols = data.len();
2320    let expected = combos
2321        .len()
2322        .checked_mul(cols)
2323        .ok_or(Linearreg_angleError::InvalidRange {
2324            start: sweep.period.0,
2325            end: sweep.period.1,
2326            step: sweep.period.2,
2327        })?;
2328    if out.len() != expected {
2329        return Err(Linearreg_angleError::OutputLengthMismatch {
2330            expected,
2331            got: out.len(),
2332        });
2333    }
2334
2335    let out_uninit: &mut [core::mem::MaybeUninit<f64>] = unsafe {
2336        core::slice::from_raw_parts_mut(
2337            out.as_mut_ptr() as *mut core::mem::MaybeUninit<f64>,
2338            out.len(),
2339        )
2340    };
2341
2342    let do_row = |row: usize, dst_mu: &mut [core::mem::MaybeUninit<f64>]| unsafe {
2343        let period = combos[row].period.unwrap();
2344
2345        let dst = core::slice::from_raw_parts_mut(dst_mu.as_mut_ptr() as *mut f64, dst_mu.len());
2346        match kern {
2347            Kernel::Scalar | Kernel::ScalarBatch => {
2348                linearreg_angle_row_scalar(data, first, period, dst)
2349            }
2350            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2351            Kernel::Avx2 | Kernel::Avx2Batch => linearreg_angle_row_avx2(data, first, period, dst),
2352            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2353            Kernel::Avx512 | Kernel::Avx512Batch => {
2354                linearreg_angle_row_avx512(data, first, period, dst)
2355            }
2356            #[cfg(not(all(feature = "nightly-avx", target_arch = "x86_64")))]
2357            Kernel::Avx2 | Kernel::Avx2Batch | Kernel::Avx512 | Kernel::Avx512Batch => {
2358                linearreg_angle_row_scalar(data, first, period, dst)
2359            }
2360            Kernel::Auto => unreachable!("resolve kernel before calling inner_into"),
2361        }
2362    };
2363
2364    if parallel {
2365        #[cfg(not(target_arch = "wasm32"))]
2366        {
2367            out_uninit
2368                .par_chunks_mut(cols)
2369                .enumerate()
2370                .for_each(|(row, slice)| do_row(row, slice));
2371        }
2372        #[cfg(target_arch = "wasm32")]
2373        {
2374            for (row, slice) in out_uninit.chunks_mut(cols).enumerate() {
2375                do_row(row, slice);
2376            }
2377        }
2378    } else {
2379        for (row, slice) in out_uninit.chunks_mut(cols).enumerate() {
2380            do_row(row, slice);
2381        }
2382    }
2383
2384    Ok(combos)
2385}
2386
2387#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2388#[wasm_bindgen]
2389pub fn linearreg_angle_js(data: &[f64], period: usize) -> Result<Vec<f64>, JsValue> {
2390    let params = Linearreg_angleParams {
2391        period: Some(period),
2392    };
2393    let input = Linearreg_angleInput::from_slice(data, params);
2394
2395    let mut output = vec![0.0; data.len()];
2396    linearreg_angle_into_slice(&mut output, &input, Kernel::Auto)
2397        .map_err(|e| JsValue::from_str(&e.to_string()))?;
2398
2399    Ok(output)
2400}
2401
2402#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2403#[wasm_bindgen]
2404pub fn linearreg_angle_alloc(len: usize) -> *mut f64 {
2405    let mut vec = Vec::<f64>::with_capacity(len);
2406    let ptr = vec.as_mut_ptr();
2407    std::mem::forget(vec);
2408    ptr
2409}
2410
2411#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2412#[wasm_bindgen]
2413pub fn linearreg_angle_free(ptr: *mut f64, len: usize) {
2414    if !ptr.is_null() {
2415        unsafe {
2416            let _ = Vec::from_raw_parts(ptr, len, len);
2417        }
2418    }
2419}
2420
2421#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2422#[wasm_bindgen]
2423pub fn linearreg_angle_into(
2424    in_ptr: *const f64,
2425    out_ptr: *mut f64,
2426    len: usize,
2427    period: usize,
2428) -> Result<(), JsValue> {
2429    if in_ptr.is_null() || out_ptr.is_null() {
2430        return Err(JsValue::from_str("Null pointer provided"));
2431    }
2432
2433    unsafe {
2434        let data = std::slice::from_raw_parts(in_ptr, len);
2435
2436        if period < 2 || period > len {
2437            return Err(JsValue::from_str("Invalid period"));
2438        }
2439
2440        let params = Linearreg_angleParams {
2441            period: Some(period),
2442        };
2443        let input = Linearreg_angleInput::from_slice(data, params);
2444
2445        if in_ptr == out_ptr {
2446            let mut temp = vec![0.0; len];
2447            linearreg_angle_into_slice(&mut temp, &input, Kernel::Auto)
2448                .map_err(|e| JsValue::from_str(&e.to_string()))?;
2449            let out = std::slice::from_raw_parts_mut(out_ptr, len);
2450            out.copy_from_slice(&temp);
2451        } else {
2452            let out = std::slice::from_raw_parts_mut(out_ptr, len);
2453            linearreg_angle_into_slice(out, &input, Kernel::Auto)
2454                .map_err(|e| JsValue::from_str(&e.to_string()))?;
2455        }
2456
2457        Ok(())
2458    }
2459}
2460
2461#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2462#[derive(Serialize, Deserialize)]
2463pub struct Linearreg_angleBatchConfig {
2464    pub period_range: (usize, usize, usize),
2465}
2466
2467#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2468#[derive(Serialize, Deserialize)]
2469pub struct Linearreg_angleBatchJsOutput {
2470    pub values: Vec<f64>,
2471    pub combos: Vec<Linearreg_angleParams>,
2472    pub rows: usize,
2473    pub cols: usize,
2474}
2475
2476#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2477#[wasm_bindgen(js_name = linearreg_angle_batch)]
2478pub fn linearreg_angle_batch_js(data: &[f64], config: JsValue) -> Result<JsValue, JsValue> {
2479    let config: Linearreg_angleBatchConfig = serde_wasm_bindgen::from_value(config)
2480        .map_err(|e| JsValue::from_str(&format!("Invalid config: {}", e)))?;
2481
2482    let sweep = Linearreg_angleBatchRange {
2483        period: config.period_range,
2484    };
2485
2486    let kernel = detect_best_batch_kernel();
2487    let simd = match kernel {
2488        Kernel::Avx512Batch => Kernel::Avx512,
2489        Kernel::Avx2Batch => Kernel::Avx2,
2490        Kernel::ScalarBatch => Kernel::Scalar,
2491        _ => Kernel::Scalar,
2492    };
2493
2494    let output = linearreg_angle_batch_inner(data, &sweep, simd, false)
2495        .map_err(|e| JsValue::from_str(&e.to_string()))?;
2496
2497    let js_output = Linearreg_angleBatchJsOutput {
2498        values: output.values,
2499        combos: output.combos,
2500        rows: output.rows,
2501        cols: output.cols,
2502    };
2503
2504    serde_wasm_bindgen::to_value(&js_output)
2505        .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
2506}