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(¶m);
1733 assert_eq!(row_idx, Some(ix), "Batch grid missing period {p}");
1734 let row = batch.values_for(¶m).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(¶m).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, ¶ms)
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}