1#[cfg(all(feature = "python", feature = "cuda"))]
2use numpy::PyUntypedArrayMethods;
3#[cfg(feature = "python")]
4use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
5#[cfg(feature = "python")]
6use pyo3::exceptions::PyValueError;
7#[cfg(feature = "python")]
8use pyo3::prelude::*;
9#[cfg(feature = "python")]
10use pyo3::types::{PyDict, PyList};
11
12#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
13use serde::{Deserialize, Serialize};
14#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
15use wasm_bindgen::prelude::*;
16
17use crate::utilities::data_loader::{source_type, Candles};
18use crate::utilities::enums::Kernel;
19use crate::utilities::helpers::{
20 alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
21 make_uninit_matrix,
22};
23#[cfg(feature = "python")]
24use crate::utilities::kernel_validation::validate_kernel;
25
26#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
27use core::arch::x86_64::*;
28
29#[cfg(not(target_arch = "wasm32"))]
30use rayon::prelude::*;
31
32use std::convert::AsRef;
33use std::error::Error;
34use std::f64::consts::PI;
35use std::mem::MaybeUninit;
36use thiserror::Error;
37
38#[derive(Debug, Clone)]
39pub enum LpcData<'a> {
40 Candles {
41 candles: &'a Candles,
42 source: &'a str,
43 },
44 Slices {
45 high: &'a [f64],
46 low: &'a [f64],
47 close: &'a [f64],
48 src: &'a [f64],
49 },
50}
51
52#[derive(Debug, Clone)]
53pub struct LpcOutput {
54 pub filter: Vec<f64>,
55 pub high_band: Vec<f64>,
56 pub low_band: Vec<f64>,
57}
58
59#[derive(Debug, Clone)]
60#[cfg_attr(
61 all(target_arch = "wasm32", feature = "wasm"),
62 derive(Serialize, Deserialize)
63)]
64pub struct LpcParams {
65 pub cutoff_type: Option<String>,
66 pub fixed_period: Option<usize>,
67 pub max_cycle_limit: Option<usize>,
68 pub cycle_mult: Option<f64>,
69 pub tr_mult: Option<f64>,
70}
71
72impl Default for LpcParams {
73 fn default() -> Self {
74 Self {
75 cutoff_type: Some("adaptive".to_string()),
76 fixed_period: Some(20),
77 max_cycle_limit: Some(60),
78 cycle_mult: Some(1.0),
79 tr_mult: Some(1.0),
80 }
81 }
82}
83
84#[derive(Debug, Clone)]
85pub struct LpcInput<'a> {
86 pub data: LpcData<'a>,
87 pub params: LpcParams,
88}
89
90impl<'a> AsRef<[f64]> for LpcInput<'a> {
91 fn as_ref(&self) -> &[f64] {
92 match &self.data {
93 LpcData::Candles { candles, source } => source_type(candles, source),
94 LpcData::Slices { src, .. } => src,
95 }
96 }
97}
98
99impl<'a> LpcInput<'a> {
100 #[inline]
101 pub fn from_candles(c: &'a Candles, s: &'a str, p: LpcParams) -> Self {
102 Self {
103 data: LpcData::Candles {
104 candles: c,
105 source: s,
106 },
107 params: p,
108 }
109 }
110
111 #[inline]
112 pub fn from_slices(
113 high: &'a [f64],
114 low: &'a [f64],
115 close: &'a [f64],
116 src: &'a [f64],
117 p: LpcParams,
118 ) -> Self {
119 Self {
120 data: LpcData::Slices {
121 high,
122 low,
123 close,
124 src,
125 },
126 params: p,
127 }
128 }
129
130 #[inline]
131 pub fn with_default_candles(c: &'a Candles) -> Self {
132 Self::from_candles(c, "close", LpcParams::default())
133 }
134
135 #[inline]
136 pub fn get_cutoff_type(&self) -> String {
137 self.params
138 .cutoff_type
139 .clone()
140 .unwrap_or_else(|| "adaptive".to_string())
141 }
142
143 #[inline]
144 pub fn get_fixed_period(&self) -> usize {
145 self.params.fixed_period.unwrap_or(20)
146 }
147
148 #[inline]
149 pub fn get_max_cycle_limit(&self) -> usize {
150 self.params.max_cycle_limit.unwrap_or(60)
151 }
152
153 #[inline]
154 pub fn get_cycle_mult(&self) -> f64 {
155 self.params.cycle_mult.unwrap_or(1.0)
156 }
157
158 #[inline]
159 pub fn get_tr_mult(&self) -> f64 {
160 self.params.tr_mult.unwrap_or(1.0)
161 }
162}
163
164#[derive(Clone, Debug)]
165pub struct LpcBuilder {
166 cutoff_type: Option<String>,
167 fixed_period: Option<usize>,
168 max_cycle_limit: Option<usize>,
169 cycle_mult: Option<f64>,
170 tr_mult: Option<f64>,
171 kernel: Kernel,
172}
173
174impl Default for LpcBuilder {
175 fn default() -> Self {
176 Self {
177 cutoff_type: None,
178 fixed_period: None,
179 max_cycle_limit: None,
180 cycle_mult: None,
181 tr_mult: None,
182 kernel: Kernel::Auto,
183 }
184 }
185}
186
187impl LpcBuilder {
188 #[inline(always)]
189 pub fn new() -> Self {
190 Self::default()
191 }
192
193 #[inline(always)]
194 pub fn cutoff_type(mut self, val: String) -> Self {
195 self.cutoff_type = Some(val);
196 self
197 }
198
199 #[inline(always)]
200 pub fn fixed_period(mut self, val: usize) -> Self {
201 self.fixed_period = Some(val);
202 self
203 }
204
205 #[inline(always)]
206 pub fn max_cycle_limit(mut self, val: usize) -> Self {
207 self.max_cycle_limit = Some(val);
208 self
209 }
210
211 #[inline(always)]
212 pub fn cycle_mult(mut self, val: f64) -> Self {
213 self.cycle_mult = Some(val);
214 self
215 }
216
217 #[inline(always)]
218 pub fn tr_mult(mut self, val: f64) -> Self {
219 self.tr_mult = Some(val);
220 self
221 }
222
223 #[inline(always)]
224 pub fn kernel(mut self, k: Kernel) -> Self {
225 self.kernel = k;
226 self
227 }
228
229 #[inline(always)]
230 pub fn apply(self, c: &Candles) -> Result<LpcOutput, LpcError> {
231 self.apply_candles(c, "close")
232 }
233
234 #[inline(always)]
235 pub fn apply_candles(self, c: &Candles, s: &str) -> Result<LpcOutput, LpcError> {
236 let p = LpcParams {
237 cutoff_type: self.cutoff_type,
238 fixed_period: self.fixed_period,
239 max_cycle_limit: self.max_cycle_limit,
240 cycle_mult: self.cycle_mult,
241 tr_mult: self.tr_mult,
242 };
243 let i = LpcInput::from_candles(c, s, p);
244 lpc_with_kernel(&i, self.kernel)
245 }
246
247 #[inline(always)]
248 pub fn apply_slices(
249 self,
250 high: &[f64],
251 low: &[f64],
252 close: &[f64],
253 src: &[f64],
254 ) -> Result<LpcOutput, LpcError> {
255 let p = LpcParams {
256 cutoff_type: self.cutoff_type,
257 fixed_period: self.fixed_period,
258 max_cycle_limit: self.max_cycle_limit,
259 cycle_mult: self.cycle_mult,
260 tr_mult: self.tr_mult,
261 };
262 let i = LpcInput::from_slices(high, low, close, src, p);
263 lpc_with_kernel(&i, self.kernel)
264 }
265
266 #[inline(always)]
267 pub fn apply_slice(
268 self,
269 high: &[f64],
270 low: &[f64],
271 close: &[f64],
272 src: &[f64],
273 ) -> Result<LpcOutput, LpcError> {
274 self.apply_slices(high, low, close, src)
275 }
276
277 #[inline(always)]
278 pub fn into_stream(self) -> Result<LpcStream, LpcError> {
279 let p = LpcParams {
280 cutoff_type: self.cutoff_type,
281 fixed_period: self.fixed_period,
282 max_cycle_limit: self.max_cycle_limit,
283 cycle_mult: self.cycle_mult,
284 tr_mult: self.tr_mult,
285 };
286 LpcStream::try_new(p)
287 }
288}
289
290#[derive(Clone, Debug)]
291pub struct LpcBatchRange {
292 pub fixed_period: (usize, usize, usize),
293 pub cycle_mult: (f64, f64, f64),
294 pub tr_mult: (f64, f64, f64),
295 pub cutoff_type: String,
296 pub max_cycle_limit: usize,
297}
298
299impl Default for LpcBatchRange {
300 fn default() -> Self {
301 Self {
302 fixed_period: (20, 269, 1),
303 cycle_mult: (1.0, 1.0, 0.0),
304 tr_mult: (1.0, 1.0, 0.0),
305 cutoff_type: "adaptive".to_string(),
306 max_cycle_limit: 60,
307 }
308 }
309}
310
311#[derive(Clone, Debug, Default)]
312pub struct LpcBatchBuilder {
313 range: LpcBatchRange,
314 kernel: Kernel,
315}
316
317impl LpcBatchBuilder {
318 pub fn new() -> Self {
319 Self::default()
320 }
321 pub fn kernel(mut self, k: Kernel) -> Self {
322 self.kernel = k;
323 self
324 }
325
326 pub fn fixed_period_range(mut self, s: usize, e: usize, st: usize) -> Self {
327 self.range.fixed_period = (s, e, st);
328 self
329 }
330 pub fn fixed_period_static(mut self, p: usize) -> Self {
331 self.range.fixed_period = (p, p, 0);
332 self
333 }
334
335 pub fn cycle_mult_range(mut self, s: f64, e: f64, st: f64) -> Self {
336 self.range.cycle_mult = (s, e, st);
337 self
338 }
339 pub fn cycle_mult_static(mut self, x: f64) -> Self {
340 self.range.cycle_mult = (x, x, 0.0);
341 self
342 }
343
344 pub fn tr_mult_range(mut self, s: f64, e: f64, st: f64) -> Self {
345 self.range.tr_mult = (s, e, st);
346 self
347 }
348 pub fn tr_mult_static(mut self, x: f64) -> Self {
349 self.range.tr_mult = (x, x, 0.0);
350 self
351 }
352
353 pub fn cutoff_type(mut self, ct: &str) -> Self {
354 self.range.cutoff_type = ct.to_string();
355 self
356 }
357 pub fn max_cycle_limit(mut self, m: usize) -> Self {
358 self.range.max_cycle_limit = m;
359 self
360 }
361
362 pub fn apply_slices(
363 self,
364 h: &[f64],
365 l: &[f64],
366 c: &[f64],
367 s: &[f64],
368 ) -> Result<LpcBatchOutput, LpcError> {
369 lpc_batch_with_kernel(h, l, c, s, &self.range, self.kernel)
370 }
371}
372
373#[derive(Clone, Debug)]
374pub struct LpcBatchOutput {
375 pub values: Vec<f64>,
376 pub combos: Vec<LpcParams>,
377 pub rows: usize,
378 pub cols: usize,
379}
380
381#[derive(Debug, Error)]
382pub enum LpcError {
383 #[error("lpc: Input data slice is empty.")]
384 EmptyInputData,
385
386 #[error("lpc: All values are NaN.")]
387 AllValuesNaN,
388
389 #[error("lpc: Invalid period: period = {period}, data length = {data_len}")]
390 InvalidPeriod { period: usize, data_len: usize },
391
392 #[error("lpc: Not enough valid data: needed = {needed}, valid = {valid}")]
393 NotEnoughValidData { needed: usize, valid: usize },
394
395 #[error("lpc: Invalid cutoff type: {cutoff_type}, must be 'adaptive' or 'fixed'")]
396 InvalidCutoffType { cutoff_type: String },
397
398 #[error("lpc: Required OHLC data is missing or has mismatched lengths")]
399 MissingData,
400
401 #[error("lpc: output length mismatch: expected = {expected}, got = {got}")]
402 OutputLengthMismatch { expected: usize, got: usize },
403
404 #[error("lpc: invalid range: start = {start}, end = {end}, step = {step}")]
405 InvalidRange {
406 start: usize,
407 end: usize,
408 step: usize,
409 },
410
411 #[error("lpc: invalid kernel for batch path: {0:?}")]
412 InvalidKernelForBatch(Kernel),
413}
414
415pub(crate) fn dom_cycle(src: &[f64], max_cycle_limit: usize) -> Vec<f64> {
416 let len = src.len();
417 let mut dom_cycles = vec![f64::NAN; len];
418
419 if len < 8 {
420 return dom_cycles;
421 }
422
423 let mut in_phase = vec![0.0; len];
424 let mut quadrature = vec![0.0; len];
425 let mut real_part = vec![0.0; len];
426 let mut imag_part = vec![0.0; len];
427 let mut delta_phase = vec![0.0; len];
428 let mut inst_per = vec![0.0; len];
429
430 for i in 7..len {
431 let val1 = src[i] - src[i - 7];
432
433 if i >= 4 {
434 let val1_4 = if i >= 4 {
435 src[i - 4] - src[i.saturating_sub(11)]
436 } else {
437 0.0
438 };
439 let val1_2 = if i >= 2 {
440 src[i - 2] - src[i.saturating_sub(9)]
441 } else {
442 0.0
443 };
444 in_phase[i] = 1.25 * (val1_4 - 0.635 * val1_2)
445 + if i >= 3 { 0.635 * in_phase[i - 3] } else { 0.0 };
446 }
447
448 if i >= 2 {
449 let val1_2 = src[i - 2] - src[i.saturating_sub(9)];
450 quadrature[i] = val1_2 - 0.338 * val1
451 + if i >= 2 {
452 0.338 * quadrature[i - 2]
453 } else {
454 0.0
455 };
456 }
457
458 if i >= 1 {
459 real_part[i] = 0.2
460 * (in_phase[i] * in_phase[i - 1] + quadrature[i] * quadrature[i - 1])
461 + 0.8 * real_part[i - 1];
462 imag_part[i] = 0.2
463 * (in_phase[i] * quadrature[i - 1] - in_phase[i - 1] * quadrature[i])
464 + 0.8 * imag_part[i - 1];
465 }
466
467 if real_part[i] != 0.0 {
468 delta_phase[i] = (imag_part[i] / real_part[i]).atan();
469 }
470
471 let mut val2 = 0.0;
472 let mut found_period = false;
473 for j in 0..=max_cycle_limit.min(i) {
474 if i >= j {
475 val2 += delta_phase[i - j];
476 if val2 > 2.0 * PI && !found_period {
477 inst_per[i] = j as f64;
478 found_period = true;
479 break;
480 }
481 }
482 }
483
484 if !found_period {
485 inst_per[i] = if i > 0 { inst_per[i - 1] } else { 20.0 };
486 }
487
488 if i > 0 && !dom_cycles[i - 1].is_nan() {
489 dom_cycles[i] = 0.25 * inst_per[i] + 0.75 * dom_cycles[i - 1];
490 } else {
491 dom_cycles[i] = inst_per[i];
492 }
493 }
494
495 dom_cycles
496}
497
498fn lp_filter(src: &[f64], period: usize) -> Vec<f64> {
499 let len = src.len();
500 let mut output = vec![f64::NAN; len];
501
502 if period == 0 || len == 0 {
503 return output;
504 }
505
506 let omega = 2.0 * PI / (period as f64);
507 let alpha = (1.0 - omega.sin()) / omega.cos();
508
509 if !src[0].is_nan() {
510 output[0] = src[0];
511 }
512
513 for i in 1..len {
514 if !src[i].is_nan() && !src[i - 1].is_nan() && !output[i - 1].is_nan() {
515 output[i] = 0.5 * (1.0 - alpha) * (src[i] + src[i - 1]) + alpha * output[i - 1];
516 } else if !src[i].is_nan() {
517 output[i] = src[i];
518 }
519 }
520
521 output
522}
523
524fn calculate_true_range(high: &[f64], low: &[f64], close: &[f64]) -> Vec<f64> {
525 let len = high.len();
526 let mut tr = vec![0.0; len];
527
528 if len == 0 {
529 return tr;
530 }
531
532 tr[0] = high[0] - low[0];
533
534 for i in 1..len {
535 let hl = high[i] - low[i];
536 let c_low1 = (close[i] - low[i - 1]).abs();
537 let c_high1 = (close[i] - high[i - 1]).abs();
538 tr[i] = hl.max(c_low1).max(c_high1);
539 }
540
541 tr
542}
543
544#[inline(always)]
545
546pub fn lpc_scalar(
547 high: &[f64],
548 low: &[f64],
549 close: &[f64],
550 src: &[f64],
551 cutoff_type: &str,
552 fixed_period: usize,
553 max_cycle_limit: usize,
554 cycle_mult: f64,
555 tr_mult: f64,
556 first: usize,
557 out_filter: &mut [f64],
558 out_high: &mut [f64],
559 out_low: &mut [f64],
560) {
561 let len = src.len();
562
563 if first > 0 {
564 out_filter[..first].fill(f64::NAN);
565 out_high[..first].fill(f64::NAN);
566 out_low[..first].fill(f64::NAN);
567 }
568
569 let dc = if cutoff_type.eq_ignore_ascii_case("adaptive") {
570 Some(dom_cycle(src, max_cycle_limit))
571 } else {
572 None
573 };
574
575 if first >= len {
576 return;
577 }
578
579 out_filter[first] = src[first];
580 let mut tr_prev = high[first] - low[first];
581 let mut ftr_prev = tr_prev;
582 let tm = tr_mult;
583
584 out_high[first] = out_filter[first] + tr_prev * tm;
585 out_low[first] = out_filter[first] - tr_prev * tm;
586
587 #[inline(always)]
588 fn alpha_from_period(p: usize) -> f64 {
589 let omega = 2.0 * std::f64::consts::PI / (p as f64);
590 let (s, c) = omega.sin_cos();
591 (1.0 - s) / c
592 }
593 #[inline(always)]
594 fn per_bar_period(dc_opt: Option<&[f64]>, idx: usize, fixed_p: usize, cm: f64) -> usize {
595 if let Some(dc) = dc_opt {
596 let base = dc[idx];
597 if base.is_nan() {
598 fixed_p
599 } else {
600 (base * cm).round().max(3.0) as usize
601 }
602 } else {
603 fixed_p
604 }
605 }
606
607 let mut last_p: usize = if dc.is_none() { fixed_period } else { 0 };
608 let mut alpha: f64 = if dc.is_none() {
609 alpha_from_period(fixed_period)
610 } else {
611 0.0
612 };
613
614 let mut i = first + 1;
615 while i + 1 < len {
616 let p_i = per_bar_period(dc.as_deref(), i, fixed_period, cycle_mult);
617 if p_i != last_p {
618 last_p = p_i;
619 alpha = alpha_from_period(last_p);
620 }
621 let one_m_a = 1.0 - alpha;
622 let s_im1 = src[i - 1];
623 let s_i = src[i];
624 let prev_f = out_filter[i - 1];
625 let f_i = alpha.mul_add(prev_f, 0.5 * one_m_a * (s_i + s_im1));
626 out_filter[i] = f_i;
627
628 let hl = high[i] - low[i];
629 let c_low1 = (close[i] - low[i - 1]).abs();
630 let c_hi1 = (close[i] - high[i - 1]).abs();
631 let tr_i = hl.max(c_low1).max(c_hi1);
632 let ftr_i = alpha.mul_add(ftr_prev, 0.5 * one_m_a * (tr_i + tr_prev));
633 tr_prev = tr_i;
634 ftr_prev = ftr_i;
635 out_high[i] = f_i + ftr_i * tm;
636 out_low[i] = f_i - ftr_i * tm;
637
638 let i1 = i + 1;
639 let p_i1 = per_bar_period(dc.as_deref(), i1, fixed_period, cycle_mult);
640 if p_i1 != last_p {
641 last_p = p_i1;
642 alpha = alpha_from_period(last_p);
643 }
644 let one_m_a1 = 1.0 - alpha;
645 let s_i1 = src[i1];
646 let f_i1 = alpha.mul_add(f_i, 0.5 * one_m_a1 * (s_i1 + s_i));
647 out_filter[i1] = f_i1;
648
649 let hl1 = high[i1] - low[i1];
650 let c_low1b = (close[i1] - low[i1 - 1]).abs();
651 let c_hi1b = (close[i1] - high[i1 - 1]).abs();
652 let tr_i1 = hl1.max(c_low1b).max(c_hi1b);
653 let ftr_i1 = alpha.mul_add(ftr_prev, 0.5 * one_m_a1 * (tr_i1 + tr_prev));
654 tr_prev = tr_i1;
655 ftr_prev = ftr_i1;
656 out_high[i1] = f_i1 + ftr_i1 * tm;
657 out_low[i1] = f_i1 - ftr_i1 * tm;
658
659 i += 2;
660 }
661
662 if i < len {
663 let p_i = per_bar_period(dc.as_deref(), i, fixed_period, cycle_mult);
664 if p_i != last_p {
665 last_p = p_i;
666 alpha = alpha_from_period(last_p);
667 }
668 let one_m_a = 1.0 - alpha;
669 let s_im1 = src[i - 1];
670 let s_i = src[i];
671 let prev_f = out_filter[i - 1];
672 let f_i = alpha.mul_add(prev_f, 0.5 * one_m_a * (s_i + s_im1));
673 out_filter[i] = f_i;
674
675 let hl = high[i] - low[i];
676 let c_low1 = (close[i] - low[i - 1]).abs();
677 let c_hi1 = (close[i] - high[i - 1]).abs();
678 let tr_i = hl.max(c_low1).max(c_hi1);
679 let ftr_i = alpha.mul_add(ftr_prev, 0.5 * one_m_a * (tr_i + tr_prev));
680 out_high[i] = f_i + ftr_i * tm;
681 out_low[i] = f_i - ftr_i * tm;
682 }
683}
684
685#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
686pub fn lpc_avx2(
687 high: &[f64],
688 low: &[f64],
689 close: &[f64],
690 src: &[f64],
691 cutoff_type: &str,
692 fixed_period: usize,
693 max_cycle_limit: usize,
694 cycle_mult: f64,
695 tr_mult: f64,
696 first: usize,
697 out_filter: &mut [f64],
698 out_high: &mut [f64],
699 out_low: &mut [f64],
700) {
701 unsafe {
702 if src.len() > first + 32 {
703 _mm_prefetch(src.as_ptr().add(first + 16) as *const i8, _MM_HINT_T0);
704 _mm_prefetch(high.as_ptr().add(first + 16) as *const i8, _MM_HINT_T0);
705 _mm_prefetch(low.as_ptr().add(first + 16) as *const i8, _MM_HINT_T0);
706 _mm_prefetch(close.as_ptr().add(first + 16) as *const i8, _MM_HINT_T0);
707 }
708 }
709 lpc_scalar(
710 high,
711 low,
712 close,
713 src,
714 cutoff_type,
715 fixed_period,
716 max_cycle_limit,
717 cycle_mult,
718 tr_mult,
719 first,
720 out_filter,
721 out_high,
722 out_low,
723 )
724}
725
726#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
727pub fn lpc_avx512(
728 high: &[f64],
729 low: &[f64],
730 close: &[f64],
731 src: &[f64],
732 cutoff_type: &str,
733 fixed_period: usize,
734 max_cycle_limit: usize,
735 cycle_mult: f64,
736 tr_mult: f64,
737 first: usize,
738 out_filter: &mut [f64],
739 out_high: &mut [f64],
740 out_low: &mut [f64],
741) {
742 unsafe {
743 if src.len() > first + 64 {
744 _mm_prefetch(src.as_ptr().add(first + 32) as *const i8, _MM_HINT_T0);
745 _mm_prefetch(high.as_ptr().add(first + 32) as *const i8, _MM_HINT_T0);
746 _mm_prefetch(low.as_ptr().add(first + 32) as *const i8, _MM_HINT_T0);
747 _mm_prefetch(close.as_ptr().add(first + 32) as *const i8, _MM_HINT_T0);
748 }
749 }
750 lpc_scalar(
751 high,
752 low,
753 close,
754 src,
755 cutoff_type,
756 fixed_period,
757 max_cycle_limit,
758 cycle_mult,
759 tr_mult,
760 first,
761 out_filter,
762 out_high,
763 out_low,
764 )
765}
766
767#[inline(always)]
768fn lpc_compute_into(
769 high: &[f64],
770 low: &[f64],
771 close: &[f64],
772 src: &[f64],
773 cutoff_type: &str,
774 fixed_period: usize,
775 max_cycle_limit: usize,
776 cycle_mult: f64,
777 tr_mult: f64,
778 first: usize,
779 kernel: Kernel,
780 out_filter: &mut [f64],
781 out_high: &mut [f64],
782 out_low: &mut [f64],
783) {
784 let actual_kernel = match kernel {
785 Kernel::Auto => Kernel::Scalar,
786 k => k,
787 };
788
789 match actual_kernel {
790 Kernel::Scalar | Kernel::ScalarBatch | Kernel::Auto => lpc_scalar(
791 high,
792 low,
793 close,
794 src,
795 cutoff_type,
796 fixed_period,
797 max_cycle_limit,
798 cycle_mult,
799 tr_mult,
800 first,
801 out_filter,
802 out_high,
803 out_low,
804 ),
805 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
806 Kernel::Avx2 | Kernel::Avx2Batch => lpc_avx2(
807 high,
808 low,
809 close,
810 src,
811 cutoff_type,
812 fixed_period,
813 max_cycle_limit,
814 cycle_mult,
815 tr_mult,
816 first,
817 out_filter,
818 out_high,
819 out_low,
820 ),
821 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
822 Kernel::Avx512 | Kernel::Avx512Batch => lpc_avx512(
823 high,
824 low,
825 close,
826 src,
827 cutoff_type,
828 fixed_period,
829 max_cycle_limit,
830 cycle_mult,
831 tr_mult,
832 first,
833 out_filter,
834 out_high,
835 out_low,
836 ),
837 #[cfg(not(all(feature = "nightly-avx", target_arch = "x86_64")))]
838 _ => lpc_scalar(
839 high,
840 low,
841 close,
842 src,
843 cutoff_type,
844 fixed_period,
845 max_cycle_limit,
846 cycle_mult,
847 tr_mult,
848 first,
849 out_filter,
850 out_high,
851 out_low,
852 ),
853 }
854}
855
856#[inline(always)]
857fn lpc_compute_into_prefilled(
858 high: &[f64],
859 low: &[f64],
860 close: &[f64],
861 src: &[f64],
862 cutoff_type: &str,
863 fixed_period: usize,
864 max_cycle_limit: usize,
865 cycle_mult: f64,
866 tr_mult: f64,
867 first: usize,
868 out_filter: &mut [f64],
869 out_high: &mut [f64],
870 out_low: &mut [f64],
871) {
872 let len = src.len();
873 if first >= len {
874 return;
875 }
876
877 out_filter[first] = src[first];
878 let mut tr_prev = high[first] - low[first];
879 let mut ftr_prev = tr_prev;
880 let tm = tr_mult;
881 out_high[first] = out_filter[first] + tr_prev * tm;
882 out_low[first] = out_filter[first] - tr_prev * tm;
883
884 let dc = if cutoff_type.eq_ignore_ascii_case("adaptive") {
885 Some(dom_cycle(src, max_cycle_limit))
886 } else {
887 None
888 };
889
890 #[inline(always)]
891 fn alpha_from_period(p: usize) -> f64 {
892 let omega = 2.0 * std::f64::consts::PI / (p as f64);
893 let (s, c) = omega.sin_cos();
894 (1.0 - s) / c
895 }
896 #[inline(always)]
897 fn per_bar_period(dc_opt: Option<&[f64]>, idx: usize, fixed_p: usize, cm: f64) -> usize {
898 if let Some(dc) = dc_opt {
899 let base = dc[idx];
900 if base.is_nan() {
901 fixed_p
902 } else {
903 (base * cm).round().max(3.0) as usize
904 }
905 } else {
906 fixed_p
907 }
908 }
909
910 let mut last_p: usize = if dc.is_none() { fixed_period } else { 0 };
911 let mut alpha: f64 = if dc.is_none() {
912 alpha_from_period(fixed_period)
913 } else {
914 0.0
915 };
916
917 let mut i = first + 1;
918 while i + 1 < len {
919 let p_i = per_bar_period(dc.as_deref(), i, fixed_period, cycle_mult);
920 if p_i != last_p {
921 last_p = p_i;
922 alpha = alpha_from_period(last_p);
923 }
924 let one_m_a = 1.0 - alpha;
925 let f_i = alpha.mul_add(out_filter[i - 1], 0.5 * one_m_a * (src[i] + src[i - 1]));
926 out_filter[i] = f_i;
927
928 let hl = high[i] - low[i];
929 let c_low1 = (close[i] - low[i - 1]).abs();
930 let c_hi1 = (close[i] - high[i - 1]).abs();
931 let tr_i = hl.max(c_low1).max(c_hi1);
932 let ftr_i = alpha.mul_add(ftr_prev, 0.5 * one_m_a * (tr_i + tr_prev));
933 tr_prev = tr_i;
934 ftr_prev = ftr_i;
935 out_high[i] = f_i + ftr_i * tm;
936 out_low[i] = f_i - ftr_i * tm;
937
938 let i1 = i + 1;
939 let p_i1 = per_bar_period(dc.as_deref(), i1, fixed_period, cycle_mult);
940 if p_i1 != last_p {
941 last_p = p_i1;
942 alpha = alpha_from_period(last_p);
943 }
944 let one_m_a1 = 1.0 - alpha;
945 let f_i1 = alpha.mul_add(f_i, 0.5 * one_m_a1 * (src[i1] + src[i]));
946 out_filter[i1] = f_i1;
947
948 let hl1 = high[i1] - low[i1];
949 let c_low1b = (close[i1] - low[i1 - 1]).abs();
950 let c_hi1b = (close[i1] - high[i1 - 1]).abs();
951 let tr_i1 = hl1.max(c_low1b).max(c_hi1b);
952 let ftr_i1 = alpha.mul_add(ftr_prev, 0.5 * one_m_a1 * (tr_i1 + tr_prev));
953 tr_prev = tr_i1;
954 ftr_prev = ftr_i1;
955 out_high[i1] = f_i1 + ftr_i1 * tm;
956 out_low[i1] = f_i1 - ftr_i1 * tm;
957
958 i += 2;
959 }
960
961 if i < len {
962 let p_i = per_bar_period(dc.as_deref(), i, fixed_period, cycle_mult);
963 if p_i != last_p {
964 last_p = p_i;
965 alpha = alpha_from_period(last_p);
966 }
967 let one_m_a = 1.0 - alpha;
968 let f_i = alpha.mul_add(out_filter[i - 1], 0.5 * one_m_a * (src[i] + src[i - 1]));
969 out_filter[i] = f_i;
970
971 let hl = high[i] - low[i];
972 let c_low1 = (close[i] - low[i - 1]).abs();
973 let c_hi1 = (close[i] - high[i - 1]).abs();
974 let tr_i = hl.max(c_low1).max(c_hi1);
975 let ftr_i = alpha.mul_add(ftr_prev, 0.5 * one_m_a * (tr_i + tr_prev));
976 out_high[i] = f_i + ftr_i * tm;
977 out_low[i] = f_i - ftr_i * tm;
978 }
979}
980
981#[inline(always)]
982fn lpc_compute_into_prefilled_pretr(
983 _high: &[f64],
984 _low: &[f64],
985 _close: &[f64],
986 src: &[f64],
987 tr: &[f64],
988 cutoff_type: &str,
989 fixed_period: usize,
990 max_cycle_limit: usize,
991 cycle_mult: f64,
992 tr_mult: f64,
993 first: usize,
994 out_filter: &mut [f64],
995 out_high: &mut [f64],
996 out_low: &mut [f64],
997) {
998 let len = src.len();
999 if first >= len {
1000 return;
1001 }
1002
1003 out_filter[first] = src[first];
1004 let mut tr_prev = tr[first];
1005 let mut ftr_prev = tr_prev;
1006 let tm = tr_mult;
1007 out_high[first] = out_filter[first] + tr_prev * tm;
1008 out_low[first] = out_filter[first] - tr_prev * tm;
1009
1010 let dc = if cutoff_type.eq_ignore_ascii_case("adaptive") {
1011 Some(dom_cycle(src, max_cycle_limit))
1012 } else {
1013 None
1014 };
1015
1016 #[inline(always)]
1017 fn alpha_from_period(p: usize) -> f64 {
1018 let omega = 2.0 * std::f64::consts::PI / (p as f64);
1019 let (s, c) = omega.sin_cos();
1020 (1.0 - s) / c
1021 }
1022 #[inline(always)]
1023 fn per_bar_period(dc_opt: Option<&[f64]>, idx: usize, fixed_p: usize, cm: f64) -> usize {
1024 if let Some(dc) = dc_opt {
1025 let base = dc[idx];
1026 if base.is_nan() {
1027 fixed_p
1028 } else {
1029 (base * cm).round().max(3.0) as usize
1030 }
1031 } else {
1032 fixed_p
1033 }
1034 }
1035
1036 let mut last_p: usize = if dc.is_none() { fixed_period } else { 0 };
1037 let mut alpha: f64 = if dc.is_none() {
1038 alpha_from_period(fixed_period)
1039 } else {
1040 0.0
1041 };
1042
1043 let mut i = first + 1;
1044 while i + 1 < len {
1045 let p_i = per_bar_period(dc.as_deref(), i, fixed_period, cycle_mult);
1046 if p_i != last_p {
1047 last_p = p_i;
1048 alpha = alpha_from_period(last_p);
1049 }
1050 let one_m_a = 1.0 - alpha;
1051 let f_i = alpha.mul_add(out_filter[i - 1], 0.5 * one_m_a * (src[i] + src[i - 1]));
1052 out_filter[i] = f_i;
1053
1054 let tr_i = tr[i];
1055 let ftr_i = alpha.mul_add(ftr_prev, 0.5 * one_m_a * (tr_i + tr_prev));
1056 tr_prev = tr_i;
1057 ftr_prev = ftr_i;
1058 out_high[i] = f_i + ftr_i * tm;
1059 out_low[i] = f_i - ftr_i * tm;
1060
1061 let i1 = i + 1;
1062 let p_i1 = per_bar_period(dc.as_deref(), i1, fixed_period, cycle_mult);
1063 if p_i1 != last_p {
1064 last_p = p_i1;
1065 alpha = alpha_from_period(last_p);
1066 }
1067 let one_m_a1 = 1.0 - alpha;
1068 let f_i1 = alpha.mul_add(f_i, 0.5 * one_m_a1 * (src[i1] + src[i]));
1069 out_filter[i1] = f_i1;
1070
1071 let tr_i1 = tr[i1];
1072 let ftr_i1 = alpha.mul_add(ftr_prev, 0.5 * one_m_a1 * (tr_i1 + tr_prev));
1073 tr_prev = tr_i1;
1074 ftr_prev = ftr_i1;
1075 out_high[i1] = f_i1 + ftr_i1 * tm;
1076 out_low[i1] = f_i1 - ftr_i1 * tm;
1077
1078 i += 2;
1079 }
1080
1081 if i < len {
1082 let p_i = per_bar_period(dc.as_deref(), i, fixed_period, cycle_mult);
1083 if p_i != last_p {
1084 last_p = p_i;
1085 alpha = alpha_from_period(last_p);
1086 }
1087 let one_m_a = 1.0 - alpha;
1088 let f_i = alpha.mul_add(out_filter[i - 1], 0.5 * one_m_a * (src[i] + src[i - 1]));
1089 out_filter[i] = f_i;
1090
1091 let tr_i = tr[i];
1092 let ftr_i = alpha.mul_add(ftr_prev, 0.5 * one_m_a * (tr_i + tr_prev));
1093 out_high[i] = f_i + ftr_i * tr_mult;
1094 out_low[i] = f_i - ftr_i * tr_mult;
1095 }
1096}
1097
1098#[inline]
1099pub fn lpc(input: &LpcInput) -> Result<LpcOutput, LpcError> {
1100 lpc_with_kernel(input, Kernel::Auto)
1101}
1102
1103pub fn lpc_with_kernel(input: &LpcInput, kernel: Kernel) -> Result<LpcOutput, LpcError> {
1104 let (h, l, c, s, cutoff, fp, mcl, cm, tm, first, _chosen) = lpc_prepare(input, kernel)?;
1105 let len = s.len();
1106
1107 let mut filter = alloc_with_nan_prefix(len, first);
1108 let mut high_band = alloc_with_nan_prefix(len, first);
1109 let mut low_band = alloc_with_nan_prefix(len, first);
1110
1111 lpc_compute_into(
1112 h,
1113 l,
1114 c,
1115 s,
1116 &cutoff,
1117 fp,
1118 mcl,
1119 cm,
1120 tm,
1121 first,
1122 kernel,
1123 &mut filter,
1124 &mut high_band,
1125 &mut low_band,
1126 );
1127
1128 Ok(LpcOutput {
1129 filter,
1130 high_band,
1131 low_band,
1132 })
1133}
1134
1135#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
1136pub fn lpc_into(
1137 input: &LpcInput,
1138 filter_out: &mut [f64],
1139 high_out: &mut [f64],
1140 low_out: &mut [f64],
1141) -> Result<(), LpcError> {
1142 lpc_into_slices(filter_out, high_out, low_out, input, Kernel::Auto)
1143}
1144
1145fn lpc_prepare<'a>(
1146 input: &'a LpcInput<'a>,
1147 kernel: Kernel,
1148) -> Result<
1149 (
1150 &'a [f64],
1151 &'a [f64],
1152 &'a [f64],
1153 &'a [f64],
1154 String,
1155 usize,
1156 usize,
1157 f64,
1158 f64,
1159 usize,
1160 Kernel,
1161 ),
1162 LpcError,
1163> {
1164 let (high, low, close, src) = match &input.data {
1165 LpcData::Candles { candles, source } => {
1166 let src_data = source_type(candles, source);
1167 (
1168 &candles.high[..],
1169 &candles.low[..],
1170 &candles.close[..],
1171 src_data,
1172 )
1173 }
1174 LpcData::Slices {
1175 high,
1176 low,
1177 close,
1178 src,
1179 } => (*high, *low, *close, *src),
1180 };
1181
1182 if src.is_empty() {
1183 return Err(LpcError::EmptyInputData);
1184 }
1185
1186 if high.len() != src.len() || low.len() != src.len() || close.len() != src.len() {
1187 return Err(LpcError::MissingData);
1188 }
1189
1190 if src.iter().all(|v| v.is_nan())
1191 || high.iter().all(|v| v.is_nan())
1192 || low.iter().all(|v| v.is_nan())
1193 || close.iter().all(|v| v.is_nan())
1194 {
1195 return Err(LpcError::AllValuesNaN);
1196 }
1197
1198 let cutoff_type = input.get_cutoff_type();
1199 if !cutoff_type.eq_ignore_ascii_case("adaptive") && !cutoff_type.eq_ignore_ascii_case("fixed") {
1200 return Err(LpcError::InvalidCutoffType { cutoff_type });
1201 }
1202
1203 let fixed_period = input.get_fixed_period();
1204 let max_cycle_limit = input.get_max_cycle_limit();
1205 let cycle_mult = input.get_cycle_mult();
1206 let tr_mult = input.get_tr_mult();
1207
1208 if fixed_period == 0 || fixed_period > src.len() {
1209 return Err(LpcError::InvalidPeriod {
1210 period: fixed_period,
1211 data_len: src.len(),
1212 });
1213 }
1214
1215 let mut first = 0;
1216 for i in 0..src.len() {
1217 if !src[i].is_nan() && !high[i].is_nan() && !low[i].is_nan() && !close[i].is_nan() {
1218 first = i;
1219 break;
1220 }
1221 }
1222
1223 let valid = src.len().saturating_sub(first);
1224 if valid < 2 {
1225 return Err(LpcError::NotEnoughValidData { needed: 2, valid });
1226 }
1227
1228 let chosen = if kernel == Kernel::Auto {
1229 Kernel::Scalar
1230 } else {
1231 kernel
1232 };
1233
1234 Ok((
1235 high,
1236 low,
1237 close,
1238 src,
1239 cutoff_type,
1240 fixed_period,
1241 max_cycle_limit,
1242 cycle_mult,
1243 tr_mult,
1244 first,
1245 chosen,
1246 ))
1247}
1248
1249pub struct LpcStream {
1250 cutoff_type: String,
1251 fixed_period: usize,
1252 max_cycle_limit: usize,
1253 cycle_mult: f64,
1254 tr_mult: f64,
1255 adaptive_enabled: bool,
1256
1257 prev_src: f64,
1258 prev_high: f64,
1259 prev_low: f64,
1260 prev_close: f64,
1261
1262 prev_filter: f64,
1263 prev_tr: f64,
1264 prev_ftr: f64,
1265
1266 last_p: usize,
1267 alpha: f64,
1268 one_minus_alpha: f64,
1269
1270 dc: DomCycleState,
1271}
1272
1273#[derive(Clone)]
1274struct DomCycleState {
1275 buf: [f64; 12],
1276 idx: usize,
1277 count: usize,
1278
1279 ip_l1: f64,
1280 ip_l2: f64,
1281 ip_l3: f64,
1282 q_l1: f64,
1283 q_l2: f64,
1284
1285 real_prev: f64,
1286 imag_prev: f64,
1287
1288 phase_accum: f64,
1289 bars_since_cross: usize,
1290 last_inst_per: f64,
1291
1292 dom_cycle_prev: f64,
1293}
1294
1295impl Default for DomCycleState {
1296 fn default() -> Self {
1297 Self {
1298 buf: [0.0; 12],
1299 idx: 0,
1300 count: 0,
1301 ip_l1: 0.0,
1302 ip_l2: 0.0,
1303 ip_l3: 0.0,
1304 q_l1: 0.0,
1305 q_l2: 0.0,
1306 real_prev: 0.0,
1307 imag_prev: 0.0,
1308 phase_accum: 0.0,
1309 bars_since_cross: 0,
1310 last_inst_per: 20.0,
1311 dom_cycle_prev: 20.0,
1312 }
1313 }
1314}
1315
1316impl DomCycleState {
1317 #[inline(always)]
1318 fn push_src(&mut self, x: f64) {
1319 self.buf[self.idx] = x;
1320 self.idx = (self.idx + 1) % 12;
1321 self.count = self.count.saturating_add(1);
1322 }
1323
1324 #[inline(always)]
1325 fn at(&self, lag: usize) -> f64 {
1326 debug_assert!(lag < 12);
1327 let pos = (self.idx + 12 - 1 - lag) % 12;
1328 self.buf[pos]
1329 }
1330
1331 #[inline(always)]
1332 fn update_ifm(&mut self) -> Option<f64> {
1333 if self.count < 12 {
1334 return None;
1335 }
1336
1337 let v0 = self.at(0);
1338 let v2 = self.at(2);
1339 let v4 = self.at(4);
1340 let v7 = self.at(7);
1341 let v9 = self.at(9);
1342 let v11 = self.at(11);
1343
1344 let ip_prev = self.ip_l1;
1345 let q_prev = self.q_l1;
1346
1347 let ip_cur = 1.25 * ((v4 - v11) - 0.635 * (v2 - v9)) + 0.635 * self.ip_l3;
1348
1349 let q_cur = (v2 - v9) - 0.338 * (v0 - v7) + 0.338 * self.q_l2;
1350
1351 let real_cur = 0.2 * (ip_cur * ip_prev + q_cur * q_prev) + 0.8 * self.real_prev;
1352 let imag_cur = 0.2 * (ip_cur * q_prev - ip_prev * q_cur) + 0.8 * self.imag_prev;
1353
1354 let delta = if real_cur != 0.0 {
1355 (imag_cur / real_cur).atan()
1356 } else {
1357 0.0
1358 };
1359
1360 const TAU: f64 = std::f64::consts::PI * 2.0;
1361 self.phase_accum += delta;
1362 self.bars_since_cross = self.bars_since_cross.saturating_add(1);
1363
1364 let mut inst = self.last_inst_per;
1365 if self.phase_accum > TAU {
1366 inst = self.bars_since_cross as f64;
1367 self.phase_accum = 0.0;
1368 self.bars_since_cross = 0;
1369 self.last_inst_per = inst;
1370 }
1371
1372 let dom = 0.25 * inst + 0.75 * self.dom_cycle_prev;
1373
1374 self.ip_l3 = self.ip_l2;
1375 self.ip_l2 = self.ip_l1;
1376 self.ip_l1 = ip_cur;
1377
1378 self.q_l2 = self.q_l1;
1379 self.q_l1 = q_cur;
1380
1381 self.real_prev = real_cur;
1382 self.imag_prev = imag_cur;
1383 self.dom_cycle_prev = dom;
1384
1385 Some(dom)
1386 }
1387}
1388
1389impl LpcStream {
1390 pub fn try_new(params: LpcParams) -> Result<Self, LpcError> {
1391 let cutoff_type = params.cutoff_type.unwrap_or_else(|| "adaptive".to_string());
1392 let ct_lower = cutoff_type.to_ascii_lowercase();
1393 if ct_lower != "adaptive" && ct_lower != "fixed" {
1394 return Err(LpcError::InvalidCutoffType { cutoff_type });
1395 }
1396
1397 let fixed_period = params.fixed_period.unwrap_or(20);
1398 if fixed_period == 0 {
1399 return Err(LpcError::InvalidPeriod {
1400 period: 0,
1401 data_len: 0,
1402 });
1403 }
1404
1405 let mut s = Self {
1406 cutoff_type,
1407 fixed_period,
1408 max_cycle_limit: params.max_cycle_limit.unwrap_or(60),
1409 cycle_mult: params.cycle_mult.unwrap_or(1.0),
1410 tr_mult: params.tr_mult.unwrap_or(1.0),
1411 adaptive_enabled: ct_lower == "adaptive",
1412
1413 prev_src: f64::NAN,
1414 prev_high: f64::NAN,
1415 prev_low: f64::NAN,
1416 prev_close: f64::NAN,
1417
1418 prev_filter: f64::NAN,
1419 prev_tr: f64::NAN,
1420 prev_ftr: f64::NAN,
1421
1422 last_p: 0,
1423 alpha: 0.0,
1424 one_minus_alpha: 0.0,
1425
1426 dc: DomCycleState::default(),
1427 };
1428
1429 s.set_alpha(fixed_period);
1430 Ok(s)
1431 }
1432
1433 #[inline(always)]
1434 fn set_alpha(&mut self, p: usize) {
1435 if p == self.last_p {
1436 return;
1437 }
1438
1439 let omega = 2.0 * std::f64::consts::PI / (p as f64);
1440 let (s, c) = omega.sin_cos();
1441 let a = if c.abs() < 1e-12 {
1442 if self.last_p == 0 {
1443 2.0 / (p as f64 + 1.0)
1444 } else {
1445 self.alpha
1446 }
1447 } else {
1448 (1.0 - s) / c
1449 };
1450 self.alpha = a;
1451 self.one_minus_alpha = 1.0 - a;
1452 self.last_p = p;
1453 }
1454
1455 pub fn update(&mut self, high: f64, low: f64, close: f64, src: f64) -> Option<(f64, f64, f64)> {
1456 if !(high.is_finite() && low.is_finite() && close.is_finite() && src.is_finite()) {
1457 return None;
1458 }
1459
1460 self.dc.push_src(src);
1461
1462 let mut period = self.fixed_period;
1463 if self.adaptive_enabled {
1464 if let Some(dom) = self.dc.update_ifm() {
1465 let p = (dom * self.cycle_mult).round().max(3.0) as usize;
1466 period = if self.max_cycle_limit > 0 {
1467 p.min(self.max_cycle_limit)
1468 } else {
1469 p
1470 };
1471 }
1472 }
1473 self.set_alpha(period);
1474
1475 let filt = if self.prev_filter.is_nan() || self.prev_src.is_nan() {
1476 src
1477 } else {
1478 self.alpha.mul_add(
1479 self.prev_filter,
1480 0.5 * self.one_minus_alpha * (src + self.prev_src),
1481 )
1482 };
1483
1484 let tr = if self.prev_high.is_nan() || self.prev_low.is_nan() || self.prev_close.is_nan() {
1485 (high - low).abs()
1486 } else {
1487 let hl = high - low;
1488 let c_low1 = (close - self.prev_low).abs();
1489 let c_high1 = (close - self.prev_high).abs();
1490 hl.max(c_low1).max(c_high1)
1491 };
1492
1493 let ftr = if self.prev_ftr.is_nan() || self.prev_tr.is_nan() {
1494 tr
1495 } else {
1496 self.alpha.mul_add(
1497 self.prev_ftr,
1498 0.5 * self.one_minus_alpha * (tr + self.prev_tr),
1499 )
1500 };
1501
1502 let band_high = filt + ftr * self.tr_mult;
1503 let band_low = filt - ftr * self.tr_mult;
1504
1505 self.prev_src = src;
1506 self.prev_high = high;
1507 self.prev_low = low;
1508 self.prev_close = close;
1509
1510 self.prev_tr = tr;
1511 self.prev_filter = filt;
1512 self.prev_ftr = ftr;
1513
1514 Some((filt, band_high, band_low))
1515 }
1516}
1517
1518#[cfg(feature = "python")]
1519#[pyfunction(name = "lpc")]
1520#[pyo3(signature = (high, low, close, src, cutoff_type=None, fixed_period=None, max_cycle_limit=None, cycle_mult=None, tr_mult=None, kernel=None))]
1521pub fn lpc_py<'py>(
1522 py: Python<'py>,
1523 high: PyReadonlyArray1<'py, f64>,
1524 low: PyReadonlyArray1<'py, f64>,
1525 close: PyReadonlyArray1<'py, f64>,
1526 src: PyReadonlyArray1<'py, f64>,
1527 cutoff_type: Option<String>,
1528 fixed_period: Option<usize>,
1529 max_cycle_limit: Option<usize>,
1530 cycle_mult: Option<f64>,
1531 tr_mult: Option<f64>,
1532 kernel: Option<&str>,
1533) -> PyResult<(
1534 Bound<'py, PyArray1<f64>>,
1535 Bound<'py, PyArray1<f64>>,
1536 Bound<'py, PyArray1<f64>>,
1537)> {
1538 let h = high.as_slice()?;
1539 let l = low.as_slice()?;
1540 let c = close.as_slice()?;
1541 let s = src.as_slice()?;
1542
1543 if h.len() != s.len() || l.len() != s.len() || c.len() != s.len() {
1544 return Err(PyValueError::new_err(
1545 "All arrays must have the same length",
1546 ));
1547 }
1548
1549 let params = LpcParams {
1550 cutoff_type,
1551 fixed_period,
1552 max_cycle_limit,
1553 cycle_mult,
1554 tr_mult,
1555 };
1556
1557 let input = LpcInput::from_slices(h, l, c, s, params);
1558 let kern = validate_kernel(kernel, false)?;
1559
1560 match lpc_with_kernel(&input, kern) {
1561 Ok(output) => Ok((
1562 output.filter.into_pyarray(py),
1563 output.high_band.into_pyarray(py),
1564 output.low_band.into_pyarray(py),
1565 )),
1566 Err(e) => Err(PyValueError::new_err(e.to_string())),
1567 }
1568}
1569
1570#[cfg(feature = "python")]
1571#[pyclass(name = "LpcStream")]
1572pub struct LpcStreamPy {
1573 inner: LpcStream,
1574}
1575
1576#[cfg(feature = "python")]
1577#[pymethods]
1578impl LpcStreamPy {
1579 #[new]
1580 #[pyo3(signature = (cutoff_type=None, fixed_period=None, max_cycle_limit=None, cycle_mult=None, tr_mult=None))]
1581 pub fn new(
1582 cutoff_type: Option<String>,
1583 fixed_period: Option<usize>,
1584 max_cycle_limit: Option<usize>,
1585 cycle_mult: Option<f64>,
1586 tr_mult: Option<f64>,
1587 ) -> PyResult<Self> {
1588 let params = LpcParams {
1589 cutoff_type,
1590 fixed_period,
1591 max_cycle_limit,
1592 cycle_mult,
1593 tr_mult,
1594 };
1595
1596 match LpcStream::try_new(params) {
1597 Ok(stream) => Ok(Self { inner: stream }),
1598 Err(e) => Err(PyValueError::new_err(e.to_string())),
1599 }
1600 }
1601
1602 pub fn update(&mut self, high: f64, low: f64, close: f64, src: f64) -> Option<(f64, f64, f64)> {
1603 self.inner.update(high, low, close, src)
1604 }
1605}
1606
1607#[cfg(feature = "python")]
1608#[pyfunction(name = "lpc_batch")]
1609#[pyo3(signature = (
1610 high, low, close, src,
1611 fixed_period_range, cycle_mult_range, tr_mult_range,
1612 cutoff_type="fixed", max_cycle_limit=60, kernel=None
1613))]
1614pub fn lpc_batch_py<'py>(
1615 py: Python<'py>,
1616 high: numpy::PyReadonlyArray1<'py, f64>,
1617 low: numpy::PyReadonlyArray1<'py, f64>,
1618 close: numpy::PyReadonlyArray1<'py, f64>,
1619 src: numpy::PyReadonlyArray1<'py, f64>,
1620 fixed_period_range: (usize, usize, usize),
1621 cycle_mult_range: (f64, f64, f64),
1622 tr_mult_range: (f64, f64, f64),
1623 cutoff_type: &str,
1624 max_cycle_limit: usize,
1625 kernel: Option<&str>,
1626) -> PyResult<Bound<'py, pyo3::types::PyDict>> {
1627 use numpy::{IntoPyArray, PyArray1, PyArrayMethods};
1628 let h = high.as_slice()?;
1629 let l = low.as_slice()?;
1630 let c = close.as_slice()?;
1631 let s = src.as_slice()?;
1632 if h.len() != s.len() || l.len() != s.len() || c.len() != s.len() {
1633 return Err(PyValueError::new_err(
1634 "All arrays must have the same length",
1635 ));
1636 }
1637
1638 let sweep = LpcBatchRange {
1639 fixed_period: fixed_period_range,
1640 cycle_mult: cycle_mult_range,
1641 tr_mult: tr_mult_range,
1642 cutoff_type: cutoff_type.to_string(),
1643 max_cycle_limit,
1644 };
1645 let combos = expand_grid_lpc(&sweep).map_err(|e| PyValueError::new_err(e.to_string()))?;
1646 let rows = combos.len() * 3;
1647 let cols = s.len();
1648
1649 let kern = validate_kernel(kernel, true)?;
1650 let first = (0..s.len())
1651 .find(|&i| !s[i].is_nan() && !h[i].is_nan() && !l[i].is_nan() && !c[i].is_nan())
1652 .unwrap_or(0);
1653
1654 let out_arr = unsafe { PyArray1::<f64>::new(py, [rows * cols], false) };
1655 let slice_out = unsafe { out_arr.as_slice_mut()? };
1656
1657 for row in 0..rows {
1658 for col in 0..first {
1659 slice_out[row * cols + col] = f64::NAN;
1660 }
1661 }
1662
1663 py.allow_threads(|| {
1664 lpc_batch_inner_into(h, l, c, s, &sweep, kern, first, slice_out)
1665 .map_err(|e| PyValueError::new_err(e.to_string()))
1666 })?;
1667
1668 let dict = pyo3::types::PyDict::new(py);
1669 dict.set_item("values", out_arr.reshape((rows, cols))?)?;
1670 dict.set_item(
1671 "fixed_periods",
1672 combos
1673 .iter()
1674 .map(|p| p.fixed_period.unwrap() as u64)
1675 .collect::<Vec<_>>()
1676 .into_pyarray(py),
1677 )?;
1678 dict.set_item(
1679 "cycle_mults",
1680 combos
1681 .iter()
1682 .map(|p| p.cycle_mult.unwrap())
1683 .collect::<Vec<_>>()
1684 .into_pyarray(py),
1685 )?;
1686 dict.set_item(
1687 "tr_mults",
1688 combos
1689 .iter()
1690 .map(|p| p.tr_mult.unwrap())
1691 .collect::<Vec<_>>()
1692 .into_pyarray(py),
1693 )?;
1694
1695 let order_list = PyList::new(py, vec!["filter", "high", "low"])?;
1696 dict.set_item("order", order_list)?;
1697 dict.set_item("rows", rows)?;
1698 dict.set_item("cols", cols)?;
1699 Ok(dict)
1700}
1701
1702#[cfg(feature = "python")]
1703pub fn register_lpc_module(m: &Bound<'_, pyo3::types::PyModule>) -> PyResult<()> {
1704 m.add_function(wrap_pyfunction!(lpc_py, m)?)?;
1705 m.add_function(wrap_pyfunction!(lpc_batch_py, m)?)?;
1706 #[cfg(feature = "cuda")]
1707 {
1708 m.add_function(wrap_pyfunction!(lpc_cuda_batch_dev_py, m)?)?;
1709 m.add_function(wrap_pyfunction!(lpc_cuda_many_series_one_param_dev_py, m)?)?;
1710 }
1711 Ok(())
1712}
1713
1714#[cfg(all(feature = "python", feature = "cuda"))]
1715use crate::cuda::cuda_available as cuda_is_available;
1716#[cfg(all(feature = "python", feature = "cuda"))]
1717use crate::cuda::lpc_wrapper::CudaLpc;
1718#[cfg(all(feature = "python", feature = "cuda"))]
1719use crate::indicators::moving_averages::alma::DeviceArrayF32Py;
1720
1721#[cfg(all(feature = "python", feature = "cuda"))]
1722#[pyfunction(name = "lpc_cuda_batch_dev")]
1723#[pyo3(signature = (high_f32, low_f32, close_f32, src_f32, fixed_period_range, cycle_mult_range, tr_mult_range, cutoff_type="fixed", max_cycle_limit=60, device_id=0))]
1724pub fn lpc_cuda_batch_dev_py<'py>(
1725 py: Python<'py>,
1726 high_f32: numpy::PyReadonlyArray1<'py, f32>,
1727 low_f32: numpy::PyReadonlyArray1<'py, f32>,
1728 close_f32: numpy::PyReadonlyArray1<'py, f32>,
1729 src_f32: numpy::PyReadonlyArray1<'py, f32>,
1730 fixed_period_range: (usize, usize, usize),
1731 cycle_mult_range: (f64, f64, f64),
1732 tr_mult_range: (f64, f64, f64),
1733 cutoff_type: &str,
1734 max_cycle_limit: usize,
1735 device_id: usize,
1736) -> PyResult<Bound<'py, pyo3::types::PyDict>> {
1737 use numpy::IntoPyArray;
1738 if !cuda_is_available() {
1739 return Err(PyValueError::new_err("CUDA not available"));
1740 }
1741 let h = high_f32.as_slice()?;
1742 let l = low_f32.as_slice()?;
1743 let c = close_f32.as_slice()?;
1744 let s = src_f32.as_slice()?;
1745 if h.len() != s.len() || l.len() != s.len() || c.len() != s.len() {
1746 return Err(PyValueError::new_err(
1747 "All arrays must have the same length",
1748 ));
1749 }
1750 let sweep = LpcBatchRange {
1751 fixed_period: fixed_period_range,
1752 cycle_mult: cycle_mult_range,
1753 tr_mult: tr_mult_range,
1754 cutoff_type: cutoff_type.to_string(),
1755 max_cycle_limit,
1756 };
1757 let (triplet, combos, ctx, dev_id) = py.allow_threads(|| {
1758 let cuda = CudaLpc::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
1759 let ctx = cuda.context_arc();
1760 let dev_id = cuda.device_id();
1761 let (triplet, combos) = cuda
1762 .lpc_batch_dev(h, l, c, s, &sweep)
1763 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1764 cuda.synchronize()
1765 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1766 Ok::<_, PyErr>((triplet, combos, ctx, dev_id))
1767 })?;
1768 let d = pyo3::types::PyDict::new(py);
1769 d.set_item(
1770 "filter",
1771 DeviceArrayF32Py {
1772 inner: triplet.wt1,
1773 _ctx: Some(ctx.clone()),
1774 device_id: Some(dev_id),
1775 },
1776 )?;
1777 d.set_item(
1778 "high",
1779 DeviceArrayF32Py {
1780 inner: triplet.wt2,
1781 _ctx: Some(ctx.clone()),
1782 device_id: Some(dev_id),
1783 },
1784 )?;
1785 d.set_item(
1786 "low",
1787 DeviceArrayF32Py {
1788 inner: triplet.hist,
1789 _ctx: Some(ctx),
1790 device_id: Some(dev_id),
1791 },
1792 )?;
1793 d.set_item(
1794 "fixed_periods",
1795 combos
1796 .iter()
1797 .map(|p| p.fixed_period.unwrap() as u64)
1798 .collect::<Vec<_>>()
1799 .into_pyarray(py),
1800 )?;
1801 d.set_item(
1802 "cycle_mults",
1803 combos
1804 .iter()
1805 .map(|p| p.cycle_mult.unwrap())
1806 .collect::<Vec<_>>()
1807 .into_pyarray(py),
1808 )?;
1809 d.set_item(
1810 "tr_mults",
1811 combos
1812 .iter()
1813 .map(|p| p.tr_mult.unwrap())
1814 .collect::<Vec<_>>()
1815 .into_pyarray(py),
1816 )?;
1817 d.set_item("rows", combos.len())?;
1818 d.set_item("cols", s.len())?;
1819 Ok(d)
1820}
1821
1822#[cfg(all(feature = "python", feature = "cuda"))]
1823#[pyfunction(name = "lpc_cuda_many_series_one_param_dev")]
1824#[pyo3(signature = (high_tm_f32, low_tm_f32, close_tm_f32, src_tm_f32, cutoff_type="fixed", fixed_period=20, tr_mult=1.0, device_id=0))]
1825pub fn lpc_cuda_many_series_one_param_dev_py<'py>(
1826 py: Python<'py>,
1827 high_tm_f32: numpy::PyReadonlyArray2<'py, f32>,
1828 low_tm_f32: numpy::PyReadonlyArray2<'py, f32>,
1829 close_tm_f32: numpy::PyReadonlyArray2<'py, f32>,
1830 src_tm_f32: numpy::PyReadonlyArray2<'py, f32>,
1831 cutoff_type: &str,
1832 fixed_period: usize,
1833 tr_mult: f64,
1834 device_id: usize,
1835) -> PyResult<Bound<'py, pyo3::types::PyDict>> {
1836 if !cuda_is_available() {
1837 return Err(PyValueError::new_err("CUDA not available"));
1838 }
1839 if !cutoff_type.eq_ignore_ascii_case("fixed") {
1840 return Err(PyValueError::new_err(
1841 "many-series CUDA supports fixed cutoff only",
1842 ));
1843 }
1844 let sh = high_tm_f32.shape();
1845 let sl = low_tm_f32.shape();
1846 let sc = close_tm_f32.shape();
1847 let ss = src_tm_f32.shape();
1848 if sh != sl || sh != sc || sh != ss || sh.len() != 2 {
1849 return Err(PyValueError::new_err(
1850 "expected matching 2D arrays [rows, cols]",
1851 ));
1852 }
1853 let rows = sh[0];
1854 let cols = sh[1];
1855 let h = high_tm_f32.as_slice()?;
1856 let l = low_tm_f32.as_slice()?;
1857 let c = close_tm_f32.as_slice()?;
1858 let s = src_tm_f32.as_slice()?;
1859 let params = LpcParams {
1860 cutoff_type: Some(cutoff_type.to_string()),
1861 fixed_period: Some(fixed_period),
1862 max_cycle_limit: Some(60),
1863 cycle_mult: Some(1.0),
1864 tr_mult: Some(tr_mult),
1865 };
1866 let (triplet, ctx, dev_id) = py.allow_threads(|| {
1867 let cuda = CudaLpc::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
1868 let ctx = cuda.context_arc();
1869 let dev_id = cuda.device_id();
1870 let triplet = cuda
1871 .lpc_many_series_one_param_time_major_dev(h, l, c, s, cols, rows, ¶ms)
1872 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1873 cuda.synchronize()
1874 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1875 Ok::<_, PyErr>((triplet, ctx, dev_id))
1876 })?;
1877 let d = pyo3::types::PyDict::new(py);
1878 d.set_item(
1879 "filter",
1880 DeviceArrayF32Py {
1881 inner: triplet.wt1,
1882 _ctx: Some(ctx.clone()),
1883 device_id: Some(dev_id),
1884 },
1885 )?;
1886 d.set_item(
1887 "high",
1888 DeviceArrayF32Py {
1889 inner: triplet.wt2,
1890 _ctx: Some(ctx.clone()),
1891 device_id: Some(dev_id),
1892 },
1893 )?;
1894 d.set_item(
1895 "low",
1896 DeviceArrayF32Py {
1897 inner: triplet.hist,
1898 _ctx: Some(ctx),
1899 device_id: Some(dev_id),
1900 },
1901 )?;
1902 d.set_item("rows", rows)?;
1903 d.set_item("cols", cols)?;
1904 d.set_item("fixed_period", fixed_period)?;
1905 d.set_item("tr_mult", tr_mult)?;
1906 Ok(d)
1907}
1908
1909#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1910#[derive(Serialize, Deserialize)]
1911struct LpcResult {
1912 filter: Vec<f64>,
1913 high_band: Vec<f64>,
1914 low_band: Vec<f64>,
1915}
1916
1917#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1918#[wasm_bindgen]
1919pub fn lpc_alloc(len: usize) -> *mut f64 {
1920 let mut v = Vec::<f64>::with_capacity(len);
1921 let p = v.as_mut_ptr();
1922 std::mem::forget(v);
1923 p
1924}
1925
1926#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1927#[wasm_bindgen]
1928pub fn lpc_free(ptr: *mut f64, len: usize) {
1929 unsafe {
1930 let _ = Vec::from_raw_parts(ptr, len, len);
1931 }
1932}
1933
1934#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1935#[wasm_bindgen]
1936pub fn lpc_into(
1937 high_ptr: *const f64,
1938 low_ptr: *const f64,
1939 close_ptr: *const f64,
1940 src_ptr: *const f64,
1941 filter_out_ptr: *mut f64,
1942 high_out_ptr: *mut f64,
1943 low_out_ptr: *mut f64,
1944 len: usize,
1945 cutoff_type: &str,
1946 fixed_period: usize,
1947 max_cycle_limit: usize,
1948 cycle_mult: f64,
1949 tr_mult: f64,
1950) -> Result<(), JsValue> {
1951 if high_ptr.is_null()
1952 || low_ptr.is_null()
1953 || close_ptr.is_null()
1954 || src_ptr.is_null()
1955 || filter_out_ptr.is_null()
1956 || high_out_ptr.is_null()
1957 || low_out_ptr.is_null()
1958 {
1959 return Err(JsValue::from_str("null pointer passed to lpc_into"));
1960 }
1961
1962 unsafe {
1963 let h = std::slice::from_raw_parts(high_ptr, len);
1964 let l = std::slice::from_raw_parts(low_ptr, len);
1965 let c = std::slice::from_raw_parts(close_ptr, len);
1966 let s = std::slice::from_raw_parts(src_ptr, len);
1967
1968 let params = LpcParams {
1969 cutoff_type: Some(cutoff_type.to_string()),
1970 fixed_period: Some(fixed_period),
1971 max_cycle_limit: Some(max_cycle_limit),
1972 cycle_mult: Some(cycle_mult),
1973 tr_mult: Some(tr_mult),
1974 };
1975 let input = LpcInput::from_slices(h, l, c, s, params);
1976
1977 let alias = filter_out_ptr as *const f64 == high_ptr
1978 || filter_out_ptr as *const f64 == low_ptr
1979 || filter_out_ptr as *const f64 == close_ptr
1980 || filter_out_ptr as *const f64 == src_ptr
1981 || high_out_ptr as *const f64 == high_ptr
1982 || high_out_ptr as *const f64 == low_ptr
1983 || high_out_ptr as *const f64 == close_ptr
1984 || high_out_ptr as *const f64 == src_ptr
1985 || low_out_ptr as *const f64 == high_ptr
1986 || low_out_ptr as *const f64 == low_ptr
1987 || low_out_ptr as *const f64 == close_ptr
1988 || low_out_ptr as *const f64 == src_ptr;
1989
1990 if alias {
1991 let mut f = vec![0.0; len];
1992 let mut hb = vec![0.0; len];
1993 let mut lb = vec![0.0; len];
1994 lpc_into_slices(&mut f, &mut hb, &mut lb, &input, Kernel::Auto)
1995 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1996 std::slice::from_raw_parts_mut(filter_out_ptr, len).copy_from_slice(&f);
1997 std::slice::from_raw_parts_mut(high_out_ptr, len).copy_from_slice(&hb);
1998 std::slice::from_raw_parts_mut(low_out_ptr, len).copy_from_slice(&lb);
1999 } else {
2000 let f = std::slice::from_raw_parts_mut(filter_out_ptr, len);
2001 let hb = std::slice::from_raw_parts_mut(high_out_ptr, len);
2002 let lb = std::slice::from_raw_parts_mut(low_out_ptr, len);
2003 lpc_into_slices(f, hb, lb, &input, Kernel::Auto)
2004 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2005 }
2006 Ok(())
2007 }
2008}
2009
2010#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2011#[wasm_bindgen]
2012pub fn lpc_wasm(
2013 high: &[f64],
2014 low: &[f64],
2015 close: &[f64],
2016 src: &[f64],
2017 cutoff_type: &str,
2018 fixed_period: usize,
2019 max_cycle_limit: usize,
2020 cycle_mult: f64,
2021 tr_mult: f64,
2022) -> Result<JsValue, JsValue> {
2023 let params = LpcParams {
2024 cutoff_type: Some(cutoff_type.to_string()),
2025 fixed_period: Some(fixed_period),
2026 max_cycle_limit: Some(max_cycle_limit),
2027 cycle_mult: Some(cycle_mult),
2028 tr_mult: Some(tr_mult),
2029 };
2030
2031 let input = LpcInput::from_slices(high, low, close, src, params);
2032
2033 match lpc(&input) {
2034 Ok(output) => {
2035 let result = LpcResult {
2036 filter: output.filter,
2037 high_band: output.high_band,
2038 low_band: output.low_band,
2039 };
2040 serde_wasm_bindgen::to_value(&result).map_err(|e| JsValue::from_str(&e.to_string()))
2041 }
2042 Err(e) => Err(JsValue::from_str(&e.to_string())),
2043 }
2044}
2045
2046#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2047#[derive(Serialize, Deserialize)]
2048pub struct LpcJsOutput {
2049 pub values: Vec<f64>,
2050 pub rows: usize,
2051 pub cols: usize,
2052}
2053
2054#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2055#[wasm_bindgen(js_name = lpc)]
2056pub fn lpc_js(
2057 high: &[f64],
2058 low: &[f64],
2059 close: &[f64],
2060 src: &[f64],
2061 cutoff_type: &str,
2062 fixed_period: usize,
2063 max_cycle_limit: usize,
2064 cycle_mult: f64,
2065 tr_mult: f64,
2066) -> Result<Vec<f64>, JsValue> {
2067 let params = LpcParams {
2068 cutoff_type: Some(cutoff_type.to_string()),
2069 fixed_period: Some(fixed_period),
2070 max_cycle_limit: Some(max_cycle_limit),
2071 cycle_mult: Some(cycle_mult),
2072 tr_mult: Some(tr_mult),
2073 };
2074 let input = LpcInput::from_slices(high, low, close, src, params);
2075 let out = lpc(&input).map_err(|e| JsValue::from_str(&e.to_string()))?;
2076 let len = src.len();
2077 let mut values = Vec::with_capacity(3 * len);
2078 values.extend_from_slice(&out.filter);
2079 values.extend_from_slice(&out.high_band);
2080 values.extend_from_slice(&out.low_band);
2081 Ok(values)
2082}
2083
2084#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2085#[derive(Serialize, Deserialize)]
2086pub struct LpcBatchConfig {
2087 pub fixed_period_range: (usize, usize, usize),
2088 pub cycle_mult_range: (f64, f64, f64),
2089 pub tr_mult_range: (f64, f64, f64),
2090 pub cutoff_type: String,
2091 pub max_cycle_limit: usize,
2092}
2093
2094#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2095#[derive(Serialize, Deserialize)]
2096pub struct LpcBatchJsOutput {
2097 pub values: Vec<Vec<f64>>,
2098 pub fixed_periods: Vec<usize>,
2099 pub cycle_mults: Vec<f64>,
2100 pub tr_mults: Vec<f64>,
2101 pub rows: usize,
2102 pub cols: usize,
2103 pub order: Vec<String>,
2104}
2105
2106#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2107#[wasm_bindgen(js_name = lpc_batch)]
2108pub fn lpc_batch_unified_js(
2109 high: &[f64],
2110 low: &[f64],
2111 close: &[f64],
2112 src: &[f64],
2113 config: JsValue,
2114) -> Result<JsValue, JsValue> {
2115 let cfg: LpcBatchConfig = serde_wasm_bindgen::from_value(config)
2116 .map_err(|e| JsValue::from_str(&format!("Invalid config: {e}")))?;
2117 let sweep = LpcBatchRange {
2118 fixed_period: cfg.fixed_period_range,
2119 cycle_mult: cfg.cycle_mult_range,
2120 tr_mult: cfg.tr_mult_range,
2121 cutoff_type: cfg.cutoff_type,
2122 max_cycle_limit: cfg.max_cycle_limit,
2123 };
2124 let out = lpc_batch_with_kernel(high, low, close, src, &sweep, Kernel::Auto)
2125 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2126
2127 let mut values_2d = Vec::with_capacity(out.rows);
2128 for i in 0..out.rows {
2129 let start = i * out.cols;
2130 let end = start + out.cols;
2131 values_2d.push(out.values[start..end].to_vec());
2132 }
2133
2134 let num_combos = out.combos.len();
2135 let mut fixed_periods = Vec::with_capacity(num_combos);
2136 let mut cycle_mults = Vec::with_capacity(num_combos);
2137 let mut tr_mults = Vec::with_capacity(num_combos);
2138
2139 for combo in &out.combos {
2140 fixed_periods.push(combo.fixed_period.unwrap());
2141 cycle_mults.push(combo.cycle_mult.unwrap());
2142 tr_mults.push(combo.tr_mult.unwrap());
2143 }
2144
2145 let js = LpcBatchJsOutput {
2146 values: values_2d,
2147 fixed_periods,
2148 cycle_mults,
2149 tr_mults,
2150 rows: out.rows,
2151 cols: out.cols,
2152 order: vec!["filter".to_string(), "high".to_string(), "low".to_string()],
2153 };
2154 serde_wasm_bindgen::to_value(&js)
2155 .map_err(|e| JsValue::from_str(&format!("Serialization error: {e}")))
2156}
2157
2158#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2159#[wasm_bindgen]
2160pub fn lpc_batch_into(
2161 high_ptr: *const f64,
2162 low_ptr: *const f64,
2163 close_ptr: *const f64,
2164 src_ptr: *const f64,
2165 out_ptr: *mut f64,
2166 len: usize,
2167 fixed_start: usize,
2168 fixed_end: usize,
2169 fixed_step: usize,
2170 cm_start: f64,
2171 cm_end: f64,
2172 cm_step: f64,
2173 tm_start: f64,
2174 tm_end: f64,
2175 tm_step: f64,
2176 cutoff_type: &str,
2177 max_cycle_limit: usize,
2178) -> Result<usize, JsValue> {
2179 if [high_ptr, low_ptr, close_ptr, src_ptr, out_ptr]
2180 .iter()
2181 .any(|&p| p.is_null())
2182 {
2183 return Err(JsValue::from_str("null pointer passed to lpc_batch_into"));
2184 }
2185 unsafe {
2186 let h = std::slice::from_raw_parts(high_ptr, len);
2187 let l = std::slice::from_raw_parts(low_ptr, len);
2188 let c = std::slice::from_raw_parts(close_ptr, len);
2189 let s = std::slice::from_raw_parts(src_ptr, len);
2190
2191 let sweep = LpcBatchRange {
2192 fixed_period: (fixed_start, fixed_end, fixed_step),
2193 cycle_mult: (cm_start, cm_end, cm_step),
2194 tr_mult: (tm_start, tm_end, tm_step),
2195 cutoff_type: cutoff_type.to_string(),
2196 max_cycle_limit,
2197 };
2198 let combos = expand_grid_lpc(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
2199 let rows = combos.len().checked_mul(3).ok_or_else(|| {
2200 JsValue::from_str(
2201 &LpcError::InvalidRange {
2202 start: fixed_start,
2203 end: fixed_end,
2204 step: fixed_step,
2205 }
2206 .to_string(),
2207 )
2208 })?;
2209 let cols = len;
2210
2211 let total = rows.checked_mul(cols).ok_or_else(|| {
2212 JsValue::from_str(
2213 &LpcError::InvalidRange {
2214 start: fixed_start,
2215 end: fixed_end,
2216 step: fixed_step,
2217 }
2218 .to_string(),
2219 )
2220 })?;
2221
2222 let out = std::slice::from_raw_parts_mut(out_ptr, total);
2223 let first = (0..len)
2224 .find(|&i| !s[i].is_nan() && !h[i].is_nan() && !l[i].is_nan() && !c[i].is_nan())
2225 .unwrap_or(0);
2226
2227 for row in 0..rows {
2228 for col in 0..first {
2229 out[row * cols + col] = f64::NAN;
2230 }
2231 }
2232
2233 lpc_batch_inner_into(
2234 h,
2235 l,
2236 c,
2237 s,
2238 &sweep,
2239 crate::utilities::enums::Kernel::Auto,
2240 first,
2241 out,
2242 )
2243 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2244 Ok(rows)
2245 }
2246}
2247
2248#[inline]
2249pub fn lpc_into_slices(
2250 filter_dst: &mut [f64],
2251 high_band_dst: &mut [f64],
2252 low_band_dst: &mut [f64],
2253 input: &LpcInput,
2254 kern: Kernel,
2255) -> Result<(), LpcError> {
2256 let (h, l, c, s, cutoff, fp, mcl, cm, tm, first, _chosen) = lpc_prepare(input, kern)?;
2257 let n = s.len();
2258 if filter_dst.len() != n || high_band_dst.len() != n || low_band_dst.len() != n {
2259 return Err(LpcError::OutputLengthMismatch {
2260 expected: n,
2261 got: filter_dst.len(),
2262 });
2263 }
2264
2265 if first > 0 {
2266 let qnan = f64::from_bits(0x7ff8_0000_0000_0000);
2267 let w = first.min(n);
2268 for v in &mut filter_dst[..w] {
2269 *v = qnan;
2270 }
2271 for v in &mut high_band_dst[..w] {
2272 *v = qnan;
2273 }
2274 for v in &mut low_band_dst[..w] {
2275 *v = qnan;
2276 }
2277 }
2278 lpc_compute_into(
2279 h,
2280 l,
2281 c,
2282 s,
2283 &cutoff,
2284 fp,
2285 mcl,
2286 cm,
2287 tm,
2288 first,
2289 kern,
2290 filter_dst,
2291 high_band_dst,
2292 low_band_dst,
2293 );
2294 Ok(())
2295}
2296
2297#[inline]
2298fn axis_usize((start, end, step): (usize, usize, usize)) -> Result<Vec<usize>, LpcError> {
2299 if step == 0 || start == end {
2300 return Ok(vec![start]);
2301 }
2302 let mut vals = Vec::new();
2303 if start < end {
2304 let mut v = start;
2305 while v <= end {
2306 vals.push(v);
2307 match v.checked_add(step) {
2308 Some(next) => {
2309 if next == v {
2310 break;
2311 }
2312 v = next;
2313 }
2314 None => break,
2315 }
2316 }
2317 } else {
2318 let mut v = start;
2319 while v >= end {
2320 vals.push(v);
2321 if v == 0 {
2322 break;
2323 }
2324 let next = v.saturating_sub(step);
2325 if next == v {
2326 break;
2327 }
2328 v = next;
2329 if v < end {
2330 break;
2331 }
2332 }
2333 }
2334 if vals.is_empty() {
2335 return Err(LpcError::InvalidRange { start, end, step });
2336 }
2337 Ok(vals)
2338}
2339
2340#[inline]
2341fn axis_f64((start, end, step): (f64, f64, f64)) -> Result<Vec<f64>, LpcError> {
2342 if step.abs() < 1e-12 || (start - end).abs() < 1e-12 {
2343 return Ok(vec![start]);
2344 }
2345 let mut out = Vec::new();
2346 if start < end {
2347 let st = if step > 0.0 { step } else { -step };
2348 let mut x = start;
2349 while x <= end + 1e-12 {
2350 out.push(x);
2351 x += st;
2352 }
2353 } else {
2354 let st = if step > 0.0 { -step } else { step };
2355 if st.abs() < 1e-12 {
2356 return Ok(vec![start]);
2357 }
2358 let mut x = start;
2359 while x >= end - 1e-12 {
2360 out.push(x);
2361 x += st;
2362 }
2363 }
2364 if out.is_empty() {
2365 return Err(LpcError::InvalidRange {
2366 start: start as usize,
2367 end: end as usize,
2368 step: step as usize,
2369 });
2370 }
2371 Ok(out)
2372}
2373
2374#[inline]
2375fn expand_grid_lpc(r: &LpcBatchRange) -> Result<Vec<LpcParams>, LpcError> {
2376 let ps = axis_usize(r.fixed_period)?;
2377 let cms = axis_f64(r.cycle_mult)?;
2378 let tms = axis_f64(r.tr_mult)?;
2379 let cap = ps
2380 .len()
2381 .checked_mul(cms.len())
2382 .and_then(|v| v.checked_mul(tms.len()))
2383 .ok_or(LpcError::InvalidRange {
2384 start: r.fixed_period.0,
2385 end: r.fixed_period.1,
2386 step: r.fixed_period.2,
2387 })?;
2388 let mut out = Vec::with_capacity(cap);
2389 for &p in &ps {
2390 for &cm in &cms {
2391 for &tm in &tms {
2392 out.push(LpcParams {
2393 cutoff_type: Some(r.cutoff_type.clone()),
2394 fixed_period: Some(p),
2395 max_cycle_limit: Some(r.max_cycle_limit),
2396 cycle_mult: Some(cm),
2397 tr_mult: Some(tm),
2398 });
2399 }
2400 }
2401 }
2402 Ok(out)
2403}
2404
2405pub fn lpc_batch_with_kernel(
2406 high: &[f64],
2407 low: &[f64],
2408 close: &[f64],
2409 src: &[f64],
2410 sweep: &LpcBatchRange,
2411 k: Kernel,
2412) -> Result<LpcBatchOutput, LpcError> {
2413 if src.is_empty() {
2414 return Err(LpcError::EmptyInputData);
2415 }
2416 if high.len() != src.len() || low.len() != src.len() || close.len() != src.len() {
2417 return Err(LpcError::MissingData);
2418 }
2419
2420 let first = (0..src.len())
2421 .find(|&i| !src[i].is_nan() && !high[i].is_nan() && !low[i].is_nan() && !close[i].is_nan())
2422 .ok_or(LpcError::AllValuesNaN)?;
2423 if src.len().saturating_sub(first) < 2 {
2424 return Err(LpcError::NotEnoughValidData {
2425 needed: 2,
2426 valid: src.len().saturating_sub(first),
2427 });
2428 }
2429
2430 let combos = expand_grid_lpc(sweep)?;
2431 let cols = src.len();
2432 let rows = combos.len().checked_mul(3).ok_or(LpcError::InvalidRange {
2433 start: sweep.fixed_period.0,
2434 end: sweep.fixed_period.1,
2435 step: sweep.fixed_period.2,
2436 })?;
2437 rows.checked_mul(cols).ok_or(LpcError::InvalidRange {
2438 start: sweep.fixed_period.0,
2439 end: sweep.fixed_period.1,
2440 step: sweep.fixed_period.2,
2441 })?;
2442
2443 let kernel = match k {
2444 Kernel::Auto => detect_best_batch_kernel(),
2445 other if other.is_batch() => other,
2446 other => return Err(LpcError::InvalidKernelForBatch(other)),
2447 };
2448
2449 let mut buf_mu = make_uninit_matrix(rows, cols);
2450 let warm = vec![first; rows];
2451 init_matrix_prefixes(&mut buf_mu, cols, &warm);
2452
2453 let mut guard = core::mem::ManuallyDrop::new(buf_mu);
2454 let out: &mut [f64] =
2455 unsafe { core::slice::from_raw_parts_mut(guard.as_mut_ptr() as *mut f64, guard.len()) };
2456
2457 lpc_batch_inner_into(high, low, close, src, sweep, kernel, first, out)?;
2458
2459 let values = unsafe {
2460 Vec::from_raw_parts(
2461 guard.as_mut_ptr() as *mut f64,
2462 guard.len(),
2463 guard.capacity(),
2464 )
2465 };
2466
2467 Ok(LpcBatchOutput {
2468 values,
2469 combos,
2470 rows,
2471 cols,
2472 })
2473}
2474
2475fn lpc_batch_inner_into(
2476 high: &[f64],
2477 low: &[f64],
2478 close: &[f64],
2479 src: &[f64],
2480 sweep: &LpcBatchRange,
2481 k: Kernel,
2482 first: usize,
2483 out: &mut [f64],
2484) -> Result<(), LpcError> {
2485 let _ = k;
2486 let combos = expand_grid_lpc(sweep)?;
2487 let cols = src.len();
2488
2489 let tr_series = calculate_true_range(high, low, close);
2490
2491 let out_mu = unsafe {
2492 std::slice::from_raw_parts_mut(out.as_mut_ptr() as *mut MaybeUninit<f64>, out.len())
2493 };
2494
2495 let do_row = |combo_idx: usize, dst3: &mut [MaybeUninit<f64>]| {
2496 let params = &combos[combo_idx];
2497 let mut rowslice = |k: usize| -> &mut [f64] {
2498 let start = k * cols;
2499 unsafe {
2500 core::slice::from_raw_parts_mut(dst3.as_mut_ptr().add(start) as *mut f64, cols)
2501 }
2502 };
2503 let (f_dst, h_dst, l_dst) = (rowslice(0), rowslice(1), rowslice(2));
2504
2505 lpc_compute_into_prefilled_pretr(
2506 high,
2507 low,
2508 close,
2509 src,
2510 &tr_series,
2511 params.cutoff_type.as_ref().unwrap(),
2512 params.fixed_period.unwrap(),
2513 params.max_cycle_limit.unwrap(),
2514 params.cycle_mult.unwrap(),
2515 params.tr_mult.unwrap(),
2516 first,
2517 f_dst,
2518 h_dst,
2519 l_dst,
2520 );
2521 };
2522
2523 #[cfg(not(target_arch = "wasm32"))]
2524 {
2525 out_mu
2526 .par_chunks_mut(3 * cols)
2527 .enumerate()
2528 .for_each(|(combo_idx, chunk)| do_row(combo_idx, chunk));
2529 }
2530 #[cfg(target_arch = "wasm32")]
2531 {
2532 for (combo_idx, chunk) in out_mu.chunks_mut(3 * cols).enumerate() {
2533 do_row(combo_idx, chunk);
2534 }
2535 }
2536
2537 Ok(())
2538}
2539
2540pub fn lpc_batch(
2541 high: &[f64],
2542 low: &[f64],
2543 close: &[f64],
2544 src: &[f64],
2545 sweep: &LpcBatchRange,
2546) -> Result<LpcBatchOutput, LpcError> {
2547 lpc_batch_with_kernel(high, low, close, src, sweep, Kernel::Auto)
2548}
2549
2550pub fn lpc_batch_slice(
2551 high: &[f64],
2552 low: &[f64],
2553 close: &[f64],
2554 src: &[f64],
2555 sweep: &LpcBatchRange,
2556) -> Result<LpcBatchOutput, LpcError> {
2557 lpc_batch_with_kernel(high, low, close, src, sweep, detect_best_batch_kernel())
2558}
2559
2560#[cfg(not(target_arch = "wasm32"))]
2561pub fn lpc_batch_par_slice(
2562 high: &[f64],
2563 low: &[f64],
2564 close: &[f64],
2565 src: &[f64],
2566 sweep: &LpcBatchRange,
2567) -> Result<LpcBatchOutput, LpcError> {
2568 lpc_batch_with_kernel(high, low, close, src, sweep, detect_best_batch_kernel())
2569}
2570
2571#[cfg(test)]
2572mod tests {
2573 use super::*;
2574 use crate::skip_if_unsupported;
2575 use crate::utilities::data_loader::read_candles_from_csv;
2576 #[cfg(feature = "proptest")]
2577 use proptest::prelude::*;
2578
2579 #[test]
2580 fn test_lpc_into_matches_api() -> Result<(), Box<dyn Error>> {
2581 let n = 256usize;
2582 let warm = 8usize;
2583 let mut ts = Vec::with_capacity(n);
2584 let mut open = Vec::with_capacity(n);
2585 let mut high = Vec::with_capacity(n);
2586 let mut low = Vec::with_capacity(n);
2587 let mut close = Vec::with_capacity(n);
2588 let mut vol = Vec::with_capacity(n);
2589
2590 for i in 0..n {
2591 ts.push(i as i64);
2592 let base = 100.0 + 0.1 * (i as f64) + (i as f64 * 0.05).sin();
2593 if i < warm {
2594 open.push(f64::NAN);
2595 high.push(f64::NAN);
2596 low.push(f64::NAN);
2597 close.push(f64::NAN);
2598 } else {
2599 open.push(base - 0.2);
2600 high.push(base + 1.0);
2601 low.push(base - 1.0);
2602 close.push(base);
2603 }
2604 vol.push(1.0);
2605 }
2606
2607 let candles = crate::utilities::data_loader::Candles::new(
2608 ts,
2609 open,
2610 high.clone(),
2611 low.clone(),
2612 close.clone(),
2613 vol,
2614 );
2615 let input = LpcInput::from_candles(&candles, "close", LpcParams::default());
2616
2617 let baseline = lpc(&input)?;
2618
2619 let mut f = vec![0.0; n];
2620 let mut hb = vec![0.0; n];
2621 let mut lb = vec![0.0; n];
2622
2623 #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
2624 {
2625 lpc_into(&input, &mut f, &mut hb, &mut lb)?;
2626 }
2627 #[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2628 {
2629 lpc_into_slices(&mut f, &mut hb, &mut lb, &input, Kernel::Auto)?;
2630 }
2631
2632 assert_eq!(f.len(), baseline.filter.len());
2633 assert_eq!(hb.len(), baseline.high_band.len());
2634 assert_eq!(lb.len(), baseline.low_band.len());
2635
2636 fn eq_or_both_nan(a: f64, b: f64) -> bool {
2637 (a.is_nan() && b.is_nan()) || (a == b) || (a - b).abs() <= 1e-12
2638 }
2639
2640 for i in 0..n {
2641 assert!(
2642 eq_or_both_nan(f[i], baseline.filter[i]),
2643 "filter mismatch at {}: {} vs {}",
2644 i,
2645 f[i],
2646 baseline.filter[i]
2647 );
2648 assert!(
2649 eq_or_both_nan(hb[i], baseline.high_band[i]),
2650 "high band mismatch at {}: {} vs {}",
2651 i,
2652 hb[i],
2653 baseline.high_band[i]
2654 );
2655 assert!(
2656 eq_or_both_nan(lb[i], baseline.low_band[i]),
2657 "low band mismatch at {}: {} vs {}",
2658 i,
2659 lb[i],
2660 baseline.low_band[i]
2661 );
2662 }
2663 Ok(())
2664 }
2665
2666 fn check_lpc_accuracy(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2667 skip_if_unsupported!(kernel, test_name);
2668 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2669 let candles = read_candles_from_csv(file_path)?;
2670
2671 let params = LpcParams::default();
2672 let input = LpcInput::from_candles(&candles, "close", params);
2673 let result = lpc_with_kernel(&input, kernel)?;
2674
2675 let expected_filter = vec![
2676 59346.30519969,
2677 59327.59393858,
2678 59290.68770889,
2679 59257.83622820,
2680 59196.32617649,
2681 ];
2682
2683 let expected_high_band = vec![
2684 60351.08358296,
2685 60220.19604722,
2686 60090.66513329,
2687 59981.40792457,
2688 59903.93414995,
2689 ];
2690
2691 let expected_low_band = vec![
2692 58341.52681643,
2693 58434.99182994,
2694 58490.71028450,
2695 58534.26453184,
2696 58488.71820303,
2697 ];
2698
2699 let start_idx = result.filter.len() - 5;
2700 for i in 0..5 {
2701 let filter_diff = (result.filter[start_idx + i] - expected_filter[i]).abs();
2702 let high_diff = (result.high_band[start_idx + i] - expected_high_band[i]).abs();
2703 let low_diff = (result.low_band[start_idx + i] - expected_low_band[i]).abs();
2704
2705 assert!(
2706 filter_diff < 0.01,
2707 "[{}] LPC Filter {:?} mismatch at idx {}: got {}, expected {}",
2708 test_name,
2709 kernel,
2710 i,
2711 result.filter[start_idx + i],
2712 expected_filter[i]
2713 );
2714
2715 assert!(
2716 high_diff < 0.01,
2717 "[{}] LPC High Band {:?} mismatch at idx {}: got {}, expected {}",
2718 test_name,
2719 kernel,
2720 i,
2721 result.high_band[start_idx + i],
2722 expected_high_band[i]
2723 );
2724
2725 assert!(
2726 low_diff < 0.01,
2727 "[{}] LPC Low Band {:?} mismatch at idx {}: got {}, expected {}",
2728 test_name,
2729 kernel,
2730 i,
2731 result.low_band[start_idx + i],
2732 expected_low_band[i]
2733 );
2734 }
2735
2736 Ok(())
2737 }
2738
2739 fn check_lpc_partial_params(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2740 skip_if_unsupported!(kernel, test_name);
2741 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2742 let candles = read_candles_from_csv(file_path)?;
2743
2744 let params = LpcParams {
2745 cutoff_type: None,
2746 fixed_period: None,
2747 max_cycle_limit: None,
2748 cycle_mult: None,
2749 tr_mult: None,
2750 };
2751 let input = LpcInput::from_candles(&candles, "close", params);
2752 let output = lpc_with_kernel(&input, kernel)?;
2753 assert_eq!(output.filter.len(), candles.close.len());
2754
2755 Ok(())
2756 }
2757
2758 fn check_lpc_default_candles(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2759 skip_if_unsupported!(kernel, test_name);
2760 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2761 let candles = read_candles_from_csv(file_path)?;
2762
2763 let input = LpcInput::with_default_candles(&candles);
2764 match input.data {
2765 LpcData::Candles { source, .. } => assert_eq!(source, "close"),
2766 _ => panic!("Expected LpcData::Candles"),
2767 }
2768 let output = lpc_with_kernel(&input, kernel)?;
2769 assert_eq!(output.filter.len(), candles.close.len());
2770
2771 Ok(())
2772 }
2773
2774 fn check_lpc_zero_period(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2775 skip_if_unsupported!(kernel, test_name);
2776 let data = vec![10.0, 20.0, 30.0];
2777 let params = LpcParams {
2778 cutoff_type: Some("fixed".to_string()),
2779 fixed_period: Some(0),
2780 max_cycle_limit: None,
2781 cycle_mult: None,
2782 tr_mult: None,
2783 };
2784 let input = LpcInput::from_slices(&data, &data, &data, &data, params);
2785 let res = lpc_with_kernel(&input, kernel);
2786 assert!(
2787 res.is_err(),
2788 "[{}] LPC should fail with zero period",
2789 test_name
2790 );
2791 Ok(())
2792 }
2793
2794 fn check_lpc_period_exceeds_length(
2795 test_name: &str,
2796 kernel: Kernel,
2797 ) -> Result<(), Box<dyn Error>> {
2798 skip_if_unsupported!(kernel, test_name);
2799 let data = vec![10.0, 20.0, 30.0];
2800 let params = LpcParams {
2801 cutoff_type: Some("fixed".to_string()),
2802 fixed_period: Some(10),
2803 max_cycle_limit: None,
2804 cycle_mult: None,
2805 tr_mult: None,
2806 };
2807 let input = LpcInput::from_slices(&data, &data, &data, &data, params);
2808 let res = lpc_with_kernel(&input, kernel);
2809 assert!(
2810 res.is_err(),
2811 "[{}] LPC should fail with period exceeding length",
2812 test_name
2813 );
2814 Ok(())
2815 }
2816
2817 fn check_lpc_very_small_dataset(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2818 skip_if_unsupported!(kernel, test_name);
2819 let single_point = vec![42.0];
2820 let params = LpcParams {
2821 cutoff_type: Some("fixed".to_string()),
2822 fixed_period: Some(20),
2823 max_cycle_limit: None,
2824 cycle_mult: None,
2825 tr_mult: None,
2826 };
2827 let input = LpcInput::from_slices(
2828 &single_point,
2829 &single_point,
2830 &single_point,
2831 &single_point,
2832 params,
2833 );
2834 let res = lpc_with_kernel(&input, kernel);
2835 assert!(
2836 res.is_err(),
2837 "[{}] LPC should fail with insufficient data",
2838 test_name
2839 );
2840 Ok(())
2841 }
2842
2843 fn check_lpc_empty_input(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2844 skip_if_unsupported!(kernel, test_name);
2845 let empty: Vec<f64> = vec![];
2846 let params = LpcParams::default();
2847 let input = LpcInput::from_slices(&empty, &empty, &empty, &empty, params);
2848
2849 let res = lpc_with_kernel(&input, kernel);
2850 assert!(
2851 matches!(res, Err(LpcError::EmptyInputData)),
2852 "[{}] LPC should fail with empty input",
2853 test_name
2854 );
2855 Ok(())
2856 }
2857
2858 fn check_lpc_invalid_cutoff_type(
2859 test_name: &str,
2860 kernel: Kernel,
2861 ) -> Result<(), Box<dyn Error>> {
2862 skip_if_unsupported!(kernel, test_name);
2863 let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
2864 let params = LpcParams {
2865 cutoff_type: Some("invalid".to_string()),
2866 fixed_period: Some(3),
2867 max_cycle_limit: None,
2868 cycle_mult: None,
2869 tr_mult: None,
2870 };
2871 let input = LpcInput::from_slices(&data, &data, &data, &data, params);
2872 let res = lpc_with_kernel(&input, kernel);
2873 assert!(
2874 matches!(res, Err(LpcError::InvalidCutoffType { .. })),
2875 "[{}] LPC should fail with invalid cutoff type",
2876 test_name
2877 );
2878 Ok(())
2879 }
2880
2881 fn check_lpc_all_nan(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2882 skip_if_unsupported!(kernel, test_name);
2883 let nan_data = vec![f64::NAN, f64::NAN, f64::NAN];
2884 let params = LpcParams::default();
2885 let input = LpcInput::from_slices(&nan_data, &nan_data, &nan_data, &nan_data, params);
2886
2887 let res = lpc_with_kernel(&input, kernel);
2888
2889 assert!(
2890 matches!(res, Err(LpcError::AllValuesNaN)),
2891 "[{}] LPC should fail with AllValuesNaN error",
2892 test_name
2893 );
2894 Ok(())
2895 }
2896
2897 fn check_lpc_reinput(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2898 skip_if_unsupported!(kernel, test_name);
2899 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2900 let candles = read_candles_from_csv(file_path)?;
2901
2902 let first_params = LpcParams {
2903 cutoff_type: Some("fixed".to_string()),
2904 fixed_period: Some(20),
2905 max_cycle_limit: None,
2906 cycle_mult: None,
2907 tr_mult: None,
2908 };
2909 let first_input = LpcInput::from_candles(&candles, "close", first_params);
2910 let first_result = lpc_with_kernel(&first_input, kernel)?;
2911
2912 let second_params = LpcParams {
2913 cutoff_type: Some("fixed".to_string()),
2914 fixed_period: Some(20),
2915 max_cycle_limit: None,
2916 cycle_mult: None,
2917 tr_mult: None,
2918 };
2919 let second_input = LpcInput::from_slices(
2920 &candles.high,
2921 &candles.low,
2922 &candles.close,
2923 &first_result.filter,
2924 second_params,
2925 );
2926 let second_result = lpc_with_kernel(&second_input, kernel)?;
2927
2928 assert_eq!(second_result.filter.len(), first_result.filter.len());
2929 Ok(())
2930 }
2931
2932 fn check_lpc_nan_handling(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2933 skip_if_unsupported!(kernel, test_name);
2934 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2935 let candles = read_candles_from_csv(file_path)?;
2936
2937 let input = LpcInput::from_candles(
2938 &candles,
2939 "close",
2940 LpcParams {
2941 cutoff_type: Some("fixed".to_string()),
2942 fixed_period: Some(20),
2943 max_cycle_limit: None,
2944 cycle_mult: None,
2945 tr_mult: None,
2946 },
2947 );
2948 let res = lpc_with_kernel(&input, kernel)?;
2949 assert_eq!(res.filter.len(), candles.close.len());
2950 if res.filter.len() > 240 {
2951 for (i, &val) in res.filter[240..].iter().enumerate() {
2952 assert!(
2953 !val.is_nan(),
2954 "[{}] Found unexpected NaN at out-index {}",
2955 test_name,
2956 240 + i
2957 );
2958 }
2959 }
2960 Ok(())
2961 }
2962
2963 fn check_lpc_streaming(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2964 skip_if_unsupported!(kernel, test_name);
2965
2966 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2967 let candles = read_candles_from_csv(file_path)?;
2968
2969 let cutoff_type = "fixed".to_string();
2970 let fixed_period = 20;
2971 let max_cycle_limit = 60;
2972 let cycle_mult = 1.0;
2973 let tr_mult = 1.0;
2974
2975 let input = LpcInput::from_candles(
2976 &candles,
2977 "close",
2978 LpcParams {
2979 cutoff_type: Some(cutoff_type.clone()),
2980 fixed_period: Some(fixed_period),
2981 max_cycle_limit: Some(max_cycle_limit),
2982 cycle_mult: Some(cycle_mult),
2983 tr_mult: Some(tr_mult),
2984 },
2985 );
2986 let batch_output = lpc_with_kernel(&input, kernel)?;
2987
2988 let mut stream = LpcStream::try_new(LpcParams {
2989 cutoff_type: Some(cutoff_type),
2990 fixed_period: Some(fixed_period),
2991 max_cycle_limit: Some(max_cycle_limit),
2992 cycle_mult: Some(cycle_mult),
2993 tr_mult: Some(tr_mult),
2994 })?;
2995
2996 let mut stream_filter = Vec::with_capacity(candles.close.len());
2997 let mut stream_high = Vec::with_capacity(candles.close.len());
2998 let mut stream_low = Vec::with_capacity(candles.close.len());
2999
3000 for i in 0..candles.close.len() {
3001 match stream.update(
3002 candles.high[i],
3003 candles.low[i],
3004 candles.close[i],
3005 candles.close[i],
3006 ) {
3007 Some((f, h, l)) => {
3008 stream_filter.push(f);
3009 stream_high.push(h);
3010 stream_low.push(l);
3011 }
3012 None => {
3013 stream_filter.push(f64::NAN);
3014 stream_high.push(f64::NAN);
3015 stream_low.push(f64::NAN);
3016 }
3017 }
3018 }
3019
3020 assert_eq!(batch_output.filter.len(), stream_filter.len());
3021
3022 for i in 20..100.min(stream_filter.len()) {
3023 if !stream_filter[i].is_nan() {
3024 assert!(
3025 stream_low[i] <= stream_filter[i] && stream_filter[i] <= stream_high[i],
3026 "[{}] Stream filter not between bands at idx {}",
3027 test_name,
3028 i
3029 );
3030 }
3031 }
3032 Ok(())
3033 }
3034
3035 #[cfg(debug_assertions)]
3036 fn check_lpc_no_poison(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
3037 skip_if_unsupported!(kernel, test_name);
3038
3039 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
3040 let candles = read_candles_from_csv(file_path)?;
3041
3042 let test_params = vec![
3043 LpcParams::default(),
3044 LpcParams {
3045 cutoff_type: Some("fixed".to_string()),
3046 fixed_period: Some(10),
3047 max_cycle_limit: Some(30),
3048 cycle_mult: Some(0.5),
3049 tr_mult: Some(0.5),
3050 },
3051 LpcParams {
3052 cutoff_type: Some("adaptive".to_string()),
3053 fixed_period: Some(20),
3054 max_cycle_limit: Some(60),
3055 cycle_mult: Some(1.0),
3056 tr_mult: Some(1.0),
3057 },
3058 LpcParams {
3059 cutoff_type: Some("fixed".to_string()),
3060 fixed_period: Some(50),
3061 max_cycle_limit: Some(100),
3062 cycle_mult: Some(2.0),
3063 tr_mult: Some(2.0),
3064 },
3065 ];
3066
3067 for (param_idx, params) in test_params.iter().enumerate() {
3068 let input = LpcInput::from_candles(&candles, "close", params.clone());
3069 let output = lpc_with_kernel(&input, kernel)?;
3070
3071 for i in 0..output.filter.len() {
3072 let f = output.filter[i];
3073 let hi = output.high_band[i];
3074 let lo = output.low_band[i];
3075
3076 for &val in &[f, hi, lo] {
3077 if val.is_nan() {
3078 continue;
3079 }
3080 let bits = val.to_bits();
3081 if bits == 0x11111111_11111111 {
3082 panic!("[{}] alloc_with_nan_prefix poison at {}", test_name, i);
3083 }
3084 if bits == 0x22222222_22222222 {
3085 panic!("[{}] init_matrix_prefixes poison at {}", test_name, i);
3086 }
3087 if bits == 0x33333333_33333333 {
3088 panic!("[{}] make_uninit_matrix poison at {}", test_name, i);
3089 }
3090 }
3091 }
3092 }
3093
3094 Ok(())
3095 }
3096
3097 #[cfg(not(debug_assertions))]
3098 fn check_lpc_no_poison(_test_name: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
3099 Ok(())
3100 }
3101
3102 #[cfg(feature = "proptest")]
3103 #[allow(clippy::float_cmp)]
3104 fn check_lpc_property(
3105 test_name: &str,
3106 kernel: Kernel,
3107 ) -> Result<(), Box<dyn std::error::Error>> {
3108 use proptest::prelude::*;
3109 skip_if_unsupported!(kernel, test_name);
3110
3111 let strat = (3usize..=50).prop_flat_map(|period| {
3112 (
3113 prop::collection::vec(
3114 (100.0f64..200.0f64).prop_filter("finite", |x| x.is_finite()),
3115 period..400,
3116 ),
3117 Just(period),
3118 0.5f64..2.0f64,
3119 0.5f64..2.0f64,
3120 )
3121 });
3122
3123 proptest::test_runner::TestRunner::default()
3124 .run(&strat, |(data, period, cycle_mult, tr_mult)| {
3125 let params = LpcParams {
3126 cutoff_type: Some("fixed".to_string()),
3127 fixed_period: Some(period),
3128 max_cycle_limit: Some(60),
3129 cycle_mult: Some(cycle_mult),
3130 tr_mult: Some(tr_mult),
3131 };
3132 let input = LpcInput::from_slices(&data, &data, &data, &data, params);
3133
3134 let result = lpc_with_kernel(&input, kernel).unwrap();
3135 let ref_result = lpc_with_kernel(&input, Kernel::Scalar).unwrap();
3136
3137 prop_assert_eq!(result.filter.len(), data.len());
3138 prop_assert_eq!(result.high_band.len(), data.len());
3139 prop_assert_eq!(result.low_band.len(), data.len());
3140
3141 let check_start = (period * 2).min(data.len());
3142 for i in check_start..data.len() {
3143 let f = result.filter[i];
3144 let h = result.high_band[i];
3145 let l = result.low_band[i];
3146
3147 if !f.is_nan() && !h.is_nan() && !l.is_nan() {
3148 prop_assert!(f.is_finite(), "filter at {i} not finite");
3149 prop_assert!(h.is_finite(), "high_band at {i} not finite");
3150 prop_assert!(l.is_finite(), "low_band at {i} not finite");
3151 }
3152
3153 if !f.is_nan() && !ref_result.filter[i].is_nan() {
3154 let diff = (f - ref_result.filter[i]).abs();
3155 prop_assert!(
3156 diff <= 1e-9,
3157 "mismatch idx {i}: {} vs {} (diff={})",
3158 f,
3159 ref_result.filter[i],
3160 diff
3161 );
3162 }
3163 }
3164 Ok(())
3165 })
3166 .unwrap();
3167
3168 Ok(())
3169 }
3170
3171 fn check_lpc_fixed_mode(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
3172 skip_if_unsupported!(kernel, test_name);
3173 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
3174 let candles = read_candles_from_csv(file_path)?;
3175
3176 let params = LpcParams {
3177 cutoff_type: Some("fixed".to_string()),
3178 fixed_period: Some(20),
3179 max_cycle_limit: Some(60),
3180 cycle_mult: Some(1.0),
3181 tr_mult: Some(1.0),
3182 };
3183
3184 let input = LpcInput::from_candles(&candles, "close", params);
3185 let result = lpc_with_kernel(&input, kernel)?;
3186
3187 assert_eq!(result.filter.len(), candles.close.len());
3188 assert_eq!(result.high_band.len(), candles.close.len());
3189 assert_eq!(result.low_band.len(), candles.close.len());
3190
3191 Ok(())
3192 }
3193
3194 macro_rules! generate_all_lpc_tests {
3195 ($($test_fn:ident),*) => {
3196 paste::paste! {
3197 $(
3198 #[test]
3199 fn [<$test_fn _scalar_f64>]() {
3200 let _ = $test_fn(stringify!([<$test_fn _scalar_f64>]), Kernel::Scalar);
3201 }
3202 )*
3203 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
3204 $(
3205 #[test]
3206 fn [<$test_fn _avx2_f64>]() {
3207 let _ = $test_fn(stringify!([<$test_fn _avx2_f64>]), Kernel::Avx2);
3208 }
3209 #[test]
3210 fn [<$test_fn _avx512_f64>]() {
3211 let _ = $test_fn(stringify!([<$test_fn _avx512_f64>]), Kernel::Avx512);
3212 }
3213 )*
3214 #[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
3215 $(
3216 #[test]
3217 fn [<$test_fn _simd128_f64>]() {
3218 let _ = $test_fn(stringify!([<$test_fn _simd128_f64>]), Kernel::Scalar);
3219 }
3220 )*
3221 }
3222 }
3223 }
3224
3225 generate_all_lpc_tests!(
3226 check_lpc_accuracy,
3227 check_lpc_partial_params,
3228 check_lpc_default_candles,
3229 check_lpc_zero_period,
3230 check_lpc_period_exceeds_length,
3231 check_lpc_very_small_dataset,
3232 check_lpc_empty_input,
3233 check_lpc_invalid_cutoff_type,
3234 check_lpc_all_nan,
3235 check_lpc_reinput,
3236 check_lpc_nan_handling,
3237 check_lpc_streaming,
3238 check_lpc_fixed_mode,
3239 check_lpc_no_poison
3240 );
3241
3242 #[cfg(feature = "proptest")]
3243 generate_all_lpc_tests!(check_lpc_property);
3244
3245 #[test]
3246 fn test_lpc_streaming_basic() {
3247 let params = LpcParams {
3248 cutoff_type: Some("fixed".to_string()),
3249 fixed_period: Some(10),
3250 max_cycle_limit: Some(60),
3251 cycle_mult: Some(1.0),
3252 tr_mult: Some(1.0),
3253 };
3254
3255 let mut stream = LpcStream::try_new(params).unwrap();
3256
3257 let test_data = vec![
3258 (100.0, 95.0, 98.0, 98.0),
3259 (102.0, 97.0, 101.0, 101.0),
3260 (105.0, 100.0, 104.0, 104.0),
3261 (103.0, 99.0, 100.0, 100.0),
3262 (104.0, 98.0, 102.0, 102.0),
3263 (106.0, 101.0, 105.0, 105.0),
3264 (108.0, 103.0, 107.0, 107.0),
3265 (107.0, 104.0, 106.0, 106.0),
3266 (109.0, 105.0, 108.0, 108.0),
3267 (110.0, 106.0, 109.0, 109.0),
3268 ];
3269
3270 for (high, low, close, src) in test_data {
3271 let result = stream.update(high, low, close, src);
3272 if let Some((filter, high_band, low_band)) = result {
3273 assert!(filter >= low_band);
3274 assert!(filter <= high_band);
3275 assert!(high_band > low_band);
3276 }
3277 }
3278 }
3279
3280 fn check_batch_shapes(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
3281 skip_if_unsupported!(kernel, test);
3282 let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
3283 let c = read_candles_from_csv(file)?;
3284
3285 let sweep = LpcBatchRange {
3286 fixed_period: (10, 12, 1),
3287 cycle_mult: (1.0, 1.0, 0.0),
3288 tr_mult: (1.0, 1.0, 0.0),
3289 cutoff_type: "fixed".to_string(),
3290 max_cycle_limit: 60,
3291 };
3292 let out = lpc_batch_with_kernel(&c.high, &c.low, &c.close, &c.close, &sweep, kernel)?;
3293 let combos = 3;
3294 assert_eq!(out.rows, combos * 3);
3295 assert_eq!(out.cols, c.close.len());
3296 assert_eq!(out.values.len(), out.rows * out.cols);
3297 Ok(())
3298 }
3299
3300 #[cfg(debug_assertions)]
3301 fn check_batch_no_poison(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
3302 skip_if_unsupported!(kernel, test);
3303 let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
3304 let c = read_candles_from_csv(file)?;
3305 let sweep = LpcBatchRange::default();
3306 let out = lpc_batch_with_kernel(&c.high, &c.low, &c.close, &c.close, &sweep, kernel)?;
3307 for &v in &out.values {
3308 if v.is_nan() {
3309 continue;
3310 }
3311 let b = v.to_bits();
3312 assert!(
3313 b != 0x11111111_11111111 && b != 0x22222222_22222222 && b != 0x33333333_33333333,
3314 "[{}] found poison value",
3315 test
3316 );
3317 }
3318 Ok(())
3319 }
3320
3321 macro_rules! gen_lpc_batch_tests {
3322 ($name:ident) => {
3323 paste::paste! {
3324 #[test] fn [<$name _scalar>]() { let _ = $name(stringify!([<$name _scalar>]), Kernel::ScalarBatch); }
3325 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
3326 #[test] fn [<$name _avx2>]() { let _ = $name(stringify!([<$name _avx2>]), Kernel::Avx2Batch); }
3327 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
3328 #[test] fn [<$name _avx512>]() { let _ = $name(stringify!([<$name _avx512>]), Kernel::Avx512Batch); }
3329 #[test] fn [<$name _auto>]() { let _ = $name(stringify!([<$name _auto>]), Kernel::Auto); }
3330 }
3331 }
3332 }
3333 gen_lpc_batch_tests!(check_batch_shapes);
3334 #[cfg(debug_assertions)]
3335 gen_lpc_batch_tests!(check_batch_no_poison);
3336}