Skip to main content

vector_ta/indicators/
lpc.rs

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