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, ¶ms)
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}