Skip to main content

vector_ta/indicators/
cksp.rs

1use crate::utilities::data_loader::Candles;
2use crate::utilities::enums::Kernel;
3use crate::utilities::helpers::{
4    alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
5    make_uninit_matrix,
6};
7#[cfg(not(target_arch = "wasm32"))]
8use rayon::prelude::*;
9use std::convert::AsRef;
10use std::error::Error;
11use std::mem::{ManuallyDrop, MaybeUninit};
12use thiserror::Error;
13
14#[cfg(all(feature = "python", feature = "cuda"))]
15use crate::cuda::{cuda_available, CudaCksp};
16#[cfg(all(feature = "python", feature = "cuda"))]
17use crate::utilities::dlpack_cuda::{make_device_array_py, DeviceArrayF32Py};
18#[cfg(feature = "python")]
19use crate::utilities::kernel_validation::validate_kernel;
20#[cfg(all(feature = "python", feature = "cuda"))]
21use numpy::PyUntypedArrayMethods;
22#[cfg(feature = "python")]
23use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
24#[cfg(feature = "python")]
25use pyo3::exceptions::PyValueError;
26#[cfg(feature = "python")]
27use pyo3::prelude::*;
28#[cfg(feature = "python")]
29use pyo3::types::PyDict;
30
31#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
32use serde::{Deserialize, Serialize};
33#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
34use wasm_bindgen::prelude::*;
35
36#[derive(Debug, Clone)]
37pub enum CkspData<'a> {
38    Candles {
39        candles: &'a Candles,
40    },
41    Slices {
42        high: &'a [f64],
43        low: &'a [f64],
44        close: &'a [f64],
45    },
46}
47
48#[derive(Debug, Clone)]
49#[cfg_attr(
50    all(target_arch = "wasm32", feature = "wasm"),
51    derive(Serialize, Deserialize)
52)]
53pub struct CkspParams {
54    pub p: Option<usize>,
55    pub x: Option<f64>,
56    pub q: Option<usize>,
57}
58
59impl Default for CkspParams {
60    fn default() -> Self {
61        Self {
62            p: Some(10),
63            x: Some(1.0),
64            q: Some(9),
65        }
66    }
67}
68
69#[derive(Debug, Clone)]
70pub struct CkspInput<'a> {
71    pub data: CkspData<'a>,
72    pub params: CkspParams,
73}
74
75impl<'a> CkspInput<'a> {
76    #[inline]
77    pub fn from_candles(candles: &'a Candles, params: CkspParams) -> Self {
78        Self {
79            data: CkspData::Candles { candles },
80            params,
81        }
82    }
83    #[inline]
84    pub fn from_slices(
85        high: &'a [f64],
86        low: &'a [f64],
87        close: &'a [f64],
88        params: CkspParams,
89    ) -> Self {
90        Self {
91            data: CkspData::Slices { high, low, close },
92            params,
93        }
94    }
95    #[inline]
96    pub fn with_default_candles(candles: &'a Candles) -> Self {
97        Self::from_candles(candles, CkspParams::default())
98    }
99    #[inline]
100    pub fn get_p(&self) -> usize {
101        self.params.p.unwrap_or(10)
102    }
103    #[inline]
104    pub fn get_x(&self) -> f64 {
105        self.params.x.unwrap_or(1.0)
106    }
107    #[inline]
108    pub fn get_q(&self) -> usize {
109        self.params.q.unwrap_or(9)
110    }
111}
112
113impl<'a> AsRef<[f64]> for CkspInput<'a> {
114    #[inline(always)]
115    fn as_ref(&self) -> &[f64] {
116        match &self.data {
117            CkspData::Candles { candles } => &candles.close,
118            CkspData::Slices { close, .. } => close,
119        }
120    }
121}
122
123#[derive(Debug, Clone)]
124pub struct CkspOutput {
125    pub long_values: Vec<f64>,
126    pub short_values: Vec<f64>,
127}
128
129#[derive(Debug, Error)]
130pub enum CkspError {
131    #[error("cksp: Data is empty")]
132    EmptyInputData,
133    #[error("cksp: No data (all values are NaN)")]
134    AllValuesNaN,
135    #[error("cksp: Not enough data for period={period} (data_len={data_len})")]
136    InvalidPeriod { period: usize, data_len: usize },
137    #[error("cksp: Not enough data: needed={needed} valid={valid}")]
138    NotEnoughValidData { needed: usize, valid: usize },
139    #[error("cksp: output length mismatch: expected={expected} got={got}")]
140    OutputLengthMismatch { expected: usize, got: usize },
141    #[error("cksp: Inconsistent input lengths")]
142    InconsistentLengths,
143
144    #[error("cksp: Invalid param x={x}")]
145    InvalidMultiplier { x: f64 },
146    #[error("cksp: Invalid param {param}")]
147    InvalidParam { param: &'static str },
148
149    #[error("cksp: invalid range: start={start} end={end} step={step}")]
150    InvalidRange { start: i128, end: i128, step: i128 },
151    #[error("cksp: invalid kernel for batch: {0:?}")]
152    InvalidKernelForBatch(Kernel),
153
154    #[error("cksp: candle field error: {0}")]
155    CandleFieldError(String),
156    #[error("cksp: invalid input: {0}")]
157    InvalidInput(String),
158}
159
160#[derive(Copy, Clone, Debug)]
161pub struct CkspBuilder {
162    p: Option<usize>,
163    x: Option<f64>,
164    q: Option<usize>,
165    kernel: Kernel,
166}
167
168impl Default for CkspBuilder {
169    fn default() -> Self {
170        Self {
171            p: None,
172            x: None,
173            q: None,
174            kernel: Kernel::Auto,
175        }
176    }
177}
178
179impl CkspBuilder {
180    #[inline(always)]
181    pub fn new() -> Self {
182        Self::default()
183    }
184    #[inline(always)]
185    pub fn p(mut self, n: usize) -> Self {
186        self.p = Some(n);
187        self
188    }
189    #[inline(always)]
190    pub fn x(mut self, v: f64) -> Self {
191        self.x = Some(v);
192        self
193    }
194    #[inline(always)]
195    pub fn q(mut self, n: usize) -> Self {
196        self.q = Some(n);
197        self
198    }
199    #[inline(always)]
200    pub fn kernel(mut self, k: Kernel) -> Self {
201        self.kernel = k;
202        self
203    }
204    #[inline(always)]
205    pub fn apply(self, candles: &Candles) -> Result<CkspOutput, CkspError> {
206        let params = CkspParams {
207            p: self.p,
208            x: self.x,
209            q: self.q,
210        };
211        let input = CkspInput::from_candles(candles, params);
212        cksp_with_kernel(&input, self.kernel)
213    }
214    #[inline(always)]
215    pub fn apply_slices(
216        self,
217        high: &[f64],
218        low: &[f64],
219        close: &[f64],
220    ) -> Result<CkspOutput, CkspError> {
221        let params = CkspParams {
222            p: self.p,
223            x: self.x,
224            q: self.q,
225        };
226        let input = CkspInput::from_slices(high, low, close, params);
227        cksp_with_kernel(&input, self.kernel)
228    }
229    #[inline(always)]
230    pub fn into_stream(self) -> Result<CkspStream, CkspError> {
231        let params = CkspParams {
232            p: self.p,
233            x: self.x,
234            q: self.q,
235        };
236        CkspStream::try_new(params)
237    }
238}
239
240#[inline]
241pub fn cksp(input: &CkspInput) -> Result<CkspOutput, CkspError> {
242    cksp_with_kernel(input, Kernel::Auto)
243}
244
245#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
246#[inline]
247pub fn cksp_into(
248    input: &CkspInput,
249    out_long: &mut [f64],
250    out_short: &mut [f64],
251) -> Result<(), CkspError> {
252    cksp_into_slices(out_long, out_short, input, Kernel::Auto)
253}
254
255pub fn cksp_with_kernel(input: &CkspInput, kernel: Kernel) -> Result<CkspOutput, CkspError> {
256    let (high, low, close) = match &input.data {
257        CkspData::Candles { candles } => {
258            let h = candles
259                .select_candle_field("high")
260                .map_err(|e| CkspError::CandleFieldError(e.to_string()))?;
261            let l = candles
262                .select_candle_field("low")
263                .map_err(|e| CkspError::CandleFieldError(e.to_string()))?;
264            let c = candles
265                .select_candle_field("close")
266                .map_err(|e| CkspError::CandleFieldError(e.to_string()))?;
267            (h, l, c)
268        }
269        CkspData::Slices { high, low, close } => {
270            if high.len() != low.len() || low.len() != close.len() {
271                return Err(CkspError::InconsistentLengths);
272            }
273            (*high, *low, *close)
274        }
275    };
276    let p = input.get_p();
277    let x = input.get_x();
278    let q = input.get_q();
279
280    if p == 0 || q == 0 {
281        return Err(CkspError::InvalidParam { param: "p/q" });
282    }
283    if !x.is_finite() {
284        return Err(CkspError::InvalidMultiplier { x });
285    }
286
287    let size = close.len();
288    if size == 0 {
289        return Err(CkspError::EmptyInputData);
290    }
291
292    let first_valid_idx = match close.iter().position(|&v| !v.is_nan()) {
293        Some(idx) => idx,
294        None => return Err(CkspError::AllValuesNaN),
295    };
296
297    let valid = size - first_valid_idx;
298    let warmup = p
299        .checked_add(q)
300        .and_then(|v| v.checked_sub(1))
301        .ok_or_else(|| CkspError::InvalidInput("warmup overflow (p+q too large)".into()))?;
302    if valid <= warmup {
303        let needed = warmup
304            .checked_add(1)
305            .ok_or_else(|| CkspError::InvalidInput("warmup+1 overflow".into()))?;
306        return Err(CkspError::NotEnoughValidData { needed, valid });
307    }
308
309    let chosen = match kernel {
310        Kernel::Auto => Kernel::Scalar,
311        other => other,
312    };
313
314    unsafe {
315        match chosen {
316            Kernel::Scalar | Kernel::ScalarBatch => {
317                cksp_scalar(high, low, close, p, x, q, first_valid_idx)
318            }
319            #[cfg(not(all(feature = "nightly-avx", target_arch = "x86_64")))]
320            Kernel::Avx2 | Kernel::Avx2Batch => {
321                cksp_scalar(high, low, close, p, x, q, first_valid_idx)
322            }
323            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
324            Kernel::Avx2 | Kernel::Avx2Batch => {
325                cksp_avx2(high, low, close, p, x, q, first_valid_idx)
326            }
327            #[cfg(not(all(feature = "nightly-avx", target_arch = "x86_64")))]
328            Kernel::Avx512 | Kernel::Avx512Batch => {
329                cksp_scalar(high, low, close, p, x, q, first_valid_idx)
330            }
331            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
332            Kernel::Avx512 | Kernel::Avx512Batch => {
333                cksp_avx512(high, low, close, p, x, q, first_valid_idx)
334            }
335            _ => unreachable!(),
336        }
337    }
338}
339
340#[inline]
341pub fn cksp_into_slices(
342    out_long: &mut [f64],
343    out_short: &mut [f64],
344    input: &CkspInput,
345    kern: Kernel,
346) -> Result<(), CkspError> {
347    let (high, low, close) = match &input.data {
348        CkspData::Candles { candles } => (
349            candles
350                .select_candle_field("high")
351                .map_err(|e| CkspError::CandleFieldError(e.to_string()))?,
352            candles
353                .select_candle_field("low")
354                .map_err(|e| CkspError::CandleFieldError(e.to_string()))?,
355            candles
356                .select_candle_field("close")
357                .map_err(|e| CkspError::CandleFieldError(e.to_string()))?,
358        ),
359        CkspData::Slices { high, low, close } => (*high, *low, *close),
360    };
361    if high.len() != low.len() || low.len() != close.len() {
362        return Err(CkspError::InconsistentLengths);
363    }
364    if out_long.len() != close.len() {
365        return Err(CkspError::OutputLengthMismatch {
366            expected: close.len(),
367            got: out_long.len(),
368        });
369    }
370    if out_short.len() != close.len() {
371        return Err(CkspError::OutputLengthMismatch {
372            expected: close.len(),
373            got: out_short.len(),
374        });
375    }
376
377    let p = input.get_p();
378    let q = input.get_q();
379    let x = input.get_x();
380    if p == 0 || q == 0 {
381        return Err(CkspError::InvalidParam { param: "p/q" });
382    }
383    if !x.is_finite() {
384        return Err(CkspError::InvalidMultiplier { x });
385    }
386
387    let size = close.len();
388    let first_valid = close
389        .iter()
390        .position(|v| !v.is_nan())
391        .ok_or(CkspError::AllValuesNaN)?;
392    let valid = size - first_valid;
393    let warmup = p
394        .checked_add(q)
395        .and_then(|v| v.checked_sub(1))
396        .ok_or_else(|| CkspError::InvalidInput("warmup overflow (p+q too large)".into()))?;
397    if valid <= warmup {
398        let needed = warmup
399            .checked_add(1)
400            .ok_or_else(|| CkspError::InvalidInput("warmup+1 overflow".into()))?;
401        return Err(CkspError::NotEnoughValidData { needed, valid });
402    }
403    let chosen = match kern {
404        Kernel::Auto => Kernel::Scalar,
405        k => k,
406    };
407
408    unsafe {
409        match chosen {
410            Kernel::Scalar | Kernel::ScalarBatch => {
411                cksp_row_scalar(high, low, close, p, x, q, first_valid, out_long, out_short)
412            }
413            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
414            Kernel::Avx2 | Kernel::Avx2Batch => {
415                cksp_row_avx2(high, low, close, p, x, q, first_valid, out_long, out_short)
416            }
417            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
418            Kernel::Avx512 | Kernel::Avx512Batch => {
419                cksp_row_avx512(high, low, close, p, x, q, first_valid, out_long, out_short)
420            }
421            _ => unreachable!(),
422        }
423    }
424    Ok(())
425}
426
427#[inline]
428pub unsafe fn cksp_scalar(
429    high: &[f64],
430    low: &[f64],
431    close: &[f64],
432    p: usize,
433    x: f64,
434    q: usize,
435    first_valid_idx: usize,
436) -> Result<CkspOutput, CkspError> {
437    let size = close.len();
438    let warmup = first_valid_idx + p + q - 1;
439
440    let mut long_values = alloc_with_nan_prefix(size, warmup);
441    let mut short_values = alloc_with_nan_prefix(size, warmup);
442
443    if first_valid_idx >= size {
444        return Ok(CkspOutput {
445            long_values,
446            short_values,
447        });
448    }
449
450    let cap = q + 1;
451
452    let mut h_idx: Vec<usize> = Vec::with_capacity(cap);
453    h_idx.set_len(cap);
454    let mut h_head: usize = 0;
455    let mut h_tail: usize = 0;
456
457    let mut l_idx: Vec<usize> = Vec::with_capacity(cap);
458    l_idx.set_len(cap);
459    let mut l_head: usize = 0;
460    let mut l_tail: usize = 0;
461
462    let mut ls_idx: Vec<usize> = Vec::with_capacity(cap);
463    let mut ls_val: Vec<f64> = Vec::with_capacity(cap);
464    ls_idx.set_len(cap);
465    ls_val.set_len(cap);
466    let mut ls_head: usize = 0;
467    let mut ls_tail: usize = 0;
468
469    let mut ss_idx: Vec<usize> = Vec::with_capacity(cap);
470    let mut ss_val: Vec<f64> = Vec::with_capacity(cap);
471    ss_idx.set_len(cap);
472    ss_val.set_len(cap);
473    let mut ss_head: usize = 0;
474    let mut ss_tail: usize = 0;
475
476    let mut sum_tr: f64 = 0.0;
477    let mut rma: f64 = 0.0;
478    let alpha: f64 = 1.0 / (p as f64);
479
480    #[inline(always)]
481    unsafe fn rb_dec(idx: usize, cap: usize) -> usize {
482        if idx == 0 {
483            cap - 1
484        } else {
485            idx - 1
486        }
487    }
488    #[inline(always)]
489    unsafe fn rb_inc(idx: usize, cap: usize) -> usize {
490        let mut t = idx + 1;
491        if t == cap {
492            t = 0;
493        }
494        t
495    }
496
497    for i in 0..size {
498        if i < first_valid_idx {
499            continue;
500        }
501
502        let hi = *high.get_unchecked(i);
503        let lo = *low.get_unchecked(i);
504        let tr = if i == first_valid_idx {
505            hi - lo
506        } else {
507            let cprev = *close.get_unchecked(i - 1);
508            let hl = hi - lo;
509            let hc = (hi - cprev).abs();
510            let lc = (lo - cprev).abs();
511            if hl >= hc {
512                if hl >= lc {
513                    hl
514                } else {
515                    lc
516                }
517            } else {
518                if hc >= lc {
519                    hc
520                } else {
521                    lc
522                }
523            }
524        };
525
526        let k = i - first_valid_idx;
527        if k < p {
528            sum_tr += tr;
529            if k == p - 1 {
530                rma = sum_tr / (p as f64);
531            }
532        } else {
533            rma = alpha.mul_add(tr - rma, rma);
534        }
535
536        while h_head != h_tail {
537            let last = rb_dec(h_tail, cap);
538            let last_i = *h_idx.get_unchecked(last);
539            if *high.get_unchecked(last_i) <= hi {
540                h_tail = last;
541            } else {
542                break;
543            }
544        }
545
546        let mut next_tail = rb_inc(h_tail, cap);
547        if next_tail == h_head {
548            h_head = rb_inc(h_head, cap);
549        }
550        *h_idx.get_unchecked_mut(h_tail) = i;
551        h_tail = next_tail;
552        while h_head != h_tail {
553            let front_i = *h_idx.get_unchecked(h_head);
554            if front_i + q <= i {
555                h_head = rb_inc(h_head, cap);
556            } else {
557                break;
558            }
559        }
560        let mh = *high.get_unchecked(*h_idx.get_unchecked(h_head));
561
562        while l_head != l_tail {
563            let last = rb_dec(l_tail, cap);
564            let last_i = *l_idx.get_unchecked(last);
565            if *low.get_unchecked(last_i) >= lo {
566                l_tail = last;
567            } else {
568                break;
569            }
570        }
571        let mut next_tail = rb_inc(l_tail, cap);
572        if next_tail == l_head {
573            l_head = rb_inc(l_head, cap);
574        }
575        *l_idx.get_unchecked_mut(l_tail) = i;
576        l_tail = next_tail;
577        while l_head != l_tail {
578            let front_i = *l_idx.get_unchecked(l_head);
579            if front_i + q <= i {
580                l_head = rb_inc(l_head, cap);
581            } else {
582                break;
583            }
584        }
585        let ml = *low.get_unchecked(*l_idx.get_unchecked(l_head));
586
587        if i >= warmup {
588            let ls0 = (-x).mul_add(rma, mh);
589            let ss0 = x.mul_add(rma, ml);
590
591            while ls_head != ls_tail {
592                let last = rb_dec(ls_tail, cap);
593                if *ls_val.get_unchecked(last) <= ls0 {
594                    ls_tail = last;
595                } else {
596                    break;
597                }
598            }
599            let mut next_tail = rb_inc(ls_tail, cap);
600            if next_tail == ls_head {
601                ls_head = rb_inc(ls_head, cap);
602            }
603            *ls_idx.get_unchecked_mut(ls_tail) = i;
604            *ls_val.get_unchecked_mut(ls_tail) = ls0;
605            ls_tail = next_tail;
606            while ls_head != ls_tail {
607                let front_i = *ls_idx.get_unchecked(ls_head);
608                if front_i + q <= i {
609                    ls_head = rb_inc(ls_head, cap);
610                } else {
611                    break;
612                }
613            }
614            let mx = *ls_val.get_unchecked(ls_head);
615            *long_values.get_unchecked_mut(i) = mx;
616
617            while ss_head != ss_tail {
618                let last = rb_dec(ss_tail, cap);
619                if *ss_val.get_unchecked(last) >= ss0 {
620                    ss_tail = last;
621                } else {
622                    break;
623                }
624            }
625            let mut next_tail = rb_inc(ss_tail, cap);
626            if next_tail == ss_head {
627                ss_head = rb_inc(ss_head, cap);
628            }
629            *ss_idx.get_unchecked_mut(ss_tail) = i;
630            *ss_val.get_unchecked_mut(ss_tail) = ss0;
631            ss_tail = next_tail;
632            while ss_head != ss_tail {
633                let front_i = *ss_idx.get_unchecked(ss_head);
634                if front_i + q <= i {
635                    ss_head = rb_inc(ss_head, cap);
636                } else {
637                    break;
638                }
639            }
640            let mn = *ss_val.get_unchecked(ss_head);
641            *short_values.get_unchecked_mut(i) = mn;
642        }
643    }
644
645    Ok(CkspOutput {
646        long_values,
647        short_values,
648    })
649}
650
651#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
652#[inline]
653pub unsafe fn cksp_avx2(
654    high: &[f64],
655    low: &[f64],
656    close: &[f64],
657    p: usize,
658    x: f64,
659    q: usize,
660    first_valid_idx: usize,
661) -> Result<CkspOutput, CkspError> {
662    cksp_scalar(high, low, close, p, x, q, first_valid_idx)
663}
664
665#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
666#[inline]
667pub unsafe fn cksp_avx512(
668    high: &[f64],
669    low: &[f64],
670    close: &[f64],
671    p: usize,
672    x: f64,
673    q: usize,
674    first_valid_idx: usize,
675) -> Result<CkspOutput, CkspError> {
676    cksp_scalar(high, low, close, p, x, q, first_valid_idx)
677}
678
679#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
680#[inline]
681pub unsafe fn cksp_avx512_short(
682    high: &[f64],
683    low: &[f64],
684    close: &[f64],
685    p: usize,
686    x: f64,
687    q: usize,
688    first_valid_idx: usize,
689) -> Result<CkspOutput, CkspError> {
690    cksp_avx512(high, low, close, p, x, q, first_valid_idx)
691}
692
693#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
694#[inline]
695pub unsafe fn cksp_avx512_long(
696    high: &[f64],
697    low: &[f64],
698    close: &[f64],
699    p: usize,
700    x: f64,
701    q: usize,
702    first_valid_idx: usize,
703) -> Result<CkspOutput, CkspError> {
704    cksp_avx512(high, low, close, p, x, q, first_valid_idx)
705}
706
707#[inline(always)]
708pub unsafe fn cksp_compute_into(
709    high: &[f64],
710    low: &[f64],
711    close: &[f64],
712    p: usize,
713    x: f64,
714    q: usize,
715    first_valid_idx: usize,
716    out_long: &mut [f64],
717    out_short: &mut [f64],
718) {
719    let size = close.len();
720    let warmup = first_valid_idx + p + q - 1;
721
722    for i in 0..warmup.min(size) {
723        *out_long.get_unchecked_mut(i) = f64::NAN;
724        *out_short.get_unchecked_mut(i) = f64::NAN;
725    }
726
727    let cap = q + 1;
728    let mut h_idx: Vec<usize> = Vec::with_capacity(cap);
729    h_idx.set_len(cap);
730    let mut h_head: usize = 0;
731    let mut h_tail: usize = 0;
732
733    let mut l_idx: Vec<usize> = Vec::with_capacity(cap);
734    l_idx.set_len(cap);
735    let mut l_head: usize = 0;
736    let mut l_tail: usize = 0;
737
738    let mut ls_idx: Vec<usize> = Vec::with_capacity(cap);
739    let mut ls_val: Vec<f64> = Vec::with_capacity(cap);
740    ls_idx.set_len(cap);
741    ls_val.set_len(cap);
742    let mut ls_head: usize = 0;
743    let mut ls_tail: usize = 0;
744
745    let mut ss_idx: Vec<usize> = Vec::with_capacity(cap);
746    let mut ss_val: Vec<f64> = Vec::with_capacity(cap);
747    ss_idx.set_len(cap);
748    ss_val.set_len(cap);
749    let mut ss_head: usize = 0;
750    let mut ss_tail: usize = 0;
751
752    let mut sum_tr: f64 = 0.0;
753    let mut rma: f64 = 0.0;
754    let alpha: f64 = 1.0 / (p as f64);
755
756    #[inline(always)]
757    unsafe fn rb_dec(idx: usize, cap: usize) -> usize {
758        if idx == 0 {
759            cap - 1
760        } else {
761            idx - 1
762        }
763    }
764    #[inline(always)]
765    unsafe fn rb_inc(idx: usize, cap: usize) -> usize {
766        let mut t = idx + 1;
767        if t == cap {
768            t = 0;
769        }
770        t
771    }
772
773    for i in 0..size {
774        if i < first_valid_idx {
775            continue;
776        }
777
778        let hi = *high.get_unchecked(i);
779        let lo = *low.get_unchecked(i);
780        let tr = if i == first_valid_idx {
781            hi - lo
782        } else {
783            let cprev = *close.get_unchecked(i - 1);
784            let hl = hi - lo;
785            let hc = (hi - cprev).abs();
786            let lc = (lo - cprev).abs();
787            if hl >= hc {
788                if hl >= lc {
789                    hl
790                } else {
791                    lc
792                }
793            } else {
794                if hc >= lc {
795                    hc
796                } else {
797                    lc
798                }
799            }
800        };
801
802        let k = i - first_valid_idx;
803        if k < p {
804            sum_tr += tr;
805            if k == p - 1 {
806                rma = sum_tr / (p as f64);
807            }
808        } else {
809            rma = alpha.mul_add(tr - rma, rma);
810        }
811
812        while h_head != h_tail {
813            let last = rb_dec(h_tail, cap);
814            let last_i = *h_idx.get_unchecked(last);
815            if *high.get_unchecked(last_i) <= hi {
816                h_tail = last;
817            } else {
818                break;
819            }
820        }
821        let mut next_tail = rb_inc(h_tail, cap);
822        if next_tail == h_head {
823            h_head = rb_inc(h_head, cap);
824        }
825        *h_idx.get_unchecked_mut(h_tail) = i;
826        h_tail = next_tail;
827        while h_head != h_tail {
828            let front_i = *h_idx.get_unchecked(h_head);
829            if front_i + q <= i {
830                h_head = rb_inc(h_head, cap);
831            } else {
832                break;
833            }
834        }
835        let mh = *high.get_unchecked(*h_idx.get_unchecked(h_head));
836
837        while l_head != l_tail {
838            let last = rb_dec(l_tail, cap);
839            let last_i = *l_idx.get_unchecked(last);
840            if *low.get_unchecked(last_i) >= lo {
841                l_tail = last;
842            } else {
843                break;
844            }
845        }
846        let mut next_tail = rb_inc(l_tail, cap);
847        if next_tail == l_head {
848            l_head = rb_inc(l_head, cap);
849        }
850        *l_idx.get_unchecked_mut(l_tail) = i;
851        l_tail = next_tail;
852        while l_head != l_tail {
853            let front_i = *l_idx.get_unchecked(l_head);
854            if front_i + q <= i {
855                l_head = rb_inc(l_head, cap);
856            } else {
857                break;
858            }
859        }
860        let ml = *low.get_unchecked(*l_idx.get_unchecked(l_head));
861
862        if i >= warmup {
863            let ls0 = (-x).mul_add(rma, mh);
864            let ss0 = x.mul_add(rma, ml);
865
866            while ls_head != ls_tail {
867                let last = rb_dec(ls_tail, cap);
868                if *ls_val.get_unchecked(last) <= ls0 {
869                    ls_tail = last;
870                } else {
871                    break;
872                }
873            }
874            let mut next_tail = rb_inc(ls_tail, cap);
875            if next_tail == ls_head {
876                ls_head = rb_inc(ls_head, cap);
877            }
878            *ls_idx.get_unchecked_mut(ls_tail) = i;
879            *ls_val.get_unchecked_mut(ls_tail) = ls0;
880            ls_tail = next_tail;
881            while ls_head != ls_tail {
882                let front_i = *ls_idx.get_unchecked(ls_head);
883                if front_i + q <= i {
884                    ls_head = rb_inc(ls_head, cap);
885                } else {
886                    break;
887                }
888            }
889            let mx = *ls_val.get_unchecked(ls_head);
890            *out_long.get_unchecked_mut(i) = mx;
891
892            while ss_head != ss_tail {
893                let last = rb_dec(ss_tail, cap);
894                if *ss_val.get_unchecked(last) >= ss0 {
895                    ss_tail = last;
896                } else {
897                    break;
898                }
899            }
900            let mut next_tail = rb_inc(ss_tail, cap);
901            if next_tail == ss_head {
902                ss_head = rb_inc(ss_head, cap);
903            }
904            *ss_idx.get_unchecked_mut(ss_tail) = i;
905            *ss_val.get_unchecked_mut(ss_tail) = ss0;
906            ss_tail = next_tail;
907            while ss_head != ss_tail {
908                let front_i = *ss_idx.get_unchecked(ss_head);
909                if front_i + q <= i {
910                    ss_head = rb_inc(ss_head, cap);
911                } else {
912                    break;
913                }
914            }
915            let mn = *ss_val.get_unchecked(ss_head);
916            *out_short.get_unchecked_mut(i) = mn;
917        }
918    }
919}
920
921#[inline(always)]
922pub unsafe fn cksp_row_scalar(
923    high: &[f64],
924    low: &[f64],
925    close: &[f64],
926    p: usize,
927    x: f64,
928    q: usize,
929    first_valid_idx: usize,
930    out_long: &mut [f64],
931    out_short: &mut [f64],
932) {
933    cksp_compute_into(
934        high,
935        low,
936        close,
937        p,
938        x,
939        q,
940        first_valid_idx,
941        out_long,
942        out_short,
943    );
944}
945
946#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
947#[inline(always)]
948pub unsafe fn cksp_row_avx2(
949    high: &[f64],
950    low: &[f64],
951    close: &[f64],
952    p: usize,
953    x: f64,
954    q: usize,
955    first_valid_idx: usize,
956    out_long: &mut [f64],
957    out_short: &mut [f64],
958) {
959    cksp_compute_into(
960        high,
961        low,
962        close,
963        p,
964        x,
965        q,
966        first_valid_idx,
967        out_long,
968        out_short,
969    )
970}
971
972#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
973#[inline(always)]
974pub unsafe fn cksp_row_avx512(
975    high: &[f64],
976    low: &[f64],
977    close: &[f64],
978    p: usize,
979    x: f64,
980    q: usize,
981    first_valid_idx: usize,
982    out_long: &mut [f64],
983    out_short: &mut [f64],
984) {
985    cksp_compute_into(
986        high,
987        low,
988        close,
989        p,
990        x,
991        q,
992        first_valid_idx,
993        out_long,
994        out_short,
995    )
996}
997#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
998#[inline(always)]
999pub unsafe fn cksp_row_avx512_short(
1000    high: &[f64],
1001    low: &[f64],
1002    close: &[f64],
1003    p: usize,
1004    x: f64,
1005    q: usize,
1006    first_valid_idx: usize,
1007    out_long: &mut [f64],
1008    out_short: &mut [f64],
1009) {
1010    cksp_compute_into(
1011        high,
1012        low,
1013        close,
1014        p,
1015        x,
1016        q,
1017        first_valid_idx,
1018        out_long,
1019        out_short,
1020    )
1021}
1022#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1023#[inline(always)]
1024pub unsafe fn cksp_row_avx512_long(
1025    high: &[f64],
1026    low: &[f64],
1027    close: &[f64],
1028    p: usize,
1029    x: f64,
1030    q: usize,
1031    first_valid_idx: usize,
1032    out_long: &mut [f64],
1033    out_short: &mut [f64],
1034) {
1035    cksp_compute_into(
1036        high,
1037        low,
1038        close,
1039        p,
1040        x,
1041        q,
1042        first_valid_idx,
1043        out_long,
1044        out_short,
1045    )
1046}
1047
1048#[derive(Debug, Clone)]
1049pub struct CkspStream {
1050    p: usize,
1051    x: f64,
1052    q: usize,
1053
1054    warmup: usize,
1055    alpha: f64,
1056    sum_tr: f64,
1057    rma: f64,
1058    prev_close: f64,
1059    i: usize,
1060
1061    cap: usize,
1062    mask: usize,
1063
1064    h_idx: Vec<usize>,
1065    h_val: Vec<f64>,
1066    h_head: usize,
1067    h_tail: usize,
1068
1069    l_idx: Vec<usize>,
1070    l_val: Vec<f64>,
1071    l_head: usize,
1072    l_tail: usize,
1073
1074    ls_idx: Vec<usize>,
1075    ls_val: Vec<f64>,
1076    ls_head: usize,
1077    ls_tail: usize,
1078
1079    ss_idx: Vec<usize>,
1080    ss_val: Vec<f64>,
1081    ss_head: usize,
1082    ss_tail: usize,
1083}
1084
1085impl CkspStream {
1086    #[inline]
1087    fn next_pow2(x: usize) -> usize {
1088        x.next_power_of_two().max(2)
1089    }
1090
1091    #[inline(always)]
1092    fn inc(idx: usize, mask: usize) -> usize {
1093        (idx + 1) & mask
1094    }
1095    #[inline(always)]
1096    fn dec(idx: usize, mask: usize) -> usize {
1097        idx.wrapping_sub(1) & mask
1098    }
1099
1100    pub fn try_new(params: CkspParams) -> Result<Self, CkspError> {
1101        let p = params.p.unwrap_or(10);
1102        let x = params.x.unwrap_or(1.0);
1103        let q = params.q.unwrap_or(9);
1104        if p == 0 || q == 0 {
1105            return Err(CkspError::InvalidParam { param: "p/q" });
1106        }
1107        if !x.is_finite() {
1108            return Err(CkspError::InvalidParam { param: "x" });
1109        }
1110
1111        let cap = Self::next_pow2(q + 1);
1112        let mask = cap - 1;
1113
1114        Ok(Self {
1115            p,
1116            x,
1117            q,
1118            warmup: p + q - 1,
1119            alpha: 1.0 / p as f64,
1120            sum_tr: 0.0,
1121            rma: 0.0,
1122            prev_close: f64::NAN,
1123            i: 0,
1124
1125            cap,
1126            mask,
1127
1128            h_idx: vec![0; cap],
1129            h_val: vec![0.0; cap],
1130            h_head: 0,
1131            h_tail: 0,
1132
1133            l_idx: vec![0; cap],
1134            l_val: vec![0.0; cap],
1135            l_head: 0,
1136            l_tail: 0,
1137
1138            ls_idx: vec![0; cap],
1139            ls_val: vec![0.0; cap],
1140            ls_head: 0,
1141            ls_tail: 0,
1142
1143            ss_idx: vec![0; cap],
1144            ss_val: vec![0.0; cap],
1145            ss_head: 0,
1146            ss_tail: 0,
1147        })
1148    }
1149
1150    #[inline(always)]
1151    pub fn update(&mut self, high: f64, low: f64, close: f64) -> Option<(f64, f64)> {
1152        let tr = if self.prev_close.is_nan() {
1153            high - low
1154        } else {
1155            let hl = high - low;
1156            let hc = (high - self.prev_close).abs();
1157            let lc = (low - self.prev_close).abs();
1158            hl.max(hc).max(lc)
1159        };
1160        self.prev_close = close;
1161
1162        let atr_ready = if self.i < self.p {
1163            self.sum_tr += tr;
1164            if self.i == self.p - 1 {
1165                self.rma = self.sum_tr / self.p as f64;
1166                true
1167            } else {
1168                false
1169            }
1170        } else {
1171            self.rma = self.alpha.mul_add(tr - self.rma, self.rma);
1172            true
1173        };
1174
1175        while self.h_head != self.h_tail {
1176            let last = Self::dec(self.h_tail, self.mask);
1177            if self.h_val[last] <= high {
1178                self.h_tail = last;
1179            } else {
1180                break;
1181            }
1182        }
1183        let mut nt = Self::inc(self.h_tail, self.mask);
1184        if nt == self.h_head {
1185            self.h_head = Self::inc(self.h_head, self.mask);
1186        }
1187        self.h_idx[self.h_tail] = self.i;
1188        self.h_val[self.h_tail] = high;
1189        self.h_tail = nt;
1190
1191        while self.h_head != self.h_tail {
1192            let front_i = self.h_idx[self.h_head];
1193            if front_i + self.q <= self.i {
1194                self.h_head = Self::inc(self.h_head, self.mask);
1195            } else {
1196                break;
1197            }
1198        }
1199        let max_high = self.h_val[self.h_head];
1200
1201        while self.l_head != self.l_tail {
1202            let last = Self::dec(self.l_tail, self.mask);
1203            if self.l_val[last] >= low {
1204                self.l_tail = last;
1205            } else {
1206                break;
1207            }
1208        }
1209        nt = Self::inc(self.l_tail, self.mask);
1210        if nt == self.l_head {
1211            self.l_head = Self::inc(self.l_head, self.mask);
1212        }
1213        self.l_idx[self.l_tail] = self.i;
1214        self.l_val[self.l_tail] = low;
1215        self.l_tail = nt;
1216
1217        while self.l_head != self.l_tail {
1218            let front_i = self.l_idx[self.l_head];
1219            if front_i + self.q <= self.i {
1220                self.l_head = Self::inc(self.l_head, self.mask);
1221            } else {
1222                break;
1223            }
1224        }
1225        let min_low = self.l_val[self.l_head];
1226
1227        if self.i < self.warmup || !atr_ready {
1228            self.i += 1;
1229            return None;
1230        }
1231
1232        let ls0 = (-self.x).mul_add(self.rma, max_high);
1233        let ss0 = self.x.mul_add(self.rma, min_low);
1234
1235        while self.ls_head != self.ls_tail {
1236            let last = Self::dec(self.ls_tail, self.mask);
1237            if self.ls_val[last] <= ls0 {
1238                self.ls_tail = last;
1239            } else {
1240                break;
1241            }
1242        }
1243        nt = Self::inc(self.ls_tail, self.mask);
1244        if nt == self.ls_head {
1245            self.ls_head = Self::inc(self.ls_head, self.mask);
1246        }
1247        self.ls_idx[self.ls_tail] = self.i;
1248        self.ls_val[self.ls_tail] = ls0;
1249        self.ls_tail = nt;
1250
1251        while self.ls_head != self.ls_tail {
1252            let front_i = self.ls_idx[self.ls_head];
1253            if front_i + self.q <= self.i {
1254                self.ls_head = Self::inc(self.ls_head, self.mask);
1255            } else {
1256                break;
1257            }
1258        }
1259        let long = self.ls_val[self.ls_head];
1260
1261        while self.ss_head != self.ss_tail {
1262            let last = Self::dec(self.ss_tail, self.mask);
1263            if self.ss_val[last] >= ss0 {
1264                self.ss_tail = last;
1265            } else {
1266                break;
1267            }
1268        }
1269        nt = Self::inc(self.ss_tail, self.mask);
1270        if nt == self.ss_head {
1271            self.ss_head = Self::inc(self.ss_head, self.mask);
1272        }
1273        self.ss_idx[self.ss_tail] = self.i;
1274        self.ss_val[self.ss_tail] = ss0;
1275        self.ss_tail = nt;
1276
1277        while self.ss_head != self.ss_tail {
1278            let front_i = self.ss_idx[self.ss_head];
1279            if front_i + self.q <= self.i {
1280                self.ss_head = Self::inc(self.ss_head, self.mask);
1281            } else {
1282                break;
1283            }
1284        }
1285        let short = self.ss_val[self.ss_head];
1286
1287        self.i += 1;
1288        Some((long, short))
1289    }
1290}
1291
1292#[derive(Clone, Debug)]
1293pub struct CkspBatchRange {
1294    pub p: (usize, usize, usize),
1295    pub x: (f64, f64, f64),
1296    pub q: (usize, usize, usize),
1297}
1298
1299impl Default for CkspBatchRange {
1300    fn default() -> Self {
1301        Self {
1302            p: (10, 10, 0),
1303            x: (1.0, 1.249, 0.001),
1304            q: (9, 9, 0),
1305        }
1306    }
1307}
1308
1309#[derive(Clone, Debug, Default)]
1310pub struct CkspBatchBuilder {
1311    range: CkspBatchRange,
1312    kernel: Kernel,
1313}
1314
1315impl CkspBatchBuilder {
1316    pub fn new() -> Self {
1317        Self::default()
1318    }
1319    pub fn kernel(mut self, k: Kernel) -> Self {
1320        self.kernel = k;
1321        self
1322    }
1323    #[inline]
1324    pub fn p_range(mut self, start: usize, end: usize, step: usize) -> Self {
1325        self.range.p = (start, end, step);
1326        self
1327    }
1328    #[inline]
1329    pub fn p_static(mut self, p: usize) -> Self {
1330        self.range.p = (p, p, 0);
1331        self
1332    }
1333    #[inline]
1334    pub fn x_range(mut self, start: f64, end: f64, step: f64) -> Self {
1335        self.range.x = (start, end, step);
1336        self
1337    }
1338    #[inline]
1339    pub fn x_static(mut self, x: f64) -> Self {
1340        self.range.x = (x, x, 0.0);
1341        self
1342    }
1343    #[inline]
1344    pub fn q_range(mut self, start: usize, end: usize, step: usize) -> Self {
1345        self.range.q = (start, end, step);
1346        self
1347    }
1348    #[inline]
1349    pub fn q_static(mut self, q: usize) -> Self {
1350        self.range.q = (q, q, 0);
1351        self
1352    }
1353    pub fn apply_slices(
1354        self,
1355        high: &[f64],
1356        low: &[f64],
1357        close: &[f64],
1358    ) -> Result<CkspBatchOutput, CkspError> {
1359        cksp_batch_with_kernel(high, low, close, &self.range, self.kernel)
1360    }
1361    pub fn with_default_slices(
1362        high: &[f64],
1363        low: &[f64],
1364        close: &[f64],
1365        k: Kernel,
1366    ) -> Result<CkspBatchOutput, CkspError> {
1367        CkspBatchBuilder::new()
1368            .kernel(k)
1369            .apply_slices(high, low, close)
1370    }
1371    pub fn apply_candles(self, c: &Candles) -> Result<CkspBatchOutput, CkspError> {
1372        let h = c
1373            .select_candle_field("high")
1374            .map_err(|e| CkspError::CandleFieldError(e.to_string()))?;
1375        let l = c
1376            .select_candle_field("low")
1377            .map_err(|e| CkspError::CandleFieldError(e.to_string()))?;
1378        let cl = c
1379            .select_candle_field("close")
1380            .map_err(|e| CkspError::CandleFieldError(e.to_string()))?;
1381        self.apply_slices(h, l, cl)
1382    }
1383    pub fn with_default_candles(c: &Candles) -> Result<CkspBatchOutput, CkspError> {
1384        CkspBatchBuilder::new()
1385            .kernel(Kernel::Auto)
1386            .apply_candles(c)
1387    }
1388}
1389
1390#[derive(Clone, Debug)]
1391pub struct CkspBatchOutput {
1392    pub long_values: Vec<f64>,
1393    pub short_values: Vec<f64>,
1394    pub combos: Vec<CkspParams>,
1395    pub rows: usize,
1396    pub cols: usize,
1397}
1398impl CkspBatchOutput {
1399    pub fn row_for_params(&self, p: &CkspParams) -> Option<usize> {
1400        self.combos.iter().position(|c| {
1401            c.p.unwrap_or(10) == p.p.unwrap_or(10)
1402                && (c.x.unwrap_or(1.0) - p.x.unwrap_or(1.0)).abs() < 1e-12
1403                && c.q.unwrap_or(9) == p.q.unwrap_or(9)
1404        })
1405    }
1406    pub fn values_for(&self, p: &CkspParams) -> Option<(&[f64], &[f64])> {
1407        self.row_for_params(p).map(|row| {
1408            let start = row * self.cols;
1409            (
1410                &self.long_values[start..start + self.cols],
1411                &self.short_values[start..start + self.cols],
1412            )
1413        })
1414    }
1415}
1416
1417#[inline(always)]
1418fn expand_grid(r: &CkspBatchRange) -> Result<Vec<CkspParams>, CkspError> {
1419    fn axis_usize((start, end, step): (usize, usize, usize)) -> Result<Vec<usize>, CkspError> {
1420        let s = start as i128;
1421        let e = end as i128;
1422        let st = step as i128;
1423        if step == 0 || start == end {
1424            return Ok(vec![start]);
1425        }
1426        let mut v = Vec::new();
1427        if start <= end {
1428            let stp = step.max(1);
1429            let mut cur = start;
1430            while cur <= end {
1431                v.push(cur);
1432                cur = match cur.checked_add(stp) {
1433                    Some(n) => n,
1434                    None => break,
1435                };
1436            }
1437        } else {
1438            let stp = step.max(1);
1439            let mut cur = start;
1440            loop {
1441                v.push(cur);
1442                if cur <= end {
1443                    break;
1444                }
1445                cur = match cur.checked_sub(stp) {
1446                    Some(n) => n,
1447                    None => break,
1448                };
1449                if cur < end {
1450                    break;
1451                }
1452            }
1453        }
1454        if v.is_empty() {
1455            Err(CkspError::InvalidRange {
1456                start: s,
1457                end: e,
1458                step: st,
1459            })
1460        } else {
1461            Ok(v)
1462        }
1463    }
1464    fn axis_f64((start, end, step): (f64, f64, f64)) -> Result<Vec<f64>, CkspError> {
1465        let s = start as f64;
1466        let e = end as f64;
1467        let st = step as f64;
1468        if step.abs() < 1e-12 || (start - end).abs() < 1e-12 {
1469            return Ok(vec![start]);
1470        }
1471        let mut v = Vec::new();
1472        if start <= end {
1473            let mut x = start;
1474            while x <= end + 1e-12 {
1475                v.push(x);
1476                x = x + step;
1477            }
1478        } else {
1479            let mut x = start;
1480            while x >= end - 1e-12 {
1481                v.push(x);
1482                x = x - step.abs();
1483            }
1484        }
1485        if v.is_empty() {
1486            Err(CkspError::InvalidRange {
1487                start: s as i128,
1488                end: e as i128,
1489                step: st as i128,
1490            })
1491        } else {
1492            Ok(v)
1493        }
1494    }
1495
1496    let ps = axis_usize(r.p)?;
1497    let xs = axis_f64(r.x)?;
1498    let qs = axis_usize(r.q)?;
1499
1500    let cap = ps
1501        .len()
1502        .checked_mul(xs.len())
1503        .and_then(|t| t.checked_mul(qs.len()))
1504        .ok_or_else(|| CkspError::InvalidInput("parameter grid too large".into()))?;
1505
1506    let mut out = Vec::with_capacity(cap);
1507    for &p in &ps {
1508        for &x in &xs {
1509            for &q in &qs {
1510                out.push(CkspParams {
1511                    p: Some(p),
1512                    x: Some(x),
1513                    q: Some(q),
1514                });
1515            }
1516        }
1517    }
1518    Ok(out)
1519}
1520
1521pub fn cksp_batch_with_kernel(
1522    high: &[f64],
1523    low: &[f64],
1524    close: &[f64],
1525    sweep: &CkspBatchRange,
1526    k: Kernel,
1527) -> Result<CkspBatchOutput, CkspError> {
1528    let kernel = match k {
1529        Kernel::Auto => detect_best_batch_kernel(),
1530        other if other.is_batch() => other,
1531        other => return Err(CkspError::InvalidKernelForBatch(other)),
1532    };
1533
1534    let simd = match kernel {
1535        Kernel::Avx512Batch => Kernel::Avx512,
1536        Kernel::Avx2Batch => Kernel::Avx2,
1537        Kernel::ScalarBatch => Kernel::Scalar,
1538        _ => unreachable!(),
1539    };
1540    cksp_batch_par_slice(high, low, close, sweep, simd)
1541}
1542
1543#[inline(always)]
1544pub fn cksp_batch_slice(
1545    high: &[f64],
1546    low: &[f64],
1547    close: &[f64],
1548    sweep: &CkspBatchRange,
1549    kern: Kernel,
1550) -> Result<CkspBatchOutput, CkspError> {
1551    cksp_batch_inner(high, low, close, sweep, kern, false)
1552}
1553
1554#[inline(always)]
1555pub fn cksp_batch_par_slice(
1556    high: &[f64],
1557    low: &[f64],
1558    close: &[f64],
1559    sweep: &CkspBatchRange,
1560    kern: Kernel,
1561) -> Result<CkspBatchOutput, CkspError> {
1562    cksp_batch_inner(high, low, close, sweep, kern, true)
1563}
1564
1565#[inline(always)]
1566fn cksp_batch_inner(
1567    high: &[f64],
1568    low: &[f64],
1569    close: &[f64],
1570    sweep: &CkspBatchRange,
1571    kern: Kernel,
1572    parallel: bool,
1573) -> Result<CkspBatchOutput, CkspError> {
1574    let _ = kern;
1575    let combos = expand_grid(sweep)?;
1576    if combos.is_empty() {
1577        return Err(CkspError::InvalidParam { param: "combos" });
1578    }
1579    let size = close.len();
1580    if high.len() != low.len() || low.len() != close.len() {
1581        return Err(CkspError::InconsistentLengths);
1582    }
1583    let first_valid = close
1584        .iter()
1585        .position(|x| !x.is_nan())
1586        .ok_or(CkspError::AllValuesNaN)?;
1587
1588    let rows = combos.len();
1589    let cols = size;
1590    let _total = rows
1591        .checked_mul(cols)
1592        .ok_or_else(|| CkspError::InvalidInput("rows*cols overflow".into()))?;
1593
1594    let valid = size - first_valid;
1595    let mut warm: Vec<usize> = Vec::with_capacity(rows);
1596    for c in &combos {
1597        let p_row = c.p.unwrap_or(10);
1598        let q_row = c.q.unwrap_or(9);
1599        let warm_rel = p_row
1600            .checked_add(q_row)
1601            .and_then(|v| v.checked_sub(1))
1602            .ok_or_else(|| CkspError::InvalidInput("warmup overflow (p+q too large)".into()))?;
1603        if valid <= warm_rel {
1604            let needed = warm_rel
1605                .checked_add(1)
1606                .ok_or_else(|| CkspError::InvalidInput("warmup+1 overflow".into()))?;
1607            return Err(CkspError::NotEnoughValidData { needed, valid });
1608        }
1609        let warm_idx = first_valid
1610            .checked_add(warm_rel)
1611            .ok_or_else(|| CkspError::InvalidInput("warmup index overflow".into()))?;
1612        warm.push(warm_idx);
1613    }
1614
1615    let mut long_buf_mu = make_uninit_matrix(rows, cols);
1616    let mut short_buf_mu = make_uninit_matrix(rows, cols);
1617
1618    init_matrix_prefixes(&mut long_buf_mu, cols, &warm);
1619    init_matrix_prefixes(&mut short_buf_mu, cols, &warm);
1620
1621    let mut long_guard = ManuallyDrop::new(long_buf_mu);
1622    let mut short_guard = ManuallyDrop::new(short_buf_mu);
1623
1624    let long_values: &mut [f64] = unsafe {
1625        core::slice::from_raw_parts_mut(long_guard.as_mut_ptr() as *mut f64, long_guard.len())
1626    };
1627
1628    let short_values: &mut [f64] = unsafe {
1629        core::slice::from_raw_parts_mut(short_guard.as_mut_ptr() as *mut f64, short_guard.len())
1630    };
1631
1632    use std::collections::{BTreeSet, HashMap};
1633
1634    #[inline]
1635    fn precompute_atr_series(
1636        high: &[f64],
1637        low: &[f64],
1638        close: &[f64],
1639        p: usize,
1640        first_valid: usize,
1641    ) -> Vec<f64> {
1642        let n = close.len();
1643        let mut atr = vec![0.0; n];
1644        let mut sum_tr = 0.0;
1645        let mut rma = 0.0;
1646        let alpha = 1.0 / (p as f64);
1647        for i in 0..n {
1648            if i < first_valid {
1649                continue;
1650            }
1651            let hi = high[i];
1652            let lo = low[i];
1653            let tr = if i == first_valid {
1654                hi - lo
1655            } else {
1656                let cp = close[i - 1];
1657                let hl = hi - lo;
1658                let hc = (hi - cp).abs();
1659                let lc = (lo - cp).abs();
1660                hl.max(hc).max(lc)
1661            };
1662            let k = i - first_valid;
1663            if k < p {
1664                sum_tr += tr;
1665                if k == p - 1 {
1666                    rma = sum_tr / (p as f64);
1667                    atr[i] = rma;
1668                }
1669            } else {
1670                rma += alpha * (tr - rma);
1671                atr[i] = rma;
1672            }
1673        }
1674        atr
1675    }
1676
1677    #[inline]
1678    fn rolling_max_series(src: &[f64], q: usize, first_valid: usize) -> Vec<f64> {
1679        let n = src.len();
1680        let mut out = vec![0.0; n];
1681        let cap = q + 1;
1682        let mut idx: Vec<usize> = Vec::with_capacity(cap);
1683        unsafe {
1684            idx.set_len(cap);
1685        }
1686        let mut head = 0usize;
1687        let mut tail = 0usize;
1688        #[inline(always)]
1689        fn dec(i: usize, c: usize) -> usize {
1690            if i == 0 {
1691                c - 1
1692            } else {
1693                i - 1
1694            }
1695        }
1696        #[inline(always)]
1697        fn inc(i: usize, c: usize) -> usize {
1698            let mut t = i + 1;
1699            if t == c {
1700                t = 0;
1701            }
1702            t
1703        }
1704        for i in 0..n {
1705            if i < first_valid {
1706                continue;
1707            }
1708            while head != tail {
1709                let last = dec(tail, cap);
1710                let li = unsafe { *idx.get_unchecked(last) };
1711                if src[li] <= src[i] {
1712                    tail = last;
1713                } else {
1714                    break;
1715                }
1716            }
1717            let mut nt = inc(tail, cap);
1718            if nt == head {
1719                head = inc(head, cap);
1720            }
1721            unsafe {
1722                *idx.get_unchecked_mut(tail) = i;
1723            }
1724            tail = nt;
1725            while head != tail {
1726                let fi = unsafe { *idx.get_unchecked(head) };
1727                if fi + q <= i {
1728                    head = inc(head, cap);
1729                } else {
1730                    break;
1731                }
1732            }
1733            out[i] = src[unsafe { *idx.get_unchecked(head) }];
1734        }
1735        out
1736    }
1737
1738    #[inline]
1739    fn rolling_min_series(src: &[f64], q: usize, first_valid: usize) -> Vec<f64> {
1740        let n = src.len();
1741        let mut out = vec![0.0; n];
1742        let cap = q + 1;
1743        let mut idx: Vec<usize> = Vec::with_capacity(cap);
1744        unsafe {
1745            idx.set_len(cap);
1746        }
1747        let mut head = 0usize;
1748        let mut tail = 0usize;
1749        #[inline(always)]
1750        fn dec(i: usize, c: usize) -> usize {
1751            if i == 0 {
1752                c - 1
1753            } else {
1754                i - 1
1755            }
1756        }
1757        #[inline(always)]
1758        fn inc(i: usize, c: usize) -> usize {
1759            let mut t = i + 1;
1760            if t == c {
1761                t = 0;
1762            }
1763            t
1764        }
1765        for i in 0..n {
1766            if i < first_valid {
1767                continue;
1768            }
1769            while head != tail {
1770                let last = dec(tail, cap);
1771                let li = unsafe { *idx.get_unchecked(last) };
1772                if src[li] >= src[i] {
1773                    tail = last;
1774                } else {
1775                    break;
1776                }
1777            }
1778            let mut nt = inc(tail, cap);
1779            if nt == head {
1780                head = inc(head, cap);
1781            }
1782            unsafe {
1783                *idx.get_unchecked_mut(tail) = i;
1784            }
1785            tail = nt;
1786            while head != tail {
1787                let fi = unsafe { *idx.get_unchecked(head) };
1788                if fi + q <= i {
1789                    head = inc(head, cap);
1790                } else {
1791                    break;
1792                }
1793            }
1794            out[i] = src[unsafe { *idx.get_unchecked(head) }];
1795        }
1796        out
1797    }
1798
1799    let mut ps: BTreeSet<usize> = BTreeSet::new();
1800    let mut qs: BTreeSet<usize> = BTreeSet::new();
1801    for prm in &combos {
1802        ps.insert(prm.p.unwrap());
1803        qs.insert(prm.q.unwrap());
1804    }
1805
1806    let mut atr_map: HashMap<usize, Vec<f64>> = HashMap::with_capacity(ps.len());
1807    for &p in &ps {
1808        atr_map.insert(p, precompute_atr_series(high, low, close, p, first_valid));
1809    }
1810
1811    let mut mh_map: HashMap<usize, Vec<f64>> = HashMap::with_capacity(qs.len());
1812    let mut ml_map: HashMap<usize, Vec<f64>> = HashMap::with_capacity(qs.len());
1813    for &qv in &qs {
1814        mh_map.insert(qv, rolling_max_series(high, qv, first_valid));
1815        ml_map.insert(qv, rolling_min_series(low, qv, first_valid));
1816    }
1817
1818    let do_row = |row: usize, out_long: &mut [f64], out_short: &mut [f64]| unsafe {
1819        let prm = &combos[row];
1820        let (p, x, q) = (prm.p.unwrap(), prm.x.unwrap(), prm.q.unwrap());
1821
1822        let warmup = first_valid + p + q - 1;
1823        let atr = atr_map.get(&p).expect("atr precompute");
1824        let mh = mh_map.get(&q).expect("mh precompute");
1825        let ml = ml_map.get(&q).expect("ml precompute");
1826
1827        let cap = q + 1;
1828        let mut ls_idx: Vec<usize> = Vec::with_capacity(cap);
1829        let mut ls_val: Vec<f64> = Vec::with_capacity(cap);
1830        ls_idx.set_len(cap);
1831        ls_val.set_len(cap);
1832        let mut ls_head = 0usize;
1833        let mut ls_tail = 0usize;
1834        let mut ss_idx: Vec<usize> = Vec::with_capacity(cap);
1835        let mut ss_val: Vec<f64> = Vec::with_capacity(cap);
1836        ss_idx.set_len(cap);
1837        ss_val.set_len(cap);
1838        let mut ss_head = 0usize;
1839        let mut ss_tail = 0usize;
1840        #[inline(always)]
1841        fn dec(i: usize, c: usize) -> usize {
1842            if i == 0 {
1843                c - 1
1844            } else {
1845                i - 1
1846            }
1847        }
1848        #[inline(always)]
1849        fn inc(i: usize, c: usize) -> usize {
1850            let mut t = i + 1;
1851            if t == c {
1852                t = 0;
1853            }
1854            t
1855        }
1856
1857        for i in warmup..cols {
1858            let ls0 = mh[i] - x * atr[i];
1859            let ss0 = ml[i] + x * atr[i];
1860
1861            while ls_head != ls_tail {
1862                let last = dec(ls_tail, cap);
1863                if unsafe { *ls_val.get_unchecked(last) } <= ls0 {
1864                    ls_tail = last;
1865                } else {
1866                    break;
1867                }
1868            }
1869            let mut nt = inc(ls_tail, cap);
1870            if nt == ls_head {
1871                ls_head = inc(ls_head, cap);
1872            }
1873            unsafe {
1874                *ls_idx.get_unchecked_mut(ls_tail) = i;
1875                *ls_val.get_unchecked_mut(ls_tail) = ls0;
1876            }
1877            ls_tail = nt;
1878            while ls_head != ls_tail {
1879                let fi = unsafe { *ls_idx.get_unchecked(ls_head) };
1880                if fi + q <= i {
1881                    ls_head = inc(ls_head, cap);
1882                } else {
1883                    break;
1884                }
1885            }
1886            let mx = unsafe { *ls_val.get_unchecked(ls_head) };
1887            *out_long.get_unchecked_mut(i) = mx;
1888
1889            while ss_head != ss_tail {
1890                let last = dec(ss_tail, cap);
1891                if unsafe { *ss_val.get_unchecked(last) } >= ss0 {
1892                    ss_tail = last;
1893                } else {
1894                    break;
1895                }
1896            }
1897            let mut nt2 = inc(ss_tail, cap);
1898            if nt2 == ss_head {
1899                ss_head = inc(ss_head, cap);
1900            }
1901            unsafe {
1902                *ss_idx.get_unchecked_mut(ss_tail) = i;
1903                *ss_val.get_unchecked_mut(ss_tail) = ss0;
1904            }
1905            ss_tail = nt2;
1906            while ss_head != ss_tail {
1907                let fi = unsafe { *ss_idx.get_unchecked(ss_head) };
1908                if fi + q <= i {
1909                    ss_head = inc(ss_head, cap);
1910                } else {
1911                    break;
1912                }
1913            }
1914            let mn = unsafe { *ss_val.get_unchecked(ss_head) };
1915            *out_short.get_unchecked_mut(i) = mn;
1916        }
1917    };
1918
1919    if parallel {
1920        #[cfg(not(target_arch = "wasm32"))]
1921        {
1922            long_values
1923                .par_chunks_mut(cols)
1924                .zip(short_values.par_chunks_mut(cols))
1925                .enumerate()
1926                .for_each(|(row, (lv, sv))| do_row(row, lv, sv));
1927        }
1928
1929        #[cfg(target_arch = "wasm32")]
1930        {
1931            for (row, (lv, sv)) in long_values
1932                .chunks_mut(cols)
1933                .zip(short_values.chunks_mut(cols))
1934                .enumerate()
1935            {
1936                do_row(row, lv, sv);
1937            }
1938        }
1939    } else {
1940        for (row, (lv, sv)) in long_values
1941            .chunks_mut(cols)
1942            .zip(short_values.chunks_mut(cols))
1943            .enumerate()
1944        {
1945            do_row(row, lv, sv);
1946        }
1947    }
1948
1949    let long_values = unsafe {
1950        Vec::from_raw_parts(
1951            long_guard.as_mut_ptr() as *mut f64,
1952            long_guard.len(),
1953            long_guard.capacity(),
1954        )
1955    };
1956
1957    let short_values = unsafe {
1958        Vec::from_raw_parts(
1959            short_guard.as_mut_ptr() as *mut f64,
1960            short_guard.len(),
1961            short_guard.capacity(),
1962        )
1963    };
1964
1965    Ok(CkspBatchOutput {
1966        long_values,
1967        short_values,
1968        combos,
1969        rows,
1970        cols,
1971    })
1972}
1973
1974#[cfg(test)]
1975mod tests {
1976    use super::*;
1977    use crate::skip_if_unsupported;
1978    use crate::utilities::data_loader::read_candles_from_csv;
1979    use crate::utilities::enums::Kernel;
1980    #[cfg(feature = "proptest")]
1981    use proptest::prelude::*;
1982
1983    fn check_cksp_partial_params(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1984        skip_if_unsupported!(kernel, test_name);
1985        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1986        let candles = read_candles_from_csv(file_path)?;
1987
1988        let default_params = CkspParams {
1989            p: None,
1990            x: None,
1991            q: None,
1992        };
1993        let input = CkspInput::from_candles(&candles, default_params);
1994        let output = cksp_with_kernel(&input, kernel)?;
1995        assert_eq!(output.long_values.len(), candles.close.len());
1996        assert_eq!(output.short_values.len(), candles.close.len());
1997        Ok(())
1998    }
1999
2000    fn check_cksp_accuracy(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2001        skip_if_unsupported!(kernel, test_name);
2002        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2003        let candles = read_candles_from_csv(file_path)?;
2004
2005        let params = CkspParams {
2006            p: Some(10),
2007            x: Some(1.0),
2008            q: Some(9),
2009        };
2010        let input = CkspInput::from_candles(&candles, params);
2011        let output = cksp_with_kernel(&input, kernel)?;
2012
2013        let expected_long_last_5 = [
2014            60306.66197802568,
2015            60306.66197802568,
2016            60306.66197802568,
2017            60203.29578022311,
2018            60201.57958198072,
2019        ];
2020        let l_start = output.long_values.len() - 5;
2021        let long_slice = &output.long_values[l_start..];
2022        for (i, &val) in long_slice.iter().enumerate() {
2023            let exp_val = expected_long_last_5[i];
2024            assert!(
2025                (val - exp_val).abs() < 1e-5,
2026                "[{}] CKSP long mismatch at idx {}: expected {}, got {}",
2027                test_name,
2028                i,
2029                exp_val,
2030                val
2031            );
2032        }
2033
2034        let expected_short_last_5 = [
2035            58757.826484736055,
2036            58701.74383626245,
2037            58656.36945263621,
2038            58611.03250737258,
2039            58611.03250737258,
2040        ];
2041        let s_start = output.short_values.len() - 5;
2042        let short_slice = &output.short_values[s_start..];
2043        for (i, &val) in short_slice.iter().enumerate() {
2044            let exp_val = expected_short_last_5[i];
2045            assert!(
2046                (val - exp_val).abs() < 1e-5,
2047                "[{}] CKSP short mismatch at idx {}: expected {}, got {}",
2048                test_name,
2049                i,
2050                exp_val,
2051                val
2052            );
2053        }
2054        Ok(())
2055    }
2056
2057    fn check_cksp_default_candles(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2058        skip_if_unsupported!(kernel, test_name);
2059        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2060        let candles = read_candles_from_csv(file_path)?;
2061
2062        let input = CkspInput::with_default_candles(&candles);
2063        match input.data {
2064            CkspData::Candles { .. } => {}
2065            _ => panic!("Expected CkspData::Candles"),
2066        }
2067        let output = cksp_with_kernel(&input, kernel)?;
2068        assert_eq!(output.long_values.len(), candles.close.len());
2069        assert_eq!(output.short_values.len(), candles.close.len());
2070        Ok(())
2071    }
2072
2073    fn check_cksp_zero_period(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2074        skip_if_unsupported!(kernel, test_name);
2075        let high = [10.0, 11.0, 12.0];
2076        let low = [9.0, 10.0, 10.5];
2077        let close = [9.5, 10.5, 11.0];
2078        let params = CkspParams {
2079            p: Some(0),
2080            x: Some(1.0),
2081            q: Some(9),
2082        };
2083        let input = CkspInput::from_slices(&high, &low, &close, params);
2084        let res = cksp_with_kernel(&input, kernel);
2085        assert!(
2086            res.is_err(),
2087            "[{}] CKSP should fail with zero period",
2088            test_name
2089        );
2090        Ok(())
2091    }
2092
2093    fn check_cksp_period_exceeds_length(
2094        test_name: &str,
2095        kernel: Kernel,
2096    ) -> Result<(), Box<dyn Error>> {
2097        skip_if_unsupported!(kernel, test_name);
2098        let high = [10.0, 11.0, 12.0];
2099        let low = [9.0, 10.0, 10.5];
2100        let close = [9.5, 10.5, 11.0];
2101        let params = CkspParams {
2102            p: Some(10),
2103            x: Some(1.0),
2104            q: Some(9),
2105        };
2106        let input = CkspInput::from_slices(&high, &low, &close, params);
2107        let res = cksp_with_kernel(&input, kernel);
2108        assert!(
2109            res.is_err(),
2110            "[{}] CKSP should fail with period exceeding length",
2111            test_name
2112        );
2113        Ok(())
2114    }
2115
2116    fn check_cksp_very_small_dataset(
2117        test_name: &str,
2118        kernel: Kernel,
2119    ) -> Result<(), Box<dyn Error>> {
2120        skip_if_unsupported!(kernel, test_name);
2121        let high = [42.0];
2122        let low = [41.0];
2123        let close = [41.5];
2124        let params = CkspParams {
2125            p: Some(10),
2126            x: Some(1.0),
2127            q: Some(9),
2128        };
2129        let input = CkspInput::from_slices(&high, &low, &close, params);
2130        let res = cksp_with_kernel(&input, kernel);
2131        assert!(
2132            res.is_err(),
2133            "[{}] CKSP should fail with insufficient data",
2134            test_name
2135        );
2136        Ok(())
2137    }
2138
2139    fn check_cksp_reinput(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2140        skip_if_unsupported!(kernel, test_name);
2141        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2142        let candles = read_candles_from_csv(file_path)?;
2143
2144        let first_params = CkspParams {
2145            p: Some(10),
2146            x: Some(1.0),
2147            q: Some(9),
2148        };
2149        let first_input = CkspInput::from_candles(&candles, first_params.clone());
2150        let first_result = cksp_with_kernel(&first_input, kernel)?;
2151
2152        let dummy_close = vec![0.0; first_result.long_values.len()];
2153        let second_input = CkspInput::from_slices(
2154            &first_result.long_values,
2155            &first_result.short_values,
2156            &dummy_close,
2157            first_params,
2158        );
2159        let second_result = cksp_with_kernel(&second_input, kernel)?;
2160        assert_eq!(second_result.long_values.len(), dummy_close.len());
2161        assert_eq!(second_result.short_values.len(), dummy_close.len());
2162        Ok(())
2163    }
2164
2165    fn check_cksp_nan_handling(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2166        skip_if_unsupported!(kernel, test_name);
2167        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2168        let candles = read_candles_from_csv(file_path)?;
2169
2170        let input = CkspInput::from_candles(
2171            &candles,
2172            CkspParams {
2173                p: Some(10),
2174                x: Some(1.0),
2175                q: Some(9),
2176            },
2177        );
2178        let res = cksp_with_kernel(&input, kernel)?;
2179        assert_eq!(res.long_values.len(), candles.close.len());
2180        assert_eq!(res.short_values.len(), candles.close.len());
2181        if res.long_values.len() > 240 {
2182            for i in 240..res.long_values.len() {
2183                assert!(
2184                    !res.long_values[i].is_nan(),
2185                    "[{}] Found unexpected NaN in long_values at out-index {}",
2186                    test_name,
2187                    i
2188                );
2189                assert!(
2190                    !res.short_values[i].is_nan(),
2191                    "[{}] Found unexpected NaN in short_values at out-index {}",
2192                    test_name,
2193                    i
2194                );
2195            }
2196        }
2197        Ok(())
2198    }
2199
2200    fn check_cksp_streaming(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2201        skip_if_unsupported!(kernel, test_name);
2202
2203        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2204        let candles = read_candles_from_csv(file_path)?;
2205
2206        let p = 10;
2207        let x = 1.0;
2208        let q = 9;
2209
2210        let input = CkspInput::from_candles(
2211            &candles,
2212            CkspParams {
2213                p: Some(p),
2214                x: Some(x),
2215                q: Some(q),
2216            },
2217        );
2218        let batch_output = cksp_with_kernel(&input, kernel)?;
2219        let mut stream = CkspStream::try_new(CkspParams {
2220            p: Some(p),
2221            x: Some(x),
2222            q: Some(q),
2223        })?;
2224
2225        let mut stream_long = Vec::with_capacity(candles.close.len());
2226        let mut stream_short = Vec::with_capacity(candles.close.len());
2227        for i in 0..candles.close.len() {
2228            let h = candles.high[i];
2229            let l = candles.low[i];
2230            let c = candles.close[i];
2231            match stream.update(h, l, c) {
2232                Some((long, short)) => {
2233                    stream_long.push(long);
2234                    stream_short.push(short);
2235                }
2236                None => {
2237                    stream_long.push(f64::NAN);
2238                    stream_short.push(f64::NAN);
2239                }
2240            }
2241        }
2242        assert_eq!(batch_output.long_values.len(), stream_long.len());
2243        assert_eq!(batch_output.short_values.len(), stream_short.len());
2244        for i in 0..stream_long.len() {
2245            let b_long = batch_output.long_values[i];
2246            let b_short = batch_output.short_values[i];
2247            let s_long = stream_long[i];
2248            let s_short = stream_short[i];
2249            let diff_long = (b_long - s_long).abs();
2250            let diff_short = (b_short - s_short).abs();
2251            if b_long.is_nan() && s_long.is_nan() && b_short.is_nan() && s_short.is_nan() {
2252                continue;
2253            }
2254            assert!(
2255                diff_long < 1e-8,
2256                "[{}] CKSP streaming long f64 mismatch at idx {}: batch={}, stream={}, diff={}",
2257                test_name,
2258                i,
2259                b_long,
2260                s_long,
2261                diff_long
2262            );
2263            assert!(
2264                diff_short < 1e-8,
2265                "[{}] CKSP streaming short f64 mismatch at idx {}: batch={}, stream={}, diff={}",
2266                test_name,
2267                i,
2268                b_short,
2269                s_short,
2270                diff_short
2271            );
2272        }
2273        Ok(())
2274    }
2275
2276    #[cfg(debug_assertions)]
2277    fn check_cksp_no_poison(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2278        skip_if_unsupported!(kernel, test_name);
2279
2280        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2281        let candles = read_candles_from_csv(file_path)?;
2282
2283        let input = CkspInput::from_candles(&candles, CkspParams::default());
2284        let output = cksp_with_kernel(&input, kernel)?;
2285
2286        for (i, &val) in output.long_values.iter().enumerate() {
2287            if val.is_nan() {
2288                continue;
2289            }
2290
2291            let bits = val.to_bits();
2292
2293            if bits == 0x11111111_11111111 {
2294                panic!(
2295					"[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at index {} in long_values",
2296					test_name, val, bits, i
2297				);
2298            }
2299
2300            if bits == 0x22222222_22222222 {
2301                panic!(
2302					"[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at index {} in long_values",
2303					test_name, val, bits, i
2304				);
2305            }
2306
2307            if bits == 0x33333333_33333333 {
2308                panic!(
2309					"[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at index {} in long_values",
2310					test_name, val, bits, i
2311				);
2312            }
2313        }
2314
2315        for (i, &val) in output.short_values.iter().enumerate() {
2316            if val.is_nan() {
2317                continue;
2318            }
2319
2320            let bits = val.to_bits();
2321
2322            if bits == 0x11111111_11111111 {
2323                panic!(
2324					"[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at index {} in short_values",
2325					test_name, val, bits, i
2326				);
2327            }
2328
2329            if bits == 0x22222222_22222222 {
2330                panic!(
2331					"[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at index {} in short_values",
2332					test_name, val, bits, i
2333				);
2334            }
2335
2336            if bits == 0x33333333_33333333 {
2337                panic!(
2338					"[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at index {} in short_values",
2339					test_name, val, bits, i
2340				);
2341            }
2342        }
2343
2344        let param_combos = vec![
2345            CkspParams {
2346                p: Some(5),
2347                x: Some(0.5),
2348                q: Some(5),
2349            },
2350            CkspParams {
2351                p: Some(20),
2352                x: Some(2.0),
2353                q: Some(15),
2354            },
2355            CkspParams {
2356                p: Some(30),
2357                x: Some(1.5),
2358                q: Some(20),
2359            },
2360        ];
2361
2362        for params in param_combos {
2363            let input = CkspInput::from_candles(&candles, params.clone());
2364            let output = cksp_with_kernel(&input, kernel)?;
2365
2366            for (i, &val) in output.long_values.iter().enumerate() {
2367                if val.is_nan() {
2368                    continue;
2369                }
2370
2371                let bits = val.to_bits();
2372                if bits == 0x11111111_11111111
2373                    || bits == 0x22222222_22222222
2374                    || bits == 0x33333333_33333333
2375                {
2376                    panic!(
2377                        "[{}] Found poison value {} (0x{:016X}) at index {} in long_values with params p={}, x={}, q={}",
2378                        test_name, val, bits, i, params.p.unwrap(), params.x.unwrap(), params.q.unwrap()
2379                    );
2380                }
2381            }
2382
2383            for (i, &val) in output.short_values.iter().enumerate() {
2384                if val.is_nan() {
2385                    continue;
2386                }
2387
2388                let bits = val.to_bits();
2389                if bits == 0x11111111_11111111
2390                    || bits == 0x22222222_22222222
2391                    || bits == 0x33333333_33333333
2392                {
2393                    panic!(
2394                        "[{}] Found poison value {} (0x{:016X}) at index {} in short_values with params p={}, x={}, q={}",
2395                        test_name, val, bits, i, params.p.unwrap(), params.x.unwrap(), params.q.unwrap()
2396                    );
2397                }
2398            }
2399        }
2400
2401        Ok(())
2402    }
2403
2404    #[cfg(not(debug_assertions))]
2405    fn check_cksp_no_poison(_test_name: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
2406        Ok(())
2407    }
2408
2409    #[cfg(feature = "proptest")]
2410    #[allow(clippy::float_cmp)]
2411    fn check_cksp_property(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2412        skip_if_unsupported!(kernel, test_name);
2413
2414        let strat = (1usize..=64).prop_flat_map(|p| {
2415            (1usize..=20).prop_flat_map(move |q| {
2416                (
2417                    prop::collection::vec(
2418                        (10.0f64..1000.0f64).prop_filter("finite", |x| x.is_finite()),
2419                        (p + q)..400,
2420                    ),
2421                    Just(p),
2422                    (0.1f64..10.0f64).prop_filter("finite", |x| x.is_finite()),
2423                    Just(q),
2424                )
2425            })
2426        });
2427
2428        proptest::test_runner::TestRunner::default()
2429            .run(&strat, |(base_prices, p, x, q)| {
2430                let mut high = Vec::with_capacity(base_prices.len());
2431                let mut low = Vec::with_capacity(base_prices.len());
2432                let mut close = Vec::with_capacity(base_prices.len());
2433
2434                for (i, price) in base_prices.iter().enumerate() {
2435                    let volatility = price * 0.02;
2436                    let h = price + volatility;
2437                    let l = price - volatility;
2438                    high.push(h);
2439                    low.push(l);
2440
2441                    let close_factor = 0.3 + 0.4 * ((i % 3) as f64 / 2.0);
2442                    close.push(l + (h - l) * close_factor);
2443                }
2444
2445                let params = CkspParams {
2446                    p: Some(p),
2447                    x: Some(x),
2448                    q: Some(q),
2449                };
2450                let input = CkspInput::from_slices(&high, &low, &close, params);
2451
2452                let result = cksp_with_kernel(&input, kernel)?;
2453                let CkspOutput {
2454                    long_values,
2455                    short_values,
2456                } = result;
2457
2458                prop_assert_eq!(
2459                    long_values.len(),
2460                    close.len(),
2461                    "Long values length mismatch"
2462                );
2463                prop_assert_eq!(
2464                    short_values.len(),
2465                    close.len(),
2466                    "Short values length mismatch"
2467                );
2468
2469                let first_long_valid = long_values.iter().position(|&v| v.is_finite());
2470                let first_short_valid = short_values.iter().position(|&v| v.is_finite());
2471
2472                if let (Some(long_idx), Some(short_idx)) = (first_long_valid, first_short_valid) {
2473                    prop_assert_eq!(
2474                        long_idx,
2475                        short_idx,
2476                        "First valid indices should match: long={}, short={}",
2477                        long_idx,
2478                        short_idx
2479                    );
2480
2481                    for i in 0..long_idx {
2482                        prop_assert!(
2483                            long_values[i].is_nan(),
2484                            "idx {}: long value should be NaN before first valid ({}), got {}",
2485                            i,
2486                            long_idx,
2487                            long_values[i]
2488                        );
2489                        prop_assert!(
2490                            short_values[i].is_nan(),
2491                            "idx {}: short value should be NaN before first valid ({}), got {}",
2492                            i,
2493                            short_idx,
2494                            short_values[i]
2495                        );
2496                    }
2497
2498                    prop_assert!(
2499                        long_idx >= p - 1,
2500                        "Warmup period {} should be at least p - 1 = {}",
2501                        long_idx,
2502                        p - 1
2503                    );
2504
2505                    let max_warmup = p + q - 1;
2506                    prop_assert!(
2507                        long_idx <= max_warmup,
2508                        "Warmup period {} should not exceed p + q - 1 = {}",
2509                        long_idx,
2510                        max_warmup
2511                    );
2512                }
2513
2514                if let Some(first_valid) = first_long_valid {
2515                    for i in first_valid..close.len() {
2516                        prop_assert!(
2517                            long_values[i].is_finite(),
2518                            "idx {}: long value should be finite after warmup, got {}",
2519                            i,
2520                            long_values[i]
2521                        );
2522                        prop_assert!(
2523                            short_values[i].is_finite(),
2524                            "idx {}: short value should be finite after warmup, got {}",
2525                            i,
2526                            short_values[i]
2527                        );
2528                    }
2529                }
2530
2531                if kernel != Kernel::Scalar {
2532                    let scalar_result = cksp_with_kernel(&input, Kernel::Scalar)?;
2533                    let CkspOutput {
2534                        long_values: scalar_long,
2535                        short_values: scalar_short,
2536                    } = scalar_result;
2537
2538                    let start_idx = first_long_valid.unwrap_or(0);
2539                    for i in start_idx..close.len() {
2540                        let long_val = long_values[i];
2541                        let scalar_long_val = scalar_long[i];
2542                        let short_val = short_values[i];
2543                        let scalar_short_val = scalar_short[i];
2544
2545                        if long_val.is_finite() && scalar_long_val.is_finite() {
2546                            let long_bits = long_val.to_bits();
2547                            let scalar_long_bits = scalar_long_val.to_bits();
2548                            let ulp_diff = long_bits.abs_diff(scalar_long_bits);
2549
2550                            prop_assert!(
2551                                (long_val - scalar_long_val).abs() <= 1e-9 || ulp_diff <= 8,
2552                                "Long value mismatch at idx {}: {} vs {} (ULP={})",
2553                                i,
2554                                long_val,
2555                                scalar_long_val,
2556                                ulp_diff
2557                            );
2558                        }
2559
2560                        if short_val.is_finite() && scalar_short_val.is_finite() {
2561                            let short_bits = short_val.to_bits();
2562                            let scalar_short_bits = scalar_short_val.to_bits();
2563                            let ulp_diff = short_bits.abs_diff(scalar_short_bits);
2564
2565                            prop_assert!(
2566                                (short_val - scalar_short_val).abs() <= 1e-9 || ulp_diff <= 8,
2567                                "Short value mismatch at idx {}: {} vs {} (ULP={})",
2568                                i,
2569                                short_val,
2570                                scalar_short_val,
2571                                ulp_diff
2572                            );
2573                        }
2574                    }
2575                }
2576
2577                let start_idx = first_long_valid.unwrap_or(0);
2578                if start_idx < close.len() {
2579                    let mut max_tr: f64 = 0.0;
2580                    for j in start_idx.saturating_sub(p)..start_idx {
2581                        if j < high.len() {
2582                            let tr = high[j] - low[j];
2583                            max_tr = max_tr.max(tr);
2584                        }
2585                    }
2586
2587                    let price_max = high.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
2588                    let price_min = low.iter().cloned().fold(f64::INFINITY, f64::min);
2589
2590                    for i in start_idx..close.len() {
2591                        prop_assert!(
2592                            long_values[i].is_finite(),
2593                            "Long stop should be finite at idx {}: {}",
2594                            i,
2595                            long_values[i]
2596                        );
2597                        prop_assert!(
2598                            short_values[i].is_finite(),
2599                            "Short stop should be finite at idx {}: {}",
2600                            i,
2601                            short_values[i]
2602                        );
2603
2604                        let price_range = price_max - price_min;
2605                        let margin = price_range * 2.0;
2606
2607                        prop_assert!(
2608                            long_values[i] <= price_max + margin,
2609                            "Long stop {} should be <= max_price {} + margin {} at idx {}",
2610                            long_values[i],
2611                            price_max,
2612                            margin,
2613                            i
2614                        );
2615
2616                        prop_assert!(
2617                            short_values[i] >= price_min - margin,
2618                            "Short stop {} should be >= min_price {} - margin {} at idx {}",
2619                            short_values[i],
2620                            price_min,
2621                            margin,
2622                            i
2623                        );
2624                    }
2625                }
2626
2627                if p == 1 && q == 1 {
2628                    let start_check = first_long_valid.unwrap_or(0).saturating_add(1);
2629                    for i in start_check..close.len() {
2630                        prop_assert!(
2631                            long_values[i].is_finite(),
2632                            "Long stop should be finite with p=1,q=1 at idx {}: {}",
2633                            i,
2634                            long_values[i]
2635                        );
2636                        prop_assert!(
2637                            short_values[i].is_finite(),
2638                            "Short stop should be finite with p=1,q=1 at idx {}: {}",
2639                            i,
2640                            short_values[i]
2641                        );
2642
2643                        let recent_high = high[i];
2644                        let recent_low = low[i];
2645                        let recent_range = recent_high - recent_low;
2646
2647                        prop_assert!(
2648                            long_values[i] <= recent_high,
2649                            "With p=1,q=1: Long stop {} should be <= recent high {} at idx {}",
2650                            long_values[i],
2651                            recent_high,
2652                            i
2653                        );
2654
2655                        prop_assert!(
2656                            short_values[i] >= recent_low,
2657                            "With p=1,q=1: Short stop {} should be >= recent low {} at idx {}",
2658                            short_values[i],
2659                            recent_low,
2660                            i
2661                        );
2662                    }
2663                }
2664
2665                if x > 1.0 {
2666                    let smaller_x = x * 0.5;
2667                    let params_small = CkspParams {
2668                        p: Some(p),
2669                        x: Some(smaller_x),
2670                        q: Some(q),
2671                    };
2672                    let input_small = CkspInput::from_slices(&high, &low, &close, params_small);
2673                    if let Ok(result_small) = cksp_with_kernel(&input_small, kernel) {
2674                        let CkspOutput {
2675                            long_values: long_small,
2676                            short_values: short_small,
2677                        } = result_small;
2678
2679                        if let Some(start) = first_long_valid {
2680                            let sample_points = 5.min((close.len() - start) / 2);
2681                            for offset in 0..sample_points {
2682                                let idx = start + offset * 2;
2683                                if idx < close.len() {
2684                                    let spread_large = (short_values[idx] - long_values[idx]).abs();
2685                                    let spread_small = (short_small[idx] - long_small[idx]).abs();
2686
2687                                    if spread_small > 0.0 {
2688                                        prop_assert!(
2689											spread_large > 0.0 && spread_small > 0.0,
2690											"At idx {}: Both spreads should be positive: large={}, small={}",
2691											idx,
2692											spread_large,
2693											spread_small
2694										);
2695                                    }
2696                                }
2697                            }
2698                        }
2699                    }
2700                }
2701
2702                if q > 2 && p < 10 {
2703                    let smaller_q = 1;
2704                    let params_small_q = CkspParams {
2705                        p: Some(p),
2706                        x: Some(x),
2707                        q: Some(smaller_q),
2708                    };
2709                    let input_small_q = CkspInput::from_slices(&high, &low, &close, params_small_q);
2710                    if let Ok(result_small_q) = cksp_with_kernel(&input_small_q, kernel) {
2711                        let CkspOutput {
2712                            long_values: long_small_q,
2713                            short_values: short_small_q,
2714                        } = result_small_q;
2715
2716                        let start = (p + q).max(p + smaller_q);
2717                        if start + 10 < close.len() {
2718                            let mut volatility_large_q = 0.0;
2719                            let mut volatility_small_q = 0.0;
2720
2721                            for i in start..(start + 10) {
2722                                if i > 0 && i < close.len() {
2723                                    volatility_large_q +=
2724                                        (long_values[i] - long_values[i - 1]).abs();
2725                                    volatility_small_q +=
2726                                        (long_small_q[i] - long_small_q[i - 1]).abs();
2727                                }
2728                            }
2729
2730                            prop_assert!(
2731                                volatility_large_q.is_finite() && volatility_small_q.is_finite(),
2732                                "Volatilities should be finite: large_q={}, small_q={}",
2733                                volatility_large_q,
2734                                volatility_small_q
2735                            );
2736                        }
2737                    }
2738                }
2739
2740                if base_prices.windows(2).all(|w| (w[0] - w[1]).abs() < 1e-10) {
2741                    let last_idx = close.len() - 1;
2742                    let min_converge_idx = first_long_valid.unwrap_or(0) + p * 2;
2743                    if last_idx > min_converge_idx {
2744                        let constant_price = base_prices[0];
2745                        let constant_volatility = constant_price * 0.02;
2746
2747                        let expected_long =
2748                            constant_price + constant_volatility - x * (2.0 * constant_volatility);
2749                        let expected_short =
2750                            constant_price - constant_volatility + x * (2.0 * constant_volatility);
2751
2752                        let long_val = long_values[last_idx];
2753                        let short_val = short_values[last_idx];
2754
2755                        let tolerance = constant_price * 0.2;
2756
2757                        prop_assert!(
2758							(long_val - expected_long).abs() <= tolerance,
2759							"With constant price {}: Long stop {} should converge near {} (within {})",
2760							constant_price,
2761							long_val,
2762							expected_long,
2763							tolerance
2764						);
2765
2766                        prop_assert!(
2767							(short_val - expected_short).abs() <= tolerance,
2768							"With constant price {}: Short stop {} should converge near {} (within {})",
2769							constant_price,
2770							short_val,
2771							expected_short,
2772							tolerance
2773						);
2774
2775                        if last_idx >= 3 {
2776                            let long_stable = (long_values[last_idx] - long_values[last_idx - 1])
2777                                .abs()
2778                                < constant_volatility * 0.1;
2779                            let short_stable =
2780                                (short_values[last_idx] - short_values[last_idx - 1]).abs()
2781                                    < constant_volatility * 0.1;
2782
2783                            prop_assert!(
2784                                long_stable && short_stable,
2785                                "Stops should stabilize: Long diff {}, Short diff {}",
2786                                (long_values[last_idx] - long_values[last_idx - 1]).abs(),
2787                                (short_values[last_idx] - short_values[last_idx - 1]).abs()
2788                            );
2789                        }
2790                    }
2791                }
2792
2793                Ok(())
2794            })
2795            .unwrap();
2796
2797        Ok(())
2798    }
2799
2800    macro_rules! generate_all_cksp_tests {
2801        ($($test_fn:ident),*) => {
2802            paste::paste! {
2803                $(
2804                    #[test]
2805                    fn [<$test_fn _scalar_f64>]() {
2806                        let _ = $test_fn(stringify!([<$test_fn _scalar_f64>]), Kernel::Scalar);
2807                    }
2808                )*
2809                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2810                $(
2811                    #[test]
2812                    fn [<$test_fn _avx2_f64>]() {
2813                        let _ = $test_fn(stringify!([<$test_fn _avx2_f64>]), Kernel::Avx2);
2814                    }
2815                    #[test]
2816                    fn [<$test_fn _avx512_f64>]() {
2817                        let _ = $test_fn(stringify!([<$test_fn _avx512_f64>]), Kernel::Avx512);
2818                    }
2819                )*
2820            }
2821        }
2822    }
2823
2824    fn check_cksp_empty_input(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2825        skip_if_unsupported!(kernel, test_name);
2826        let empty: [f64; 0] = [];
2827        let input = CkspInput::from_slices(&empty, &empty, &empty, CkspParams::default());
2828        let res = cksp_with_kernel(&input, kernel);
2829        assert!(
2830            matches!(res, Err(CkspError::EmptyInputData)),
2831            "[{}] CKSP should fail with empty input",
2832            test_name
2833        );
2834        Ok(())
2835    }
2836
2837    fn check_cksp_invalid_x_param(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2838        skip_if_unsupported!(kernel, test_name);
2839        let high = [10.0, 11.0, 12.0, 13.0, 14.0];
2840        let low = [9.0, 10.0, 11.0, 12.0, 13.0];
2841        let close = [9.5, 10.5, 11.5, 12.5, 13.5];
2842        let params = CkspParams {
2843            p: Some(2),
2844            x: Some(f64::NAN),
2845            q: Some(2),
2846        };
2847        let input = CkspInput::from_slices(&high, &low, &close, params);
2848        let res = cksp_with_kernel(&input, kernel);
2849        assert!(
2850            matches!(res, Err(CkspError::InvalidMultiplier { .. })),
2851            "[{}] CKSP should fail with invalid x parameter (NaN)",
2852            test_name
2853        );
2854        Ok(())
2855    }
2856
2857    fn check_cksp_invalid_q_param(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2858        skip_if_unsupported!(kernel, test_name);
2859        let high = [10.0, 11.0, 12.0, 13.0, 14.0];
2860        let low = [9.0, 10.0, 11.0, 12.0, 13.0];
2861        let close = [9.5, 10.5, 11.5, 12.5, 13.5];
2862        let params = CkspParams {
2863            p: Some(2),
2864            x: Some(1.0),
2865            q: Some(0),
2866        };
2867        let input = CkspInput::from_slices(&high, &low, &close, params);
2868        let res = cksp_with_kernel(&input, kernel);
2869        assert!(
2870            matches!(res, Err(CkspError::InvalidParam { .. })),
2871            "[{}] CKSP should fail with invalid q parameter (0)",
2872            test_name
2873        );
2874        Ok(())
2875    }
2876
2877    generate_all_cksp_tests!(
2878        check_cksp_partial_params,
2879        check_cksp_accuracy,
2880        check_cksp_default_candles,
2881        check_cksp_zero_period,
2882        check_cksp_period_exceeds_length,
2883        check_cksp_very_small_dataset,
2884        check_cksp_empty_input,
2885        check_cksp_invalid_x_param,
2886        check_cksp_invalid_q_param,
2887        check_cksp_reinput,
2888        check_cksp_nan_handling,
2889        check_cksp_streaming,
2890        check_cksp_no_poison
2891    );
2892
2893    #[cfg(feature = "proptest")]
2894    generate_all_cksp_tests!(check_cksp_property);
2895
2896    fn check_batch_default_row(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2897        skip_if_unsupported!(kernel, test);
2898
2899        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2900        let c = read_candles_from_csv(file)?;
2901
2902        let output = CkspBatchBuilder::new().kernel(kernel).apply_candles(&c)?;
2903
2904        let def = CkspParams::default();
2905        let (long_row, short_row) = output.values_for(&def).expect("default row missing");
2906
2907        assert_eq!(long_row.len(), c.close.len());
2908        assert_eq!(short_row.len(), c.close.len());
2909
2910        let expected_long = [
2911            60306.66197802568,
2912            60306.66197802568,
2913            60306.66197802568,
2914            60203.29578022311,
2915            60201.57958198072,
2916        ];
2917        let start = long_row.len() - 5;
2918        for (i, &v) in long_row[start..].iter().enumerate() {
2919            assert!(
2920                (v - expected_long[i]).abs() < 1e-5,
2921                "[{test}] default-row long mismatch at idx {i}: {v} vs {expected_long:?}"
2922            );
2923        }
2924
2925        let expected_short = [
2926            58757.826484736055,
2927            58701.74383626245,
2928            58656.36945263621,
2929            58611.03250737258,
2930            58611.03250737258,
2931        ];
2932        for (i, &v) in short_row[start..].iter().enumerate() {
2933            assert!(
2934                (v - expected_short[i]).abs() < 1e-5,
2935                "[{test}] default-row short mismatch at idx {i}: {v} vs {expected_short:?}"
2936            );
2937        }
2938        Ok(())
2939    }
2940
2941    #[cfg(debug_assertions)]
2942    fn check_batch_no_poison(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2943        skip_if_unsupported!(kernel, test);
2944
2945        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2946        let c = read_candles_from_csv(file)?;
2947
2948        let output = CkspBatchBuilder::new()
2949            .kernel(kernel)
2950            .p_range(5, 25, 5)
2951            .x_range(0.5, 2.5, 0.5)
2952            .q_range(5, 20, 5)
2953            .apply_candles(&c)?;
2954
2955        for (idx, &val) in output.long_values.iter().enumerate() {
2956            if val.is_nan() {
2957                continue;
2958            }
2959
2960            let bits = val.to_bits();
2961            let row = idx / output.cols;
2962            let col = idx % output.cols;
2963
2964            if bits == 0x11111111_11111111 {
2965                panic!(
2966                    "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at row {} col {} (flat index {}) in long_values",
2967                    test, val, bits, row, col, idx
2968                );
2969            }
2970
2971            if bits == 0x22222222_22222222 {
2972                panic!(
2973                    "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at row {} col {} (flat index {}) in long_values",
2974                    test, val, bits, row, col, idx
2975                );
2976            }
2977
2978            if bits == 0x33333333_33333333 {
2979                panic!(
2980                    "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at row {} col {} (flat index {}) in long_values",
2981                    test, val, bits, row, col, idx
2982                );
2983            }
2984        }
2985
2986        for (idx, &val) in output.short_values.iter().enumerate() {
2987            if val.is_nan() {
2988                continue;
2989            }
2990
2991            let bits = val.to_bits();
2992            let row = idx / output.cols;
2993            let col = idx % output.cols;
2994
2995            if bits == 0x11111111_11111111 {
2996                panic!(
2997                    "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at row {} col {} (flat index {}) in short_values",
2998                    test, val, bits, row, col, idx
2999                );
3000            }
3001
3002            if bits == 0x22222222_22222222 {
3003                panic!(
3004                    "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at row {} col {} (flat index {}) in short_values",
3005                    test, val, bits, row, col, idx
3006                );
3007            }
3008
3009            if bits == 0x33333333_33333333 {
3010                panic!(
3011                    "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at row {} col {} (flat index {}) in short_values",
3012                    test, val, bits, row, col, idx
3013                );
3014            }
3015        }
3016
3017        Ok(())
3018    }
3019
3020    #[cfg(not(debug_assertions))]
3021    fn check_batch_no_poison(_test: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
3022        Ok(())
3023    }
3024
3025    macro_rules! gen_batch_tests {
3026        ($fn_name:ident) => {
3027            paste::paste! {
3028                #[test] fn [<$fn_name _scalar>]()      {
3029                    let _ = $fn_name(stringify!([<$fn_name _scalar>]), Kernel::ScalarBatch);
3030                }
3031                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
3032                #[test] fn [<$fn_name _avx2>]()        {
3033                    let _ = $fn_name(stringify!([<$fn_name _avx2>]), Kernel::Avx2Batch);
3034                }
3035                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
3036                #[test] fn [<$fn_name _avx512>]()      {
3037                    let _ = $fn_name(stringify!([<$fn_name _avx512>]), Kernel::Avx512Batch);
3038                }
3039                #[test] fn [<$fn_name _auto_detect>]() {
3040                    let _ = $fn_name(stringify!([<$fn_name _auto_detect>]), Kernel::Auto);
3041                }
3042            }
3043        };
3044    }
3045    gen_batch_tests!(check_batch_default_row);
3046    gen_batch_tests!(check_batch_no_poison);
3047
3048    #[test]
3049    fn test_cksp_into_matches_api() -> Result<(), Box<dyn Error>> {
3050        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
3051        let candles = read_candles_from_csv(file_path)?;
3052
3053        let input = CkspInput::from_candles(&candles, CkspParams::default());
3054
3055        let baseline = cksp(&input)?;
3056
3057        let n = candles.close.len();
3058        let mut out_long = vec![0.0; n];
3059        let mut out_short = vec![0.0; n];
3060
3061        #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
3062        {
3063            cksp_into(&input, &mut out_long, &mut out_short)?;
3064        }
3065
3066        #[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3067        {
3068            cksp_into_slices(&mut out_long, &mut out_short, &input, Kernel::Auto)?;
3069        }
3070
3071        assert_eq!(baseline.long_values.len(), out_long.len());
3072        assert_eq!(baseline.short_values.len(), out_short.len());
3073
3074        fn eq_or_both_nan(a: f64, b: f64) -> bool {
3075            (a.is_nan() && b.is_nan()) || (a - b).abs() <= 1e-12
3076        }
3077
3078        for i in 0..n {
3079            assert!(
3080                eq_or_both_nan(baseline.long_values[i], out_long[i]),
3081                "long mismatch at {}: baseline={}, into={}",
3082                i,
3083                baseline.long_values[i],
3084                out_long[i]
3085            );
3086            assert!(
3087                eq_or_both_nan(baseline.short_values[i], out_short[i]),
3088                "short mismatch at {}: baseline={}, into={}",
3089                i,
3090                baseline.short_values[i],
3091                out_short[i]
3092            );
3093        }
3094
3095        Ok(())
3096    }
3097}
3098
3099#[cfg(feature = "python")]
3100#[inline(always)]
3101fn cksp_prepare(
3102    high: &[f64],
3103    low: &[f64],
3104    close: &[f64],
3105    p: usize,
3106    x: f64,
3107    q: usize,
3108    kernel: Kernel,
3109) -> Result<(usize, Kernel), CkspError> {
3110    if p == 0 || q == 0 {
3111        return Err(CkspError::InvalidParam { param: "p/q" });
3112    }
3113    if !x.is_finite() {
3114        return Err(CkspError::InvalidMultiplier { x });
3115    }
3116
3117    let size = close.len();
3118    if size == 0 {
3119        return Err(CkspError::EmptyInputData);
3120    }
3121    if high.len() != low.len() || low.len() != close.len() {
3122        return Err(CkspError::InconsistentLengths);
3123    }
3124    let first_valid_idx = match close.iter().position(|&v| !v.is_nan()) {
3125        Some(idx) => idx,
3126        None => return Err(CkspError::AllValuesNaN),
3127    };
3128    let valid = size - first_valid_idx;
3129    let warmup = p
3130        .checked_add(q)
3131        .and_then(|v| v.checked_sub(1))
3132        .ok_or_else(|| CkspError::InvalidInput("warmup overflow (p+q too large)".into()))?;
3133    if valid <= warmup {
3134        let needed = warmup
3135            .checked_add(1)
3136            .ok_or_else(|| CkspError::InvalidInput("warmup+1 overflow".into()))?;
3137        return Err(CkspError::NotEnoughValidData { needed, valid });
3138    }
3139
3140    let chosen = match kernel {
3141        Kernel::Auto => Kernel::Scalar,
3142        other => other,
3143    };
3144
3145    Ok((first_valid_idx, chosen))
3146}
3147
3148#[cfg(feature = "python")]
3149#[pyfunction(name = "cksp")]
3150#[pyo3(signature = (high, low, close, p=10, x=1.0, q=9, kernel=None))]
3151pub fn cksp_py<'py>(
3152    py: Python<'py>,
3153    high: PyReadonlyArray1<'py, f64>,
3154    low: PyReadonlyArray1<'py, f64>,
3155    close: PyReadonlyArray1<'py, f64>,
3156    p: usize,
3157    x: f64,
3158    q: usize,
3159    kernel: Option<&str>,
3160) -> PyResult<(Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<f64>>)> {
3161    use numpy::{IntoPyArray, PyArrayMethods};
3162
3163    let high_slice = high.as_slice()?;
3164    let low_slice = low.as_slice()?;
3165    let close_slice = close.as_slice()?;
3166    let kern = validate_kernel(kernel, false)?;
3167
3168    let (first_valid_idx, chosen) = cksp_prepare(high_slice, low_slice, close_slice, p, x, q, kern)
3169        .map_err(|e| PyValueError::new_err(e.to_string()))?;
3170
3171    let result = py
3172        .allow_threads(|| unsafe {
3173            match chosen {
3174                Kernel::Scalar | Kernel::ScalarBatch => {
3175                    cksp_scalar(high_slice, low_slice, close_slice, p, x, q, first_valid_idx)
3176                }
3177                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
3178                Kernel::Avx2 | Kernel::Avx2Batch => {
3179                    cksp_avx2(high_slice, low_slice, close_slice, p, x, q, first_valid_idx)
3180                }
3181                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
3182                Kernel::Avx512 | Kernel::Avx512Batch => {
3183                    cksp_avx512(high_slice, low_slice, close_slice, p, x, q, first_valid_idx)
3184                }
3185                _ => unreachable!(),
3186            }
3187        })
3188        .map_err(|e| PyValueError::new_err(e.to_string()))?;
3189
3190    Ok((
3191        result.long_values.into_pyarray(py),
3192        result.short_values.into_pyarray(py),
3193    ))
3194}
3195
3196#[cfg(feature = "python")]
3197#[pyclass(name = "CkspStream")]
3198pub struct CkspStreamPy {
3199    inner: CkspStream,
3200}
3201
3202#[cfg(feature = "python")]
3203#[pymethods]
3204impl CkspStreamPy {
3205    #[new]
3206    pub fn new(p: usize, x: f64, q: usize) -> PyResult<Self> {
3207        let params = CkspParams {
3208            p: Some(p),
3209            x: Some(x),
3210            q: Some(q),
3211        };
3212        let inner =
3213            CkspStream::try_new(params).map_err(|e| PyValueError::new_err(e.to_string()))?;
3214        Ok(CkspStreamPy { inner })
3215    }
3216
3217    pub fn update(&mut self, high: f64, low: f64, close: f64) -> Option<(f64, f64)> {
3218        self.inner.update(high, low, close)
3219    }
3220}
3221
3222#[inline(always)]
3223fn cksp_batch_inner_into(
3224    high: &[f64],
3225    low: &[f64],
3226    close: &[f64],
3227    sweep: &CkspBatchRange,
3228    kern: Kernel,
3229    parallel: bool,
3230    long_out: &mut [f64],
3231    short_out: &mut [f64],
3232) -> Result<Vec<CkspParams>, CkspError> {
3233    let combos = expand_grid(sweep)?;
3234    if combos.is_empty() {
3235        return Err(CkspError::InvalidParam { param: "combos" });
3236    }
3237    let size = close.len();
3238    if high.len() != low.len() || low.len() != close.len() {
3239        return Err(CkspError::InconsistentLengths);
3240    }
3241    let first_valid = close
3242        .iter()
3243        .position(|x| !x.is_nan())
3244        .ok_or(CkspError::AllValuesNaN)?;
3245
3246    let rows = combos.len();
3247    let cols = size;
3248    let expected = rows
3249        .checked_mul(cols)
3250        .ok_or_else(|| CkspError::InvalidInput("rows*cols overflow".into()))?;
3251    if long_out.len() != expected {
3252        return Err(CkspError::OutputLengthMismatch {
3253            expected,
3254            got: long_out.len(),
3255        });
3256    }
3257    if short_out.len() != expected {
3258        return Err(CkspError::OutputLengthMismatch {
3259            expected,
3260            got: short_out.len(),
3261        });
3262    }
3263    let valid = size - first_valid;
3264    let mut warm: Vec<usize> = Vec::with_capacity(rows);
3265    for c in &combos {
3266        let p_row = c.p.unwrap_or(10);
3267        let q_row = c.q.unwrap_or(9);
3268        let warm_rel = p_row
3269            .checked_add(q_row)
3270            .and_then(|v| v.checked_sub(1))
3271            .ok_or_else(|| CkspError::InvalidInput("warmup overflow (p+q too large)".into()))?;
3272        if valid <= warm_rel {
3273            let needed = warm_rel
3274                .checked_add(1)
3275                .ok_or_else(|| CkspError::InvalidInput("warmup+1 overflow".into()))?;
3276            return Err(CkspError::NotEnoughValidData { needed, valid });
3277        }
3278        let warm_idx = first_valid
3279            .checked_add(warm_rel)
3280            .ok_or_else(|| CkspError::InvalidInput("warmup index overflow".into()))?;
3281        warm.push(warm_idx);
3282    }
3283
3284    unsafe {
3285        let mut long_mu = core::slice::from_raw_parts_mut(
3286            long_out.as_mut_ptr() as *mut MaybeUninit<f64>,
3287            long_out.len(),
3288        );
3289        let mut short_mu = core::slice::from_raw_parts_mut(
3290            short_out.as_mut_ptr() as *mut MaybeUninit<f64>,
3291            short_out.len(),
3292        );
3293        init_matrix_prefixes(&mut long_mu, cols, &warm);
3294        init_matrix_prefixes(&mut short_mu, cols, &warm);
3295    }
3296
3297    let do_row = |row: usize, out_long: &mut [f64], out_short: &mut [f64]| unsafe {
3298        let prm = &combos[row];
3299        let (p, x, q) = (prm.p.unwrap(), prm.x.unwrap(), prm.q.unwrap());
3300        match kern {
3301            Kernel::Scalar => {
3302                cksp_row_scalar(high, low, close, p, x, q, first_valid, out_long, out_short)
3303            }
3304            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
3305            Kernel::Avx2 => {
3306                cksp_row_avx2(high, low, close, p, x, q, first_valid, out_long, out_short)
3307            }
3308            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
3309            Kernel::Avx512 => {
3310                cksp_row_avx512(high, low, close, p, x, q, first_valid, out_long, out_short)
3311            }
3312            _ => unreachable!(),
3313        }
3314    };
3315
3316    if parallel {
3317        #[cfg(not(target_arch = "wasm32"))]
3318        {
3319            long_out
3320                .par_chunks_mut(cols)
3321                .zip(short_out.par_chunks_mut(cols))
3322                .enumerate()
3323                .for_each(|(row, (lv, sv))| do_row(row, lv, sv));
3324        }
3325
3326        #[cfg(target_arch = "wasm32")]
3327        {
3328            for (row, (lv, sv)) in long_out
3329                .chunks_mut(cols)
3330                .zip(short_out.chunks_mut(cols))
3331                .enumerate()
3332            {
3333                do_row(row, lv, sv);
3334            }
3335        }
3336    } else {
3337        for (row, (lv, sv)) in long_out
3338            .chunks_mut(cols)
3339            .zip(short_out.chunks_mut(cols))
3340            .enumerate()
3341        {
3342            do_row(row, lv, sv);
3343        }
3344    }
3345
3346    Ok(combos)
3347}
3348
3349#[cfg(feature = "python")]
3350#[pyfunction(name = "cksp_batch")]
3351#[pyo3(signature = (high, low, close, p_range=(10, 10, 0), x_range=(1.0, 1.0, 0.0), q_range=(9, 9, 0), kernel=None))]
3352pub fn cksp_batch_py<'py>(
3353    py: Python<'py>,
3354    high: PyReadonlyArray1<'py, f64>,
3355    low: PyReadonlyArray1<'py, f64>,
3356    close: PyReadonlyArray1<'py, f64>,
3357    p_range: (usize, usize, usize),
3358    x_range: (f64, f64, f64),
3359    q_range: (usize, usize, usize),
3360    kernel: Option<&str>,
3361) -> PyResult<Bound<'py, PyDict>> {
3362    use numpy::{IntoPyArray, PyArrayMethods};
3363
3364    let high_slice = high.as_slice()?;
3365    let low_slice = low.as_slice()?;
3366    let close_slice = close.as_slice()?;
3367    let kern = validate_kernel(kernel, true)?;
3368
3369    let sweep = CkspBatchRange {
3370        p: p_range,
3371        x: x_range,
3372        q: q_range,
3373    };
3374
3375    let combos = expand_grid(&sweep).map_err(|e| PyValueError::new_err(e.to_string()))?;
3376    let rows = combos.len();
3377    let cols = close_slice.len();
3378    let total = rows
3379        .checked_mul(cols)
3380        .ok_or_else(|| PyValueError::new_err("rows*cols overflow"))?;
3381
3382    let long_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
3383    let short_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
3384    let long_slice = unsafe { long_arr.as_slice_mut()? };
3385    let short_slice = unsafe { short_arr.as_slice_mut()? };
3386
3387    let combos = py
3388        .allow_threads(|| {
3389            let kernel = match kern {
3390                Kernel::Auto => detect_best_batch_kernel(),
3391                k => k,
3392            };
3393
3394            let simd = match kernel {
3395                Kernel::Avx512Batch => Kernel::Avx512,
3396                Kernel::Avx2Batch => Kernel::Avx2,
3397                Kernel::ScalarBatch => Kernel::Scalar,
3398                _ => kernel,
3399            };
3400
3401            cksp_batch_inner_into(
3402                high_slice,
3403                low_slice,
3404                close_slice,
3405                &sweep,
3406                simd,
3407                true,
3408                long_slice,
3409                short_slice,
3410            )
3411        })
3412        .map_err(|e| PyValueError::new_err(e.to_string()))?;
3413
3414    let dict = PyDict::new(py);
3415    dict.set_item("long_values", long_arr.reshape((rows, cols))?)?;
3416    dict.set_item("short_values", short_arr.reshape((rows, cols))?)?;
3417
3418    dict.set_item(
3419        "p",
3420        combos
3421            .iter()
3422            .map(|p| p.p.unwrap() as u64)
3423            .collect::<Vec<_>>()
3424            .into_pyarray(py),
3425    )?;
3426    dict.set_item(
3427        "x",
3428        combos
3429            .iter()
3430            .map(|p| p.x.unwrap())
3431            .collect::<Vec<_>>()
3432            .into_pyarray(py),
3433    )?;
3434    dict.set_item(
3435        "q",
3436        combos
3437            .iter()
3438            .map(|p| p.q.unwrap() as u64)
3439            .collect::<Vec<_>>()
3440            .into_pyarray(py),
3441    )?;
3442
3443    Ok(dict)
3444}
3445
3446#[cfg(all(feature = "python", feature = "cuda"))]
3447#[pyfunction(name = "cksp_cuda_batch_dev")]
3448#[pyo3(signature = (high, low, close, p_range=(10,10,0), x_range=(1.0,1.0,0.0), q_range=(9,9,0), device_id=0))]
3449pub fn cksp_cuda_batch_dev_py<'py>(
3450    py: Python<'py>,
3451    high: PyReadonlyArray1<'py, f32>,
3452    low: PyReadonlyArray1<'py, f32>,
3453    close: PyReadonlyArray1<'py, f32>,
3454    p_range: (usize, usize, usize),
3455    x_range: (f32, f32, f32),
3456    q_range: (usize, usize, usize),
3457    device_id: usize,
3458) -> PyResult<Bound<'py, PyDict>> {
3459    if !cuda_available() {
3460        return Err(PyValueError::new_err("CUDA not available"));
3461    }
3462    let hs = high.as_slice()?;
3463    let ls = low.as_slice()?;
3464    let cs = close.as_slice()?;
3465    let sweep = CkspBatchRange {
3466        p: p_range,
3467        x: (x_range.0 as f64, x_range.1 as f64, x_range.2 as f64),
3468        q: q_range,
3469    };
3470    let (pair, combos) = py.allow_threads(|| {
3471        let cuda = CudaCksp::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
3472        cuda.cksp_batch_dev(hs, ls, cs, &sweep)
3473            .map_err(|e| PyValueError::new_err(e.to_string()))
3474    })?;
3475    let dict = PyDict::new(py);
3476    let long_dev = make_device_array_py(device_id, pair.long)?;
3477    let short_dev = make_device_array_py(device_id, pair.short)?;
3478    dict.set_item("long_values", Py::new(py, long_dev)?)?;
3479    dict.set_item("short_values", Py::new(py, short_dev)?)?;
3480    use numpy::IntoPyArray;
3481    dict.set_item(
3482        "p",
3483        combos
3484            .iter()
3485            .map(|c| c.p.unwrap() as u64)
3486            .collect::<Vec<_>>()
3487            .into_pyarray(py),
3488    )?;
3489    dict.set_item(
3490        "x",
3491        combos
3492            .iter()
3493            .map(|c| c.x.unwrap() as f64)
3494            .collect::<Vec<_>>()
3495            .into_pyarray(py),
3496    )?;
3497    dict.set_item(
3498        "q",
3499        combos
3500            .iter()
3501            .map(|c| c.q.unwrap() as u64)
3502            .collect::<Vec<_>>()
3503            .into_pyarray(py),
3504    )?;
3505    dict.set_item("rows", combos.len())?;
3506    dict.set_item("cols", cs.len())?;
3507    Ok(dict)
3508}
3509
3510#[cfg(all(feature = "python", feature = "cuda"))]
3511#[pyfunction(name = "cksp_cuda_many_series_one_param_dev")]
3512#[pyo3(signature = (high_tm, low_tm, close_tm, p=10, x=1.0, q=9, device_id=0))]
3513pub fn cksp_cuda_many_series_one_param_dev_py<'py>(
3514    py: Python<'py>,
3515    high_tm: numpy::PyReadonlyArray2<'py, f32>,
3516    low_tm: numpy::PyReadonlyArray2<'py, f32>,
3517    close_tm: numpy::PyReadonlyArray2<'py, f32>,
3518    p: usize,
3519    x: f64,
3520    q: usize,
3521    device_id: usize,
3522) -> PyResult<Bound<'py, PyDict>> {
3523    if !cuda_available() {
3524        return Err(PyValueError::new_err("CUDA not available"));
3525    }
3526    let sh = high_tm.shape();
3527    let sl = low_tm.shape();
3528    let sc = close_tm.shape();
3529    if sh.len() != 2 || sl.len() != 2 || sc.len() != 2 || sh != sl || sh != sc {
3530        return Err(PyValueError::new_err(
3531            "expected 2D arrays with identical shape",
3532        ));
3533    }
3534    let rows = sh[0];
3535    let cols = sh[1];
3536    let hflat = high_tm.as_slice()?;
3537    let lflat = low_tm.as_slice()?;
3538    let cflat = close_tm.as_slice()?;
3539    let params = CkspParams {
3540        p: Some(p),
3541        x: Some(x),
3542        q: Some(q),
3543    };
3544    let pair = py.allow_threads(|| {
3545        let cuda = CudaCksp::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
3546        cuda.cksp_many_series_one_param_time_major_dev(hflat, lflat, cflat, cols, rows, &params)
3547            .map_err(|e| PyValueError::new_err(e.to_string()))
3548    })?;
3549    let dict = PyDict::new(py);
3550    let long_dev = make_device_array_py(device_id, pair.long)?;
3551    let short_dev = make_device_array_py(device_id, pair.short)?;
3552    dict.set_item("long_values", Py::new(py, long_dev)?)?;
3553    dict.set_item("short_values", Py::new(py, short_dev)?)?;
3554    dict.set_item("rows", rows)?;
3555    dict.set_item("cols", cols)?;
3556    dict.set_item("p", p)?;
3557    dict.set_item("x", x)?;
3558    dict.set_item("q", q)?;
3559    Ok(dict)
3560}
3561
3562#[inline]
3563pub fn cksp_into_slice(
3564    long_dst: &mut [f64],
3565    short_dst: &mut [f64],
3566    high: &[f64],
3567    low: &[f64],
3568    close: &[f64],
3569    p: usize,
3570    x: f64,
3571    q: usize,
3572    kern: Kernel,
3573) -> Result<(), CkspError> {
3574    if high.len() != low.len() || low.len() != close.len() {
3575        return Err(CkspError::InconsistentLengths);
3576    }
3577    if long_dst.len() != close.len() {
3578        return Err(CkspError::OutputLengthMismatch {
3579            expected: close.len(),
3580            got: long_dst.len(),
3581        });
3582    }
3583    if short_dst.len() != close.len() {
3584        return Err(CkspError::OutputLengthMismatch {
3585            expected: close.len(),
3586            got: short_dst.len(),
3587        });
3588    }
3589    if close.is_empty() {
3590        return Err(CkspError::EmptyInputData);
3591    }
3592    if p == 0 || q == 0 {
3593        return Err(CkspError::InvalidParam { param: "p/q" });
3594    }
3595    if !x.is_finite() {
3596        return Err(CkspError::InvalidMultiplier { x });
3597    }
3598    let size = close.len();
3599    let first_valid_idx = match close.iter().position(|&v| !v.is_nan()) {
3600        Some(idx) => idx,
3601        None => return Err(CkspError::AllValuesNaN),
3602    };
3603    let valid = size - first_valid_idx;
3604    let warmup = p
3605        .checked_add(q)
3606        .and_then(|v| v.checked_sub(1))
3607        .ok_or_else(|| CkspError::InvalidInput("warmup overflow (p+q too large)".into()))?;
3608    if valid <= warmup {
3609        let needed = warmup
3610            .checked_add(1)
3611            .ok_or_else(|| CkspError::InvalidInput("warmup+1 overflow".into()))?;
3612        return Err(CkspError::NotEnoughValidData { needed, valid });
3613    }
3614
3615    let chosen = match kern {
3616        Kernel::Auto => Kernel::Scalar,
3617        other => other,
3618    };
3619
3620    unsafe {
3621        cksp_compute_into(
3622            high,
3623            low,
3624            close,
3625            p,
3626            x,
3627            q,
3628            first_valid_idx,
3629            long_dst,
3630            short_dst,
3631        );
3632    }
3633
3634    Ok(())
3635}
3636
3637#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3638#[wasm_bindgen]
3639pub fn cksp_js(
3640    high: &[f64],
3641    low: &[f64],
3642    close: &[f64],
3643    p: usize,
3644    x: f64,
3645    q: usize,
3646) -> Result<Vec<f64>, JsValue> {
3647    let input = CkspInput::from_slices(
3648        high,
3649        low,
3650        close,
3651        CkspParams {
3652            p: Some(p),
3653            x: Some(x),
3654            q: Some(q),
3655        },
3656    );
3657    let out =
3658        cksp_with_kernel(&input, Kernel::Auto).map_err(|e| JsValue::from_str(&e.to_string()))?;
3659    let cols = close.len();
3660    let mut values = Vec::with_capacity(2 * cols);
3661    values.extend_from_slice(&out.long_values);
3662    values.extend_from_slice(&out.short_values);
3663    Ok(values)
3664}
3665
3666#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3667#[wasm_bindgen]
3668pub fn cksp_into(
3669    high: &[f64],
3670    low: &[f64],
3671    close: &[f64],
3672    long_ptr: *mut f64,
3673    short_ptr: *mut f64,
3674    len: usize,
3675    p: usize,
3676    x: f64,
3677    q: usize,
3678) -> Result<(), JsValue> {
3679    if long_ptr.is_null() || short_ptr.is_null() {
3680        return Err(JsValue::from_str("Null pointer provided"));
3681    }
3682
3683    if high.len() != len || low.len() != len || close.len() != len {
3684        return Err(JsValue::from_str("Input length mismatch"));
3685    }
3686
3687    unsafe {
3688        let high_ptr = high.as_ptr();
3689        let low_ptr = low.as_ptr();
3690        let close_ptr = close.as_ptr();
3691
3692        let has_aliasing = (high_ptr as *const f64 == long_ptr as *const f64)
3693            || (high_ptr as *const f64 == short_ptr as *const f64)
3694            || (low_ptr as *const f64 == long_ptr as *const f64)
3695            || (low_ptr as *const f64 == short_ptr as *const f64)
3696            || (close_ptr as *const f64 == long_ptr as *const f64)
3697            || (close_ptr as *const f64 == short_ptr as *const f64)
3698            || (long_ptr == short_ptr);
3699
3700        if has_aliasing {
3701            let mut temp_long = vec![0.0; len];
3702            let mut temp_short = vec![0.0; len];
3703
3704            cksp_into_slice(
3705                &mut temp_long,
3706                &mut temp_short,
3707                high,
3708                low,
3709                close,
3710                p,
3711                x,
3712                q,
3713                Kernel::Auto,
3714            )
3715            .map_err(|e| JsValue::from_str(&e.to_string()))?;
3716
3717            let long_out = std::slice::from_raw_parts_mut(long_ptr, len);
3718            let short_out = std::slice::from_raw_parts_mut(short_ptr, len);
3719            long_out.copy_from_slice(&temp_long);
3720            short_out.copy_from_slice(&temp_short);
3721        } else {
3722            let long_out = std::slice::from_raw_parts_mut(long_ptr, len);
3723            let short_out = std::slice::from_raw_parts_mut(short_ptr, len);
3724
3725            cksp_into_slice(long_out, short_out, high, low, close, p, x, q, Kernel::Auto)
3726                .map_err(|e| JsValue::from_str(&e.to_string()))?;
3727        }
3728
3729        Ok(())
3730    }
3731}
3732
3733#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3734#[wasm_bindgen]
3735pub fn cksp_alloc(len: usize) -> *mut f64 {
3736    let mut vec = Vec::<f64>::with_capacity(len);
3737    let ptr = vec.as_mut_ptr();
3738    std::mem::forget(vec);
3739    ptr
3740}
3741
3742#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3743#[wasm_bindgen]
3744pub fn cksp_free(ptr: *mut f64, len: usize) {
3745    if !ptr.is_null() {
3746        unsafe {
3747            let _ = Vec::from_raw_parts(ptr, len, len);
3748        }
3749    }
3750}
3751
3752#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3753#[derive(Serialize, Deserialize)]
3754pub struct CkspJsResult {
3755    pub values: Vec<f64>,
3756    pub rows: usize,
3757    pub cols: usize,
3758}
3759
3760#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3761#[derive(Serialize, Deserialize)]
3762pub struct CkspBatchConfig {
3763    pub p_range: (usize, usize, usize),
3764    pub x_range: (f64, f64, f64),
3765    pub q_range: (usize, usize, usize),
3766}
3767
3768#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3769#[derive(Serialize, Deserialize)]
3770pub struct CkspBatchJsOutput {
3771    pub long_values: Vec<f64>,
3772    pub short_values: Vec<f64>,
3773    pub combos: Vec<CkspParams>,
3774    pub rows: usize,
3775    pub cols: usize,
3776}
3777
3778#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3779#[wasm_bindgen(js_name = cksp_batch)]
3780pub fn cksp_batch_js(
3781    high: &[f64],
3782    low: &[f64],
3783    close: &[f64],
3784    config: JsValue,
3785) -> Result<JsValue, JsValue> {
3786    let config: CkspBatchConfig = serde_wasm_bindgen::from_value(config)
3787        .map_err(|e| JsValue::from_str(&format!("Invalid config: {}", e)))?;
3788
3789    let sweep = CkspBatchRange {
3790        p: config.p_range,
3791        x: config.x_range,
3792        q: config.q_range,
3793    };
3794
3795    let combos = expand_grid(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
3796    let rows = combos.len();
3797    let cols = close.len();
3798    let total = rows
3799        .checked_mul(cols)
3800        .ok_or_else(|| JsValue::from_str("rows*cols overflow"))?;
3801
3802    let mut long_values = vec![0.0; total];
3803    let mut short_values = vec![0.0; total];
3804
3805    let kernel = detect_best_batch_kernel();
3806    let simd = match kernel {
3807        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
3808        Kernel::Avx512Batch => Kernel::Avx512,
3809        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
3810        Kernel::Avx2Batch => Kernel::Avx2,
3811        Kernel::ScalarBatch | _ => Kernel::Scalar,
3812    };
3813    cksp_batch_inner_into(
3814        high,
3815        low,
3816        close,
3817        &sweep,
3818        simd,
3819        false,
3820        &mut long_values,
3821        &mut short_values,
3822    )
3823    .map_err(|e| JsValue::from_str(&e.to_string()))?;
3824
3825    let js_output = CkspBatchJsOutput {
3826        long_values,
3827        short_values,
3828        combos,
3829        rows,
3830        cols,
3831    };
3832
3833    serde_wasm_bindgen::to_value(&js_output)
3834        .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
3835}
3836
3837#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3838#[wasm_bindgen]
3839pub fn cksp_batch_into(
3840    high_ptr: *const f64,
3841    low_ptr: *const f64,
3842    close_ptr: *const f64,
3843    long_ptr: *mut f64,
3844    short_ptr: *mut f64,
3845    len: usize,
3846    p_start: usize,
3847    p_end: usize,
3848    p_step: usize,
3849    x_start: f64,
3850    x_end: f64,
3851    x_step: f64,
3852    q_start: usize,
3853    q_end: usize,
3854    q_step: usize,
3855) -> Result<usize, JsValue> {
3856    if high_ptr.is_null()
3857        || low_ptr.is_null()
3858        || close_ptr.is_null()
3859        || long_ptr.is_null()
3860        || short_ptr.is_null()
3861    {
3862        return Err(JsValue::from_str("Null pointer provided"));
3863    }
3864
3865    unsafe {
3866        let high = std::slice::from_raw_parts(high_ptr, len);
3867        let low = std::slice::from_raw_parts(low_ptr, len);
3868        let close = std::slice::from_raw_parts(close_ptr, len);
3869
3870        let sweep = CkspBatchRange {
3871            p: (p_start, p_end, p_step),
3872            x: (x_start, x_end, x_step),
3873            q: (q_start, q_end, q_step),
3874        };
3875
3876        let combos = expand_grid(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
3877        let rows = combos.len();
3878        let cols = len;
3879        let total = rows
3880            .checked_mul(cols)
3881            .ok_or_else(|| JsValue::from_str("rows*cols overflow"))?;
3882
3883        let long_out = std::slice::from_raw_parts_mut(long_ptr, total);
3884        let short_out = std::slice::from_raw_parts_mut(short_ptr, total);
3885
3886        let kernel = detect_best_batch_kernel();
3887        let simd = match kernel {
3888            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
3889            Kernel::Avx512Batch => Kernel::Avx512,
3890            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
3891            Kernel::Avx2Batch => Kernel::Avx2,
3892            Kernel::ScalarBatch | _ => Kernel::Scalar,
3893        };
3894        cksp_batch_inner_into(high, low, close, &sweep, simd, false, long_out, short_out)
3895            .map_err(|e| JsValue::from_str(&e.to_string()))?;
3896
3897        Ok(rows)
3898    }
3899}