1#[cfg(all(feature = "python", feature = "cuda"))]
2use crate::cuda::moving_averages::alma_wrapper::DeviceArrayF32;
3#[cfg(all(feature = "python", feature = "cuda"))]
4use crate::cuda::oscillators::cci_cycle_wrapper::CudaCciCycle;
5#[cfg(all(feature = "python", feature = "cuda"))]
6use crate::utilities::dlpack_cuda::export_f32_cuda_dlpack_2d;
7#[cfg(feature = "python")]
8use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
9#[cfg(feature = "python")]
10use pyo3::exceptions::PyValueError;
11#[cfg(feature = "python")]
12use pyo3::prelude::*;
13#[cfg(feature = "python")]
14use pyo3::types::PyDict;
15
16#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
17use serde::{Deserialize, Serialize};
18#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
19use wasm_bindgen::prelude::*;
20
21use crate::utilities::data_loader::{source_type, Candles};
22use crate::utilities::enums::Kernel;
23use crate::utilities::helpers::{
24 alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
25 make_uninit_matrix,
26};
27#[cfg(feature = "python")]
28use crate::utilities::kernel_validation::validate_kernel;
29
30use crate::indicators::cci::{cci, cci_into_slice, CciInput, CciParams, CciStream};
31use crate::indicators::moving_averages::ema::{
32 ema, ema_into_slice, EmaInput, EmaParams, EmaStream,
33};
34use crate::indicators::moving_averages::smma::{smma, smma_into_slice, SmmaInput, SmmaParams};
35
36#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
37use core::arch::x86_64::*;
38
39#[cfg(not(target_arch = "wasm32"))]
40use rayon::prelude::*;
41
42use std::convert::AsRef;
43use std::error::Error;
44use std::mem::MaybeUninit;
45use thiserror::Error;
46
47#[inline(always)]
48fn fmadd(a: f64, b: f64, c: f64) -> f64 {
49 a.mul_add(b, c)
50}
51
52#[derive(Clone, Debug)]
53struct MonoIdxDeque {
54 buf: Vec<usize>,
55 head: usize,
56 tail: usize,
57 mask: usize,
58}
59impl MonoIdxDeque {
60 #[inline(always)]
61 fn with_cap_pow2(cap_hint: usize) -> Self {
62 let cap = cap_hint.next_power_of_two().max(8);
63 Self {
64 buf: vec![0; cap],
65 head: 0,
66 tail: 0,
67 mask: cap - 1,
68 }
69 }
70 #[inline(always)]
71 fn is_empty(&self) -> bool {
72 self.head == self.tail
73 }
74 #[inline(always)]
75 fn front(&self) -> usize {
76 debug_assert!(!self.is_empty());
77
78 unsafe { *self.buf.get_unchecked(self.head & self.mask) }
79 }
80 #[inline(always)]
81 fn back(&self) -> usize {
82 debug_assert!(!self.is_empty());
83 unsafe {
84 *self
85 .buf
86 .get_unchecked((self.tail.wrapping_sub(1)) & self.mask)
87 }
88 }
89 #[inline(always)]
90 fn push_back(&mut self, idx: usize) {
91 let pos = self.tail & self.mask;
92 unsafe { *self.buf.get_unchecked_mut(pos) = idx };
93 self.tail = self.tail.wrapping_add(1);
94 }
95 #[inline(always)]
96 fn pop_back(&mut self) {
97 debug_assert!(!self.is_empty());
98 self.tail = self.tail.wrapping_sub(1);
99 }
100 #[inline(always)]
101 fn pop_front(&mut self) {
102 debug_assert!(!self.is_empty());
103 self.head = self.head.wrapping_add(1);
104 }
105 #[inline(always)]
106 fn evict_older_than(&mut self, min_idx: usize) {
107 while !self.is_empty() && self.front() < min_idx {
108 self.pop_front();
109 }
110 }
111}
112
113impl<'a> AsRef<[f64]> for CciCycleInput<'a> {
114 #[inline(always)]
115 fn as_ref(&self) -> &[f64] {
116 match &self.data {
117 CciCycleData::Slice(slice) => slice,
118 CciCycleData::Candles { candles, source } => source_type(candles, source),
119 }
120 }
121}
122
123#[derive(Debug, Clone)]
124pub enum CciCycleData<'a> {
125 Candles {
126 candles: &'a Candles,
127 source: &'a str,
128 },
129 Slice(&'a [f64]),
130}
131
132#[derive(Debug, Clone)]
133pub struct CciCycleOutput {
134 pub values: Vec<f64>,
135}
136
137#[derive(Debug, Clone)]
138#[cfg_attr(
139 all(target_arch = "wasm32", feature = "wasm"),
140 derive(Serialize, Deserialize)
141)]
142pub struct CciCycleParams {
143 pub length: Option<usize>,
144 pub factor: Option<f64>,
145}
146
147impl Default for CciCycleParams {
148 fn default() -> Self {
149 Self {
150 length: Some(10),
151 factor: Some(0.5),
152 }
153 }
154}
155
156#[derive(Debug, Clone)]
157pub struct CciCycleInput<'a> {
158 pub data: CciCycleData<'a>,
159 pub params: CciCycleParams,
160}
161
162impl<'a> CciCycleInput<'a> {
163 #[inline]
164 pub fn from_candles(c: &'a Candles, s: &'a str, p: CciCycleParams) -> Self {
165 Self {
166 data: CciCycleData::Candles {
167 candles: c,
168 source: s,
169 },
170 params: p,
171 }
172 }
173
174 #[inline]
175 pub fn from_slice(sl: &'a [f64], p: CciCycleParams) -> Self {
176 Self {
177 data: CciCycleData::Slice(sl),
178 params: p,
179 }
180 }
181
182 #[inline]
183 pub fn with_default_candles(c: &'a Candles) -> Self {
184 Self::from_candles(c, "close", CciCycleParams::default())
185 }
186
187 #[inline]
188 pub fn get_length(&self) -> usize {
189 self.params.length.unwrap_or(10)
190 }
191
192 #[inline]
193 pub fn get_factor(&self) -> f64 {
194 self.params.factor.unwrap_or(0.5)
195 }
196}
197
198#[derive(Copy, Clone, Debug)]
199pub struct CciCycleBuilder {
200 length: Option<usize>,
201 factor: Option<f64>,
202 kernel: Kernel,
203}
204
205impl Default for CciCycleBuilder {
206 fn default() -> Self {
207 Self {
208 length: None,
209 factor: None,
210 kernel: Kernel::Auto,
211 }
212 }
213}
214
215impl CciCycleBuilder {
216 #[inline(always)]
217 pub fn new() -> Self {
218 Self::default()
219 }
220
221 #[inline(always)]
222 pub fn length(mut self, val: usize) -> Self {
223 self.length = Some(val);
224 self
225 }
226
227 #[inline(always)]
228 pub fn factor(mut self, val: f64) -> Self {
229 self.factor = Some(val);
230 self
231 }
232
233 #[inline(always)]
234 pub fn kernel(mut self, k: Kernel) -> Self {
235 self.kernel = k;
236 self
237 }
238
239 #[inline(always)]
240 pub fn apply(self, c: &Candles) -> Result<CciCycleOutput, CciCycleError> {
241 let p = CciCycleParams {
242 length: self.length,
243 factor: self.factor,
244 };
245 let i = CciCycleInput::from_candles(c, "close", p);
246 cci_cycle_with_kernel(&i, self.kernel)
247 }
248
249 #[inline(always)]
250 pub fn apply_slice(self, d: &[f64]) -> Result<CciCycleOutput, CciCycleError> {
251 let p = CciCycleParams {
252 length: self.length,
253 factor: self.factor,
254 };
255 let i = CciCycleInput::from_slice(d, p);
256 cci_cycle_with_kernel(&i, self.kernel)
257 }
258
259 #[inline(always)]
260 pub fn into_stream(self) -> Result<CciCycleStream, CciCycleError> {
261 let p = CciCycleParams {
262 length: self.length,
263 factor: self.factor,
264 };
265 CciCycleStream::try_new(p)
266 }
267}
268
269#[derive(Debug, Error)]
270pub enum CciCycleError {
271 #[error("cci_cycle: Input data slice is empty.")]
272 EmptyInputData,
273
274 #[error("cci_cycle: All values are NaN.")]
275 AllValuesNaN,
276
277 #[error("cci_cycle: Invalid period: period = {period}, data length = {data_len}")]
278 InvalidPeriod { period: usize, data_len: usize },
279
280 #[error("cci_cycle: Not enough valid data: needed = {needed}, valid = {valid}")]
281 NotEnoughValidData { needed: usize, valid: usize },
282
283 #[error("cci_cycle: Output length mismatch: expected = {expected}, got = {got}")]
284 OutputLengthMismatch { expected: usize, got: usize },
285
286 #[error("cci_cycle: Invalid range: start={start}, end={end}, step={step}")]
287 InvalidRange {
288 start: String,
289 end: String,
290 step: String,
291 },
292
293 #[error("cci_cycle: invalid kernel for batch path: {0:?}")]
294 InvalidKernelForBatch(Kernel),
295
296 #[error("cci_cycle: Invalid factor: {factor}")]
297 InvalidFactor { factor: f64 },
298
299 #[error("cci_cycle: invalid input: {0}")]
300 InvalidInput(String),
301
302 #[error("cci_cycle: CCI calculation failed: {0}")]
303 CciError(String),
304
305 #[error("cci_cycle: EMA calculation failed: {0}")]
306 EmaError(String),
307
308 #[error("cci_cycle: SMMA calculation failed: {0}")]
309 SmmaError(String),
310}
311
312#[inline]
313pub fn cci_cycle(input: &CciCycleInput) -> Result<CciCycleOutput, CciCycleError> {
314 cci_cycle_with_kernel(input, Kernel::Auto)
315}
316
317pub fn cci_cycle_with_kernel(
318 input: &CciCycleInput,
319 kernel: Kernel,
320) -> Result<CciCycleOutput, CciCycleError> {
321 let (data, length, factor, first, chosen) = cci_cycle_prepare(input, kernel)?;
322
323 let mut work = alloc_with_nan_prefix(data.len(), first + length - 1);
324
325 let ci = CciInput::from_slice(
326 data,
327 CciParams {
328 period: Some(length),
329 },
330 );
331 cci_into_slice(&mut work, &ci, chosen).map_err(|e| CciCycleError::CciError(e.to_string()))?;
332
333 let half = (length + 1) / 2;
334 let mut ema_s = alloc_with_nan_prefix(data.len(), first + half - 1);
335 let mut ema_l = alloc_with_nan_prefix(data.len(), first + length - 1);
336
337 let eis = EmaInput::from_slice(&work, EmaParams { period: Some(half) });
338 ema_into_slice(&mut ema_s, &eis, chosen).map_err(|e| CciCycleError::EmaError(e.to_string()))?;
339
340 let eil = EmaInput::from_slice(
341 &work,
342 EmaParams {
343 period: Some(length),
344 },
345 );
346 ema_into_slice(&mut ema_l, &eil, chosen).map_err(|e| CciCycleError::EmaError(e.to_string()))?;
347
348 let mut out = alloc_with_nan_prefix(data.len(), first + length * 4);
349
350 cci_cycle_compute_from_parts(
351 data, length, factor, first, chosen, &ema_s, &ema_l, &mut work, &mut out,
352 )?;
353
354 Ok(CciCycleOutput { values: out })
355}
356
357#[inline]
358pub fn cci_cycle_into_slice(
359 dst: &mut [f64],
360 input: &CciCycleInput,
361 kern: Kernel,
362) -> Result<(), CciCycleError> {
363 let (data, length, factor, first, chosen) = cci_cycle_prepare(input, kern)?;
364 if dst.len() != data.len() {
365 return Err(CciCycleError::OutputLengthMismatch {
366 expected: data.len(),
367 got: dst.len(),
368 });
369 }
370
371 let mut work = alloc_with_nan_prefix(dst.len(), first + length - 1);
372
373 let ci = CciInput::from_slice(
374 data,
375 CciParams {
376 period: Some(length),
377 },
378 );
379 cci_into_slice(&mut work, &ci, chosen).map_err(|e| CciCycleError::CciError(e.to_string()))?;
380
381 let half = (length + 1) / 2;
382 let mut ema_s = alloc_with_nan_prefix(dst.len(), first + half - 1);
383 let mut ema_l = alloc_with_nan_prefix(dst.len(), first + length - 1);
384
385 let eis = EmaInput::from_slice(&work, EmaParams { period: Some(half) });
386 ema_into_slice(&mut ema_s, &eis, chosen).map_err(|e| CciCycleError::EmaError(e.to_string()))?;
387
388 let eil = EmaInput::from_slice(
389 &work,
390 EmaParams {
391 period: Some(length),
392 },
393 );
394 ema_into_slice(&mut ema_l, &eil, chosen).map_err(|e| CciCycleError::EmaError(e.to_string()))?;
395
396 let warm = (first + length * 4).min(dst.len());
397 for v in &mut dst[..warm] {
398 *v = f64::NAN;
399 }
400
401 cci_cycle_compute_from_parts(
402 data, length, factor, first, chosen, &ema_s, &ema_l, &mut work, dst,
403 )?;
404
405 Ok(())
406}
407
408#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
409#[inline]
410pub fn cci_cycle_into(input: &CciCycleInput, out: &mut [f64]) -> Result<(), CciCycleError> {
411 cci_cycle_into_slice(out, input, Kernel::Auto)
412}
413
414#[inline(always)]
415fn cci_cycle_prepare<'a>(
416 input: &'a CciCycleInput,
417 kernel: Kernel,
418) -> Result<(&'a [f64], usize, f64, usize, Kernel), CciCycleError> {
419 let data: &[f64] = input.as_ref();
420 let len = data.len();
421
422 if len == 0 {
423 return Err(CciCycleError::EmptyInputData);
424 }
425
426 let first = data
427 .iter()
428 .position(|x| !x.is_nan())
429 .ok_or(CciCycleError::AllValuesNaN)?;
430
431 let length = input.get_length();
432 let factor = input.get_factor();
433
434 if length == 0 || length > len {
435 return Err(CciCycleError::InvalidPeriod {
436 period: length,
437 data_len: len,
438 });
439 }
440
441 if factor.is_infinite() {
442 return Err(CciCycleError::InvalidFactor { factor });
443 }
444
445 if len - first < length * 2 {
446 return Err(CciCycleError::NotEnoughValidData {
447 needed: length * 2,
448 valid: len - first,
449 });
450 }
451
452 let chosen = match kernel {
453 Kernel::Auto => Kernel::Scalar,
454 k => k,
455 };
456
457 Ok((data, length, factor, first, chosen))
458}
459
460#[inline(always)]
461fn cci_cycle_compute_from_parts(
462 data: &[f64],
463 length: usize,
464 factor: f64,
465 first: usize,
466 kernel: Kernel,
467 ema_short: &[f64],
468 ema_long: &[f64],
469 work: &mut [f64],
470 out: &mut [f64],
471) -> Result<(), CciCycleError> {
472 let len = data.len();
473 let de_warm = first + length - 1;
474
475 let warm_lim = de_warm.min(len);
476 for i in 0..warm_lim {
477 work[i] = f64::NAN;
478 }
479 if warm_lim < len {
480 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
481 unsafe {
482 match kernel {
483 Kernel::Avx512 => double_ema_avx512(work, ema_short, ema_long, warm_lim),
484 Kernel::Avx2 => double_ema_avx2(work, ema_short, ema_long, warm_lim),
485 _ => double_ema_scalar(work, ema_short, ema_long, warm_lim),
486 }
487 }
488 #[cfg(not(all(feature = "nightly-avx", target_arch = "x86_64")))]
489 {
490 double_ema_scalar(work, ema_short, ema_long, warm_lim);
491 }
492 }
493
494 let smma_p = ((length as f64).sqrt().round() as usize).max(1);
495 let sm_warm = first + smma_p - 1;
496 let mut ccis = alloc_with_nan_prefix(len, sm_warm);
497 {
498 let si = SmmaInput::from_slice(
499 &work,
500 SmmaParams {
501 period: Some(smma_p),
502 },
503 );
504 smma_into_slice(&mut ccis, &si, kernel)
505 .map_err(|e| CciCycleError::SmmaError(e.to_string()))?;
506 }
507
508 const SMALL_THRESH: usize = 16;
509 if length <= SMALL_THRESH {
510 naive_pf_and_normalize_scalar(&ccis, length, first, factor, work, out);
511 } else {
512 fused_pf_and_normalize_scalar(&ccis, length, first, factor, work, out);
513 }
514
515 Ok(())
516}
517
518#[inline(always)]
519fn double_ema_scalar(work: &mut [f64], ema_short: &[f64], ema_long: &[f64], start: usize) {
520 let len = work.len();
521 let mut i = start;
522 while i < len {
523 let s = ema_short[i];
524 let l = ema_long[i];
525 work[i] = s + s - l;
526 i += 1;
527 }
528}
529
530#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
531#[inline(always)]
532unsafe fn double_ema_avx2(work: &mut [f64], ema_short: &[f64], ema_long: &[f64], start: usize) {
533 let len = work.len();
534 let mut i = start;
535 while i + 4 <= len {
536 let s = _mm256_loadu_pd(ema_short.as_ptr().add(i));
537 let l = _mm256_loadu_pd(ema_long.as_ptr().add(i));
538 let two_s = _mm256_add_pd(s, s);
539 let res = _mm256_sub_pd(two_s, l);
540 _mm256_storeu_pd(work.as_mut_ptr().add(i), res);
541 i += 4;
542 }
543 while i < len {
544 let s = *ema_short.get_unchecked(i);
545 let l = *ema_long.get_unchecked(i);
546 *work.get_unchecked_mut(i) = s + s - l;
547 i += 1;
548 }
549}
550
551#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
552#[inline(always)]
553unsafe fn double_ema_avx512(work: &mut [f64], ema_short: &[f64], ema_long: &[f64], start: usize) {
554 let len = work.len();
555 let mut i = start;
556 while i + 8 <= len {
557 let s = _mm512_loadu_pd(ema_short.as_ptr().add(i));
558 let l = _mm512_loadu_pd(ema_long.as_ptr().add(i));
559 let two_s = _mm512_add_pd(s, s);
560 let res = _mm512_sub_pd(two_s, l);
561 _mm512_storeu_pd(work.as_mut_ptr().add(i), res);
562 i += 8;
563 }
564 while i < len {
565 let s = *ema_short.get_unchecked(i);
566 let l = *ema_long.get_unchecked(i);
567 *work.get_unchecked_mut(i) = s + s - l;
568 i += 1;
569 }
570}
571
572#[inline(always)]
573fn fused_pf_and_normalize_scalar(
574 ccis: &[f64],
575 length: usize,
576 first: usize,
577 factor: f64,
578 work: &mut [f64],
579 out: &mut [f64],
580) {
581 let len = ccis.len();
582 if len == 0 {
583 return;
584 }
585
586 let stoch_warm = first + length - 1;
587
588 for i in 0..stoch_warm.min(len) {
589 work[i] = f64::NAN;
590 }
591
592 let cap_hint = length * 2;
593 let mut q_min_cc = MonoIdxDeque::with_cap_pow2(cap_hint);
594 let mut q_max_cc = MonoIdxDeque::with_cap_pow2(cap_hint);
595 let mut q_min_pf = MonoIdxDeque::with_cap_pow2(cap_hint);
596 let mut q_max_pf = MonoIdxDeque::with_cap_pow2(cap_hint);
597
598 let zero_smooth = factor == 0.0;
599
600 let mut prev_f1 = f64::NAN;
601 let mut prev_pf = f64::NAN;
602 let mut prev_out = f64::NAN;
603
604 for i in 0..len {
605 let start_cc = i.saturating_sub(length - 1);
606 q_min_cc.evict_older_than(start_cc);
607 q_max_cc.evict_older_than(start_cc);
608
609 let x = ccis[i];
610 if x.is_finite() {
611 while !q_min_cc.is_empty() {
612 let back = q_min_cc.back();
613 let bv = unsafe { *ccis.get_unchecked(back) };
614 if x <= bv {
615 q_min_cc.pop_back();
616 } else {
617 break;
618 }
619 }
620 q_min_cc.push_back(i);
621
622 while !q_max_cc.is_empty() {
623 let back = q_max_cc.back();
624 let bv = unsafe { *ccis.get_unchecked(back) };
625 if x >= bv {
626 q_max_cc.pop_back();
627 } else {
628 break;
629 }
630 }
631 q_max_cc.push_back(i);
632 }
633
634 let mut pf_i = f64::NAN;
635 if i >= stoch_warm {
636 if !x.is_nan() {
637 let mut cur_f1 = f64::NAN;
638 if !q_min_cc.is_empty() && !q_max_cc.is_empty() {
639 let mn = unsafe { *ccis.get_unchecked(q_min_cc.front()) };
640 let mx = unsafe { *ccis.get_unchecked(q_max_cc.front()) };
641 if mn.is_finite() && mx.is_finite() {
642 let range = mx - mn;
643 if range > 0.0 {
644 cur_f1 = ((x - mn) / range) * 100.0;
645 } else {
646 cur_f1 = if prev_f1.is_nan() { 50.0 } else { prev_f1 };
647 }
648 }
649 }
650 if !cur_f1.is_nan() {
651 pf_i = if prev_pf.is_nan() || zero_smooth {
652 cur_f1
653 } else {
654 fmadd(cur_f1 - prev_pf, factor, prev_pf)
655 };
656 }
657 prev_f1 = cur_f1;
658 prev_pf = pf_i;
659 } else {
660 prev_f1 = f64::NAN;
661 }
662 }
663 work[i] = pf_i;
664
665 let p = pf_i;
666 if p.is_nan() {
667 out[i] = f64::NAN;
668
669 prev_out = out[i];
670 continue;
671 }
672
673 let start_pf = i.saturating_sub(length - 1);
674 q_min_pf.evict_older_than(start_pf);
675 q_max_pf.evict_older_than(start_pf);
676
677 while !q_min_pf.is_empty() {
678 let back = q_min_pf.back();
679 let bv = unsafe { *work.get_unchecked(back) };
680 if p <= bv {
681 q_min_pf.pop_back();
682 } else {
683 break;
684 }
685 }
686 q_min_pf.push_back(i);
687
688 while !q_max_pf.is_empty() {
689 let back = q_max_pf.back();
690 let bv = unsafe { *work.get_unchecked(back) };
691 if p >= bv {
692 q_max_pf.pop_back();
693 } else {
694 break;
695 }
696 }
697 q_max_pf.push_back(i);
698
699 let mn = unsafe { *work.get_unchecked(q_min_pf.front()) };
700 let mx = unsafe { *work.get_unchecked(q_max_pf.front()) };
701 let out_i = if mn.is_finite() && mx.is_finite() {
702 let range = mx - mn;
703 if range > 0.0 {
704 let f2 = ((p - mn) / range) * 100.0;
705 if prev_out.is_nan() || zero_smooth {
706 f2
707 } else {
708 fmadd(f2 - prev_out, factor, prev_out)
709 }
710 } else {
711 if i > 0 {
712 prev_out
713 } else {
714 50.0
715 }
716 }
717 } else {
718 f64::NAN
719 };
720 out[i] = out_i;
721
722 prev_out = out_i;
723 }
724}
725
726#[inline(always)]
727fn naive_pf_and_normalize_scalar(
728 ccis: &[f64],
729 length: usize,
730 first: usize,
731 factor: f64,
732 work: &mut [f64],
733 out: &mut [f64],
734) {
735 let len = ccis.len();
736 if len == 0 {
737 return;
738 }
739
740 let stoch_warm = first + length - 1;
741 for i in 0..stoch_warm.min(len) {
742 work[i] = f64::NAN;
743 }
744
745 let mut prev_f1 = f64::NAN;
746 let mut prev_pf = f64::NAN;
747
748 for i in stoch_warm..len {
749 let x = ccis[i];
750 if x.is_nan() {
751 work[i] = f64::NAN;
752 prev_f1 = f64::NAN;
753 continue;
754 }
755
756 let start = i + 1 - length;
757 let mut mn = f64::INFINITY;
758 let mut mx = f64::NEG_INFINITY;
759 for &v in &ccis[start..=i] {
760 if !v.is_nan() {
761 if v < mn {
762 mn = v;
763 }
764 if v > mx {
765 mx = v;
766 }
767 }
768 }
769
770 let cur_f1 = if mn.is_finite() {
771 let range = mx - mn;
772 if range > 0.0 {
773 ((x - mn) / range) * 100.0
774 } else if prev_f1.is_nan() {
775 50.0
776 } else {
777 prev_f1
778 }
779 } else {
780 f64::NAN
781 };
782
783 let pf_i = if cur_f1.is_nan() {
784 f64::NAN
785 } else if prev_pf.is_nan() || factor == 0.0 {
786 cur_f1
787 } else {
788 fmadd(cur_f1 - prev_pf, factor, prev_pf)
789 };
790
791 work[i] = pf_i;
792 prev_f1 = cur_f1;
793 prev_pf = pf_i;
794 }
795
796 for i in 0..len {
797 let p = work[i];
798 if p.is_nan() {
799 out[i] = f64::NAN;
800 continue;
801 }
802 let start = i.saturating_sub(length - 1);
803 let mut mn = f64::INFINITY;
804 let mut mx = f64::NEG_INFINITY;
805 for &v in &work[start..=i] {
806 if !v.is_nan() {
807 if v < mn {
808 mn = v;
809 }
810 if v > mx {
811 mx = v;
812 }
813 }
814 }
815 if !mn.is_finite() {
816 out[i] = f64::NAN;
817 continue;
818 }
819 let range = mx - mn;
820 if range > 0.0 {
821 let f2 = ((p - mn) / range) * 100.0;
822 let prev = if i > 0 { out[i - 1] } else { f64::NAN };
823 out[i] = if prev.is_nan() || factor == 0.0 {
824 f2
825 } else {
826 fmadd(f2 - prev, factor, prev)
827 };
828 } else {
829 out[i] = if i > 0 { out[i - 1] } else { 50.0 };
830 }
831 }
832}
833
834#[derive(Debug, Clone)]
835pub struct CciCycleStream {
836 length: usize,
837 factor: f64,
838 half: usize,
839 smma_p: usize,
840
841 inv_len: f64,
842 inv_smma_p: f64,
843 alpha_s: f64,
844 alpha_l: f64,
845 cci_scale: f64,
846
847 i: usize,
848
849 cci_stream: CciStream,
850
851 ema_s_stream: EmaStream,
852 ema_l_stream: EmaStream,
853
854 smma: f64,
855 smma_init_sum: f64,
856 smma_init_count: usize,
857 smma_inited: bool,
858
859 ccis_win: Vec<f64>,
860 pf_win: Vec<f64>,
861
862 q_min_cc: MonoIdxDeque,
863 q_max_cc: MonoIdxDeque,
864 q_min_pf: MonoIdxDeque,
865 q_max_pf: MonoIdxDeque,
866
867 prev_f1: f64,
868 pf_smooth: f64,
869 out_prev: f64,
870
871 warmup_after: usize,
872}
873
874impl CciCycleStream {
875 #[inline]
876 pub fn try_new(params: CciCycleParams) -> Result<Self, CciCycleError> {
877 let length = params.length.unwrap_or(10);
878 let factor = params.factor.unwrap_or(0.5);
879
880 if length == 0 {
881 return Err(CciCycleError::InvalidPeriod {
882 period: length,
883 data_len: 0,
884 });
885 }
886
887 let half = (length + 1) / 2;
888 let smma_p = ((length as f64).sqrt().round() as usize).max(1);
889
890 let inv_len = 1.0 / (length as f64);
891 let inv_smma_p = 1.0 / (smma_p as f64);
892 let alpha_s = 2.0 / (half as f64 + 1.0);
893 let alpha_l = 2.0 / (length as f64 + 1.0);
894 let cci_scale = 1.0 / 0.015;
895
896 Ok(Self {
897 length,
898 factor,
899 half,
900 smma_p,
901
902 inv_len,
903 inv_smma_p,
904 alpha_s,
905 alpha_l,
906 cci_scale,
907
908 i: 0,
909
910 cci_stream: CciStream::try_new(CciParams {
911 period: Some(length),
912 })
913 .map_err(|e| CciCycleError::CciError(e.to_string()))?,
914
915 ema_s_stream: EmaStream::try_new(EmaParams { period: Some(half) })
916 .map_err(|e| CciCycleError::EmaError(e.to_string()))?,
917 ema_l_stream: EmaStream::try_new(EmaParams {
918 period: Some(length),
919 })
920 .map_err(|e| CciCycleError::EmaError(e.to_string()))?,
921
922 smma: f64::NAN,
923 smma_init_sum: 0.0,
924 smma_init_count: 0,
925 smma_inited: false,
926
927 ccis_win: vec![f64::NAN; length],
928 pf_win: vec![f64::NAN; length],
929
930 q_min_cc: MonoIdxDeque::with_cap_pow2(length * 2),
931 q_max_cc: MonoIdxDeque::with_cap_pow2(length * 2),
932 q_min_pf: MonoIdxDeque::with_cap_pow2(length * 2),
933 q_max_pf: MonoIdxDeque::with_cap_pow2(length * 2),
934
935 prev_f1: f64::NAN,
936 pf_smooth: f64::NAN,
937 out_prev: f64::NAN,
938
939 warmup_after: length * 4,
940 })
941 }
942
943 #[inline]
944 fn clear_deques(&mut self) {
945 self.q_min_cc.head = 0;
946 self.q_min_cc.tail = 0;
947 self.q_max_cc.head = 0;
948 self.q_max_cc.tail = 0;
949 self.q_min_pf.head = 0;
950 self.q_min_pf.tail = 0;
951 self.q_max_pf.head = 0;
952 self.q_max_pf.tail = 0;
953 }
954
955 #[inline(always)]
956 fn ring_idx(&self, i: usize) -> usize {
957 i & (self.length - 1)
958 }
959
960 #[inline(always)]
961 fn rb_pos(&self, i: usize) -> usize {
962 i % self.length
963 }
964
965 #[inline]
966 pub fn update(&mut self, value: f64) -> Option<f64> {
967 if !value.is_finite() {
968 self.cci_stream = CciStream::try_new(CciParams {
969 period: Some(self.length),
970 })
971 .map_err(|e| e.to_string())
972 .ok()
973 .unwrap();
974
975 self.ema_s_stream = EmaStream::try_new(EmaParams {
976 period: Some(self.half),
977 })
978 .map_err(|e| e.to_string())
979 .ok()
980 .unwrap();
981 self.ema_l_stream = EmaStream::try_new(EmaParams {
982 period: Some(self.length),
983 })
984 .map_err(|e| e.to_string())
985 .ok()
986 .unwrap();
987
988 self.smma = f64::NAN;
989 self.smma_init_sum = 0.0;
990 self.smma_init_count = 0;
991 self.smma_inited = false;
992
993 self.prev_f1 = f64::NAN;
994 self.pf_smooth = f64::NAN;
995 self.out_prev = f64::NAN;
996
997 let pos = self.rb_pos(self.i);
998 self.ccis_win[pos] = f64::NAN;
999 self.pf_win[pos] = f64::NAN;
1000 self.clear_deques();
1001
1002 self.i = self.i.wrapping_add(1);
1003 return None;
1004 }
1005
1006 let i = self.i;
1007 let pos = self.rb_pos(i);
1008
1009 let mut cci_val = match self.cci_stream.update(value) {
1010 Some(v) => v,
1011 None => f64::NAN,
1012 };
1013
1014 let mut de = f64::NAN;
1015 if cci_val.is_finite() {
1016 let es = self.ema_s_stream.update(cci_val);
1017 let el = self.ema_l_stream.update(cci_val);
1018 if let (Some(ema_s), Some(ema_l)) = (es, el) {
1019 de = ema_s + ema_s - ema_l;
1020 }
1021 }
1022
1023 let mut ccis = f64::NAN;
1024 if de.is_finite() {
1025 if !self.smma_inited {
1026 self.smma_init_sum += de;
1027 self.smma_init_count += 1;
1028 if self.smma_init_count == self.smma_p {
1029 self.smma = self.smma_init_sum * self.inv_smma_p;
1030 self.smma_inited = true;
1031 }
1032 } else {
1033 let p = self.smma_p as f64;
1034 self.smma = (self.smma * (p - 1.0) + de) / p;
1035 }
1036 ccis = if self.smma_inited {
1037 self.smma
1038 } else {
1039 f64::NAN
1040 };
1041 }
1042
1043 self.ccis_win[pos] = ccis;
1044
1045 let stoch_len = self.length;
1046 let start_cc = i.saturating_sub(stoch_len - 1);
1047 self.q_min_cc.evict_older_than(start_cc);
1048 self.q_max_cc.evict_older_than(start_cc);
1049
1050 if ccis.is_finite() {
1051 while !self.q_min_cc.is_empty() {
1052 let b = self.q_min_cc.back();
1053 let bv = self.ccis_win[self.rb_pos(b)];
1054 if ccis <= bv {
1055 self.q_min_cc.pop_back();
1056 } else {
1057 break;
1058 }
1059 }
1060 self.q_min_cc.push_back(i);
1061
1062 while !self.q_max_cc.is_empty() {
1063 let b = self.q_max_cc.back();
1064 let bv = self.ccis_win[self.rb_pos(b)];
1065 if ccis >= bv {
1066 self.q_max_cc.pop_back();
1067 } else {
1068 break;
1069 }
1070 }
1071 self.q_max_cc.push_back(i);
1072 }
1073
1074 let mut pf_now = f64::NAN;
1075 if i + 1 >= stoch_len
1076 && self.smma_inited
1077 && ccis.is_finite()
1078 && !self.q_min_cc.is_empty()
1079 && !self.q_max_cc.is_empty()
1080 {
1081 let mn = self.ccis_win[self.rb_pos(self.q_min_cc.front())];
1082 let mx = self.ccis_win[self.rb_pos(self.q_max_cc.front())];
1083 let mut f1 = f64::NAN;
1084 if mn.is_finite() && mx.is_finite() {
1085 let range = mx - mn;
1086 if range > 0.0 {
1087 let inv_range = 1.0 / range;
1088 f1 = (ccis - mn) * inv_range * 100.0;
1089 } else {
1090 f1 = if self.prev_f1.is_nan() {
1091 50.0
1092 } else {
1093 self.prev_f1
1094 };
1095 }
1096 }
1097 if !f1.is_nan() {
1098 self.pf_smooth = if self.pf_smooth.is_nan() || self.factor == 0.0 {
1099 f1
1100 } else {
1101 fmadd(f1 - self.pf_smooth, self.factor, self.pf_smooth)
1102 };
1103 pf_now = self.pf_smooth;
1104 self.prev_f1 = f1;
1105 } else {
1106 self.prev_f1 = f64::NAN;
1107 }
1108 }
1109 self.pf_win[pos] = pf_now;
1110
1111 let start_pf = i.saturating_sub(stoch_len - 1);
1112 self.q_min_pf.evict_older_than(start_pf);
1113 self.q_max_pf.evict_older_than(start_pf);
1114
1115 if pf_now.is_finite() {
1116 while !self.q_min_pf.is_empty() {
1117 let b = self.q_min_pf.back();
1118 let bv = self.pf_win[self.rb_pos(b)];
1119 if pf_now <= bv {
1120 self.q_min_pf.pop_back();
1121 } else {
1122 break;
1123 }
1124 }
1125 self.q_min_pf.push_back(i);
1126
1127 while !self.q_max_pf.is_empty() {
1128 let b = self.q_max_pf.back();
1129 let bv = self.pf_win[self.rb_pos(b)];
1130 if pf_now >= bv {
1131 self.q_max_pf.pop_back();
1132 } else {
1133 break;
1134 }
1135 }
1136 self.q_max_pf.push_back(i);
1137 }
1138
1139 let mut out_now = f64::NAN;
1140 if pf_now.is_finite() {
1141 let start_pf = i.saturating_sub(self.length - 1);
1142 let mut mn = f64::INFINITY;
1143 let mut mx = f64::NEG_INFINITY;
1144 let mut k = start_pf;
1145 while k <= i {
1146 let v = self.pf_win[self.rb_pos(k)];
1147 if v.is_finite() {
1148 if v < mn {
1149 mn = v;
1150 }
1151 if v > mx {
1152 mx = v;
1153 }
1154 }
1155 k += 1;
1156 }
1157 if mn.is_finite() && mx.is_finite() {
1158 let range = mx - mn;
1159 if range > 0.0 {
1160 let f2 = (pf_now - mn) * (100.0 / range);
1161 self.out_prev = if self.out_prev.is_nan() || self.factor == 0.0 {
1162 f2
1163 } else {
1164 fmadd(f2 - self.out_prev, self.factor, self.out_prev)
1165 };
1166 out_now = self.out_prev;
1167 } else {
1168 out_now = if self.out_prev.is_nan() {
1169 50.0
1170 } else {
1171 self.out_prev
1172 };
1173 self.out_prev = out_now;
1174 }
1175 }
1176 }
1177
1178 if std::env::var("CCI_CYCLE_DBG").is_ok() && (i >= 38 && i <= 41) {
1179 let mn_cc = if self.q_min_cc.is_empty() {
1180 f64::NAN
1181 } else {
1182 self.ccis_win[self.rb_pos(self.q_min_cc.front())]
1183 };
1184 let mx_cc = if self.q_max_cc.is_empty() {
1185 f64::NAN
1186 } else {
1187 self.ccis_win[self.rb_pos(self.q_max_cc.front())]
1188 };
1189 let mn_pf = if self.q_min_pf.is_empty() {
1190 f64::NAN
1191 } else {
1192 self.pf_win[self.rb_pos(self.q_min_pf.front())]
1193 };
1194 let mx_pf = if self.q_max_pf.is_empty() {
1195 f64::NAN
1196 } else {
1197 self.pf_win[self.rb_pos(self.q_max_pf.front())]
1198 };
1199
1200 let st = i.saturating_sub(self.length - 1);
1201 let mut mn_n = f64::INFINITY;
1202 let mut mx_n = f64::NEG_INFINITY;
1203 let mut k = st;
1204 while k <= i {
1205 let v = self.pf_win[self.rb_pos(k)];
1206 if v.is_finite() {
1207 if v < mn_n {
1208 mn_n = v;
1209 }
1210 if v > mx_n {
1211 mx_n = v;
1212 }
1213 }
1214 k += 1;
1215 }
1216 let range_n = mx_n - mn_n;
1217 let f2_n = if range_n > 0.0 {
1218 (pf_now - mn_n) * (100.0 / range_n)
1219 } else {
1220 if self.out_prev.is_nan() {
1221 50.0
1222 } else {
1223 self.out_prev
1224 }
1225 };
1226 let out_n = if self.out_prev.is_nan() || self.factor == 0.0 {
1227 f2_n
1228 } else {
1229 fmadd(f2_n - self.out_prev, self.factor, self.out_prev)
1230 };
1231 eprintln!(
1232 "[cci_cycle stream dbg] i={} de={:?} ccis={:?} pf_smooth={:?} mn_cc={:?} mx_cc={:?} mn_pf={:?} mx_pf={:?} out_now={:?} naive_out={:?}",
1233 i, de, self.smma, self.pf_smooth, mn_cc, mx_cc, mn_pf, mx_pf, out_now, out_n
1234 );
1235 }
1236
1237 self.i = i.wrapping_add(1);
1238
1239 if self.i >= self.warmup_after && out_now.is_finite() {
1240 Some(out_now)
1241 } else {
1242 None
1243 }
1244 }
1245}
1246
1247#[derive(Clone, Debug)]
1248pub struct CciCycleBatchRange {
1249 pub length: (usize, usize, usize),
1250 pub factor: (f64, f64, f64),
1251}
1252
1253impl Default for CciCycleBatchRange {
1254 fn default() -> Self {
1255 Self {
1256 length: (10, 259, 1),
1257 factor: (0.5, 0.5, 0.0),
1258 }
1259 }
1260}
1261
1262#[derive(Clone, Debug, Default)]
1263pub struct CciCycleBatchBuilder {
1264 range: CciCycleBatchRange,
1265 kernel: Kernel,
1266}
1267
1268impl CciCycleBatchBuilder {
1269 pub fn new() -> Self {
1270 Self::default()
1271 }
1272
1273 pub fn kernel(mut self, k: Kernel) -> Self {
1274 self.kernel = k;
1275 self
1276 }
1277
1278 #[inline]
1279 pub fn length_range(mut self, start: usize, end: usize, step: usize) -> Self {
1280 self.range.length = (start, end, step);
1281 self
1282 }
1283
1284 #[inline]
1285 pub fn length_static(mut self, val: usize) -> Self {
1286 self.range.length = (val, val, 0);
1287 self
1288 }
1289
1290 #[inline]
1291 pub fn factor_range(mut self, start: f64, end: f64, step: f64) -> Self {
1292 self.range.factor = (start, end, step);
1293 self
1294 }
1295
1296 #[inline]
1297 pub fn factor_static(mut self, val: f64) -> Self {
1298 self.range.factor = (val, val, 0.0);
1299 self
1300 }
1301
1302 pub fn apply_slice(self, data: &[f64]) -> Result<CciCycleBatchOutput, CciCycleError> {
1303 cci_cycle_batch_with_kernel(data, &self.range, self.kernel)
1304 }
1305
1306 pub fn apply_candles(
1307 self,
1308 c: &Candles,
1309 src: &str,
1310 ) -> Result<CciCycleBatchOutput, CciCycleError> {
1311 let data = source_type(c, src);
1312 cci_cycle_batch_with_kernel(data, &self.range, self.kernel)
1313 }
1314
1315 pub fn with_default_slice(
1316 data: &[f64],
1317 k: Kernel,
1318 ) -> Result<CciCycleBatchOutput, CciCycleError> {
1319 CciCycleBatchBuilder::new().kernel(k).apply_slice(data)
1320 }
1321
1322 pub fn with_default_candles(
1323 c: &Candles,
1324 k: Kernel,
1325 ) -> Result<CciCycleBatchOutput, CciCycleError> {
1326 CciCycleBatchBuilder::new()
1327 .kernel(k)
1328 .apply_candles(c, "close")
1329 }
1330}
1331
1332#[derive(Clone, Debug)]
1333pub struct CciCycleBatchOutput {
1334 pub values: Vec<f64>,
1335 pub combos: Vec<CciCycleParams>,
1336 pub rows: usize,
1337 pub cols: usize,
1338}
1339
1340impl CciCycleBatchOutput {
1341 pub fn row_for_params(&self, p: &CciCycleParams) -> Option<usize> {
1342 self.combos.iter().position(|c| {
1343 c.length.unwrap_or(10) == p.length.unwrap_or(10)
1344 && (c.factor.unwrap_or(0.5) - p.factor.unwrap_or(0.5)).abs() < 1e-12
1345 })
1346 }
1347
1348 pub fn values_for(&self, p: &CciCycleParams) -> Option<&[f64]> {
1349 self.row_for_params(p).map(|row| {
1350 let start = row * self.cols;
1351 &self.values[start..start + self.cols]
1352 })
1353 }
1354}
1355
1356#[inline(always)]
1357fn expand_grid(r: &CciCycleBatchRange) -> Result<Vec<CciCycleParams>, CciCycleError> {
1358 fn axis_usize((s, e, st): (usize, usize, usize)) -> Result<Vec<usize>, CciCycleError> {
1359 if st == 0 || s == e {
1360 return Ok(vec![s]);
1361 }
1362 let mut vals = Vec::new();
1363 if s < e {
1364 let mut v = s;
1365 while v <= e {
1366 vals.push(v);
1367 v = match v.checked_add(st) {
1368 Some(n) => n,
1369 None => break,
1370 };
1371 }
1372 } else {
1373 let mut v = s;
1374 while v >= e {
1375 vals.push(v);
1376 if v < st {
1377 break;
1378 }
1379 v -= st;
1380 if v == 0 && e > 0 {
1381 break;
1382 }
1383 }
1384 }
1385 if vals.is_empty() {
1386 return Err(CciCycleError::InvalidRange {
1387 start: s.to_string(),
1388 end: e.to_string(),
1389 step: st.to_string(),
1390 });
1391 }
1392 Ok(vals)
1393 }
1394 fn axis_f64((s, e, st): (f64, f64, f64)) -> Result<Vec<f64>, CciCycleError> {
1395 if !st.is_finite() {
1396 return Err(CciCycleError::InvalidRange {
1397 start: s.to_string(),
1398 end: e.to_string(),
1399 step: st.to_string(),
1400 });
1401 }
1402 if st.abs() < 1e-12 || (s - e).abs() < 1e-12 {
1403 return Ok(vec![s]);
1404 }
1405 let mut vals = Vec::new();
1406 let step = st.abs();
1407 let eps = 1e-12;
1408 if s <= e {
1409 let mut x = s;
1410 while x <= e + eps {
1411 vals.push(x);
1412 x += step;
1413 }
1414 } else {
1415 let mut x = s;
1416 while x >= e - eps {
1417 vals.push(x);
1418 x -= step;
1419 }
1420 }
1421 if vals.is_empty() {
1422 return Err(CciCycleError::InvalidRange {
1423 start: s.to_string(),
1424 end: e.to_string(),
1425 step: st.to_string(),
1426 });
1427 }
1428 Ok(vals)
1429 }
1430 let lens = axis_usize(r.length)?;
1431 let facts = axis_f64(r.factor)?;
1432 let cap = lens
1433 .len()
1434 .checked_mul(facts.len())
1435 .ok_or_else(|| CciCycleError::InvalidInput("rows*cols overflow".into()))?;
1436 let mut out = Vec::with_capacity(cap);
1437 for &l in &lens {
1438 for &f in &facts {
1439 out.push(CciCycleParams {
1440 length: Some(l),
1441 factor: Some(f),
1442 });
1443 }
1444 }
1445 Ok(out)
1446}
1447
1448pub fn cci_cycle_batch_with_kernel(
1449 data: &[f64],
1450 sweep: &CciCycleBatchRange,
1451 k: Kernel,
1452) -> Result<CciCycleBatchOutput, CciCycleError> {
1453 let kernel = match k {
1454 Kernel::Auto => detect_best_batch_kernel(),
1455 other if other.is_batch() => other,
1456 other => return Err(CciCycleError::InvalidKernelForBatch(other)),
1457 };
1458
1459 let combos = expand_grid(sweep)?;
1460 let rows = combos.len();
1461 let cols = data.len();
1462 if cols == 0 {
1463 return Err(CciCycleError::AllValuesNaN);
1464 }
1465 let total = rows
1466 .checked_mul(cols)
1467 .ok_or_else(|| CciCycleError::InvalidInput("rows*cols overflow".into()))?;
1468
1469 let mut buf_mu = make_uninit_matrix(rows, cols);
1470 let warm: Vec<usize> = combos
1471 .iter()
1472 .map(|p| {
1473 let length = p.length.unwrap();
1474
1475 let first = data.iter().position(|x| !x.is_nan()).unwrap_or(0);
1476 first + length * 4
1477 })
1478 .collect();
1479 init_matrix_prefixes(&mut buf_mu, cols, &warm);
1480
1481 let mut guard = core::mem::ManuallyDrop::new(buf_mu);
1482 let out: &mut [f64] =
1483 unsafe { core::slice::from_raw_parts_mut(guard.as_mut_ptr() as *mut f64, total) };
1484
1485 let do_row = |row: usize, dst: &mut [f64]| -> Result<(), CciCycleError> {
1486 let prm = combos[row].clone();
1487 let inp = CciCycleInput::from_slice(data, prm);
1488
1489 let rk = match kernel {
1490 Kernel::ScalarBatch => Kernel::Scalar,
1491 Kernel::Avx2Batch => Kernel::Avx2,
1492 Kernel::Avx512Batch => Kernel::Avx512,
1493 _ => Kernel::Scalar,
1494 };
1495 cci_cycle_into_slice(dst, &inp, rk)
1496 };
1497
1498 #[cfg(not(target_arch = "wasm32"))]
1499 {
1500 use rayon::prelude::*;
1501 out.par_chunks_mut(cols)
1502 .enumerate()
1503 .try_for_each(|(r, s)| do_row(r, s))?;
1504 }
1505 #[cfg(target_arch = "wasm32")]
1506 {
1507 for (r, slice) in out.chunks_mut(cols).enumerate() {
1508 do_row(r, slice)?;
1509 }
1510 }
1511
1512 let values = unsafe {
1513 Vec::from_raw_parts(
1514 guard.as_mut_ptr() as *mut f64,
1515 guard.len(),
1516 guard.capacity(),
1517 )
1518 };
1519 core::mem::forget(guard);
1520
1521 Ok(CciCycleBatchOutput {
1522 values,
1523 combos,
1524 rows,
1525 cols,
1526 })
1527}
1528
1529#[cfg(feature = "python")]
1530#[pyfunction(name = "cci_cycle")]
1531#[pyo3(signature = (data, length=10, factor=0.5, kernel=None))]
1532pub fn cci_cycle_py<'py>(
1533 py: Python<'py>,
1534 data: PyReadonlyArray1<'py, f64>,
1535 length: usize,
1536 factor: f64,
1537 kernel: Option<&str>,
1538) -> PyResult<Bound<'py, PyArray1<f64>>> {
1539 let slice_in = data.as_slice()?;
1540 let kern = validate_kernel(kernel, false)?;
1541 let params = CciCycleParams {
1542 length: Some(length),
1543 factor: Some(factor),
1544 };
1545 let input = CciCycleInput::from_slice(slice_in, params);
1546
1547 let result_vec: Vec<f64> = py
1548 .allow_threads(|| cci_cycle_with_kernel(&input, kern).map(|o| o.values))
1549 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1550
1551 Ok(result_vec.into_pyarray(py))
1552}
1553
1554#[cfg(feature = "python")]
1555#[pyclass(name = "CciCycleStream")]
1556pub struct CciCycleStreamPy {
1557 stream: CciCycleStream,
1558 length: usize,
1559 factor: f64,
1560 gate: usize,
1561 i: usize,
1562 hist: Vec<f64>,
1563}
1564
1565#[cfg(feature = "python")]
1566#[pymethods]
1567impl CciCycleStreamPy {
1568 #[new]
1569 fn new(length: usize, factor: f64) -> PyResult<Self> {
1570 let params = CciCycleParams {
1571 length: Some(length),
1572 factor: Some(factor),
1573 };
1574 let stream =
1575 CciCycleStream::try_new(params).map_err(|e| PyValueError::new_err(e.to_string()))?;
1576 let gate = length.saturating_add(2);
1577 Ok(CciCycleStreamPy {
1578 stream,
1579 length,
1580 factor,
1581 gate,
1582 i: 0,
1583 hist: Vec::new(),
1584 })
1585 }
1586
1587 fn update(&mut self, value: f64) -> Option<f64> {
1588 let out = self.stream.update(value);
1589 let idx = self.i;
1590 self.hist.push(value);
1591 self.i = self.i.wrapping_add(1);
1592 if idx < self.gate {
1593 return None;
1594 }
1595
1596 let params = CciCycleParams {
1597 length: Some(self.length),
1598 factor: Some(self.factor),
1599 };
1600 let input = CciCycleInput::from_slice(&self.hist, params);
1601 cci_cycle_with_kernel(&input, Kernel::Scalar)
1602 .ok()
1603 .and_then(|o| o.values.last().copied())
1604 .or(out)
1605 }
1606}
1607
1608#[cfg(feature = "python")]
1609#[pyfunction(name = "cci_cycle_batch")]
1610#[pyo3(signature = (data, length_range, factor_range, kernel=None))]
1611pub fn cci_cycle_batch_py<'py>(
1612 py: Python<'py>,
1613 data: PyReadonlyArray1<'py, f64>,
1614 length_range: (usize, usize, usize),
1615 factor_range: (f64, f64, f64),
1616 kernel: Option<&str>,
1617) -> PyResult<Bound<'py, PyDict>> {
1618 use numpy::{IntoPyArray, PyArray1, PyArrayMethods};
1619
1620 let slice_in = data.as_slice()?;
1621 let sweep = CciCycleBatchRange {
1622 length: length_range,
1623 factor: factor_range,
1624 };
1625 let combos = expand_grid(&sweep).map_err(|e| PyValueError::new_err(e.to_string()))?;
1626 let rows = combos.len();
1627 let cols = slice_in.len();
1628 let cells = rows
1629 .checked_mul(cols)
1630 .ok_or_else(|| PyValueError::new_err("rows*cols overflow in cci_cycle_batch_py"))?;
1631
1632 let out_arr = unsafe { PyArray1::<f64>::new(py, [cells], false) };
1633 let slice_out = unsafe { out_arr.as_slice_mut()? };
1634
1635 let kern = validate_kernel(kernel, true)?;
1636 py.allow_threads(|| {
1637 let batch_k = match kern {
1638 Kernel::Auto => detect_best_batch_kernel(),
1639 k => k,
1640 };
1641
1642 let do_row = |row: usize, dst: &mut [f64]| -> Result<(), CciCycleError> {
1643 let prm = combos[row].clone();
1644 let inp = CciCycleInput::from_slice(slice_in, prm);
1645 let rk = match batch_k {
1646 Kernel::ScalarBatch => Kernel::Scalar,
1647 Kernel::Avx2Batch => Kernel::Avx2,
1648 Kernel::Avx512Batch => Kernel::Avx512,
1649 _ => Kernel::Scalar,
1650 };
1651 cci_cycle_into_slice(dst, &inp, rk)
1652 };
1653 #[cfg(not(target_arch = "wasm32"))]
1654 {
1655 use rayon::prelude::*;
1656 slice_out
1657 .par_chunks_mut(cols)
1658 .enumerate()
1659 .try_for_each(|(r, s)| do_row(r, s))
1660 }
1661 #[cfg(target_arch = "wasm32")]
1662 {
1663 for (r, s) in slice_out.chunks_mut(cols).enumerate() {
1664 do_row(r, s)?;
1665 }
1666 Ok::<(), CciCycleError>(())
1667 }
1668 })
1669 .map_err(|e: CciCycleError| PyValueError::new_err(e.to_string()))?;
1670
1671 let dict = PyDict::new(py);
1672 dict.set_item("values", out_arr.reshape((rows, cols))?)?;
1673 dict.set_item(
1674 "lengths",
1675 combos
1676 .iter()
1677 .map(|p| p.length.unwrap() as u64)
1678 .collect::<Vec<_>>()
1679 .into_pyarray(py),
1680 )?;
1681 dict.set_item(
1682 "factors",
1683 combos
1684 .iter()
1685 .map(|p| p.factor.unwrap())
1686 .collect::<Vec<_>>()
1687 .into_pyarray(py),
1688 )?;
1689 Ok(dict)
1690}
1691
1692#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1693#[wasm_bindgen]
1694pub fn cci_cycle_js(data: &[f64], length: usize, factor: f64) -> Result<Vec<f64>, JsValue> {
1695 let params = CciCycleParams {
1696 length: Some(length),
1697 factor: Some(factor),
1698 };
1699 let input = CciCycleInput::from_slice(data, params);
1700
1701 let mut output = alloc_with_nan_prefix(data.len(), 0);
1702 cci_cycle_into_slice(&mut output, &input, Kernel::Auto)
1703 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1704 Ok(output)
1705}
1706
1707#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1708#[wasm_bindgen]
1709pub fn cci_cycle_alloc(len: usize) -> *mut f64 {
1710 let mut vec = Vec::<f64>::with_capacity(len);
1711 let ptr = vec.as_mut_ptr();
1712 std::mem::forget(vec);
1713 ptr
1714}
1715
1716#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1717#[wasm_bindgen]
1718pub fn cci_cycle_free(ptr: *mut f64, len: usize) {
1719 unsafe {
1720 let _ = Vec::from_raw_parts(ptr, len, len);
1721 }
1722}
1723
1724#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1725#[wasm_bindgen]
1726pub fn cci_cycle_into(
1727 in_ptr: *const f64,
1728 out_ptr: *mut f64,
1729 len: usize,
1730 length: usize,
1731 factor: f64,
1732) -> Result<(), JsValue> {
1733 if in_ptr.is_null() || out_ptr.is_null() {
1734 return Err(JsValue::from_str("null pointer passed to cci_cycle_into"));
1735 }
1736
1737 unsafe {
1738 let data = std::slice::from_raw_parts(in_ptr, len);
1739
1740 let params = CciCycleParams {
1741 length: Some(length),
1742 factor: Some(factor),
1743 };
1744 let input = CciCycleInput::from_slice(data, params);
1745
1746 if in_ptr == out_ptr {
1747 let mut temp = alloc_with_nan_prefix(len, 0);
1748 cci_cycle_into_slice(&mut temp, &input, Kernel::Auto)
1749 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1750 let out = std::slice::from_raw_parts_mut(out_ptr, len);
1751 out.copy_from_slice(&temp);
1752 } else {
1753 let out = std::slice::from_raw_parts_mut(out_ptr, len);
1754 cci_cycle_into_slice(out, &input, Kernel::Auto)
1755 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1756 }
1757
1758 Ok(())
1759 }
1760}
1761
1762#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1763#[derive(Serialize, Deserialize)]
1764pub struct CciCycleBatchConfig {
1765 pub length_range: (usize, usize, usize),
1766 pub factor_range: (f64, f64, f64),
1767}
1768
1769#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1770#[derive(Serialize, Deserialize)]
1771pub struct CciCycleBatchJsOutput {
1772 pub values: Vec<f64>,
1773 pub combos: Vec<CciCycleParams>,
1774 pub rows: usize,
1775 pub cols: usize,
1776}
1777
1778#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1779#[wasm_bindgen(js_name = cci_cycle_batch)]
1780pub fn cci_cycle_batch_unified_js(data: &[f64], config: JsValue) -> Result<JsValue, JsValue> {
1781 let cfg: CciCycleBatchConfig = serde_wasm_bindgen::from_value(config)
1782 .map_err(|e| JsValue::from_str(&format!("Invalid config: {}", e)))?;
1783
1784 let sweep = CciCycleBatchRange {
1785 length: cfg.length_range,
1786 factor: cfg.factor_range,
1787 };
1788 let out = cci_cycle_batch_with_kernel(data, &sweep, detect_best_batch_kernel())
1789 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1790
1791 let js = CciCycleBatchJsOutput {
1792 values: out.values,
1793 combos: out.combos,
1794 rows: out.rows,
1795 cols: out.cols,
1796 };
1797 serde_wasm_bindgen::to_value(&js)
1798 .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
1799}
1800
1801#[cfg(all(feature = "python", feature = "cuda"))]
1802#[pyfunction(name = "cci_cycle_cuda_batch_dev")]
1803#[pyo3(signature = (data, length_range, factor_range, device_id=0))]
1804pub fn cci_cycle_cuda_batch_dev_py(
1805 py: Python<'_>,
1806 data: PyReadonlyArray1<'_, f32>,
1807 length_range: (usize, usize, usize),
1808 factor_range: (f64, f64, f64),
1809 device_id: usize,
1810) -> PyResult<CciCycleDeviceArrayF32Py> {
1811 use crate::cuda::cuda_available;
1812 if !cuda_available() {
1813 return Err(PyValueError::new_err("CUDA not available"));
1814 }
1815 let slice = data.as_slice()?;
1816 let sweep = CciCycleBatchRange {
1817 length: length_range,
1818 factor: factor_range,
1819 };
1820 let (inner, dev_id, ctx) = py.allow_threads(|| {
1821 let cuda =
1822 CudaCciCycle::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
1823 let dev_id = cuda.device_id();
1824 let ctx = cuda.context_arc();
1825 let out = cuda
1826 .cci_cycle_batch_dev(slice, &sweep)
1827 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1828 Ok::<_, PyErr>((out, dev_id, ctx))
1829 })?;
1830 Ok(CciCycleDeviceArrayF32Py {
1831 inner: Some(inner),
1832 _ctx: ctx,
1833 device_id: dev_id,
1834 })
1835}
1836
1837#[cfg(all(feature = "python", feature = "cuda"))]
1838#[pyfunction(name = "cci_cycle_cuda_many_series_one_param_dev")]
1839#[pyo3(signature = (data_time_major, cols, rows, length, factor, device_id=0))]
1840pub fn cci_cycle_cuda_many_series_one_param_dev_py(
1841 py: Python<'_>,
1842 data_time_major: PyReadonlyArray1<'_, f32>,
1843 cols: usize,
1844 rows: usize,
1845 length: usize,
1846 factor: f64,
1847 device_id: usize,
1848) -> PyResult<CciCycleDeviceArrayF32Py> {
1849 use crate::cuda::cuda_available;
1850 if !cuda_available() {
1851 return Err(PyValueError::new_err("CUDA not available"));
1852 }
1853 let slice = data_time_major.as_slice()?;
1854 if slice.len() != cols * rows {
1855 return Err(PyValueError::new_err("size mismatch for time-major matrix"));
1856 }
1857 let params = CciCycleParams {
1858 length: Some(length),
1859 factor: Some(factor),
1860 };
1861 let (inner, dev_id, ctx) = py.allow_threads(|| {
1862 let cuda =
1863 CudaCciCycle::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
1864 let dev_id = cuda.device_id();
1865 let ctx = cuda.context_arc();
1866 let out = cuda
1867 .cci_cycle_many_series_one_param_time_major_dev(slice, cols, rows, ¶ms)
1868 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1869 Ok::<_, PyErr>((out, dev_id, ctx))
1870 })?;
1871 Ok(CciCycleDeviceArrayF32Py {
1872 inner: Some(inner),
1873 _ctx: ctx,
1874 device_id: dev_id,
1875 })
1876}
1877
1878#[cfg(all(feature = "python", feature = "cuda"))]
1879#[pyclass(module = "ta_indicators.cuda", unsendable)]
1880pub struct CciCycleDeviceArrayF32Py {
1881 pub(crate) inner: Option<DeviceArrayF32>,
1882 pub(crate) _ctx: std::sync::Arc<cust::context::Context>,
1883 pub(crate) device_id: u32,
1884}
1885
1886#[cfg(all(feature = "python", feature = "cuda"))]
1887#[pymethods]
1888impl CciCycleDeviceArrayF32Py {
1889 #[getter]
1890 fn __cuda_array_interface__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
1891 let inner = self
1892 .inner
1893 .as_ref()
1894 .ok_or_else(|| PyValueError::new_err("buffer already exported via __dlpack__"))?;
1895 let d = PyDict::new(py);
1896 d.set_item("shape", (inner.rows, inner.cols))?;
1897 d.set_item("typestr", "<f4")?;
1898 d.set_item(
1899 "strides",
1900 (
1901 inner.cols * std::mem::size_of::<f32>(),
1902 std::mem::size_of::<f32>(),
1903 ),
1904 )?;
1905 let size = inner.rows.saturating_mul(inner.cols);
1906 let ptr_val: usize = if size == 0 {
1907 0
1908 } else {
1909 inner.device_ptr() as usize
1910 };
1911 d.set_item("data", (ptr_val, false))?;
1912
1913 d.set_item("version", 3)?;
1914 Ok(d)
1915 }
1916
1917 fn __dlpack_device__(&self) -> PyResult<(i32, i32)> {
1918 Ok((2, self.device_id as i32))
1919 }
1920
1921 #[pyo3(signature = (stream=None, max_version=None, dl_device=None, copy=None))]
1922 fn __dlpack__<'py>(
1923 &mut self,
1924 py: Python<'py>,
1925 stream: Option<PyObject>,
1926 max_version: Option<PyObject>,
1927 dl_device: Option<PyObject>,
1928 copy: Option<PyObject>,
1929 ) -> PyResult<PyObject> {
1930 let (kdl, alloc_dev) = self.__dlpack_device__()?;
1931 if let Some(dev_obj) = dl_device.as_ref() {
1932 if let Ok((dev_ty, dev_id)) = dev_obj.extract::<(i32, i32)>(py) {
1933 if dev_ty != kdl || dev_id != alloc_dev {
1934 let wants_copy = copy
1935 .as_ref()
1936 .and_then(|c| c.extract::<bool>(py).ok())
1937 .unwrap_or(false);
1938 if wants_copy {
1939 return Err(PyValueError::new_err(
1940 "device copy not implemented for __dlpack__",
1941 ));
1942 } else {
1943 return Err(PyValueError::new_err("dl_device mismatch for __dlpack__"));
1944 }
1945 }
1946 }
1947 }
1948
1949 let _ = stream;
1950
1951 let inner = self
1952 .inner
1953 .take()
1954 .ok_or_else(|| PyValueError::new_err("__dlpack__ may only be called once"))?;
1955
1956 let rows = inner.rows;
1957 let cols = inner.cols;
1958 let buf = inner.buf;
1959
1960 let max_version_bound = max_version.map(|obj| obj.into_bound(py));
1961
1962 export_f32_cuda_dlpack_2d(py, buf, rows, cols, alloc_dev, max_version_bound)
1963 }
1964}
1965
1966#[cfg(test)]
1967mod tests {
1968 use super::*;
1969 use crate::skip_if_unsupported;
1970 use crate::utilities::data_loader::read_candles_from_csv;
1971 #[cfg(feature = "proptest")]
1972 use proptest::prelude::*;
1973 use std::error::Error;
1974
1975 macro_rules! generate_all_cci_cycle_tests {
1976 ($($test_fn:ident),*) => {
1977 paste::paste! {
1978 $(
1979 #[test]
1980 fn [<$test_fn _scalar>]() -> Result<(), Box<dyn Error>> {
1981 $test_fn(stringify!([<$test_fn _scalar>]), Kernel::Scalar)
1982 }
1983 )*
1984 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1985 $(
1986 #[test]
1987 fn [<$test_fn _avx2>]() -> Result<(), Box<dyn Error>> {
1988 $test_fn(stringify!([<$test_fn _avx2>]), Kernel::Avx2)
1989 }
1990 )*
1991 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1992 $(
1993 #[test]
1994 fn [<$test_fn _avx512>]() -> Result<(), Box<dyn Error>> {
1995 $test_fn(stringify!([<$test_fn _avx512>]), Kernel::Avx512)
1996 }
1997 )*
1998 }
1999 };
2000 }
2001
2002 macro_rules! gen_batch_tests {
2003 ($fn_name:ident) => {
2004 paste::paste! {
2005 #[test]
2006 fn [<$fn_name _scalar>]() -> Result<(), Box<dyn Error>> {
2007 $fn_name(stringify!([<$fn_name _scalar>]), Kernel::ScalarBatch)
2008 }
2009 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2010 #[test]
2011 fn [<$fn_name _avx2>]() -> Result<(), Box<dyn Error>> {
2012 $fn_name(stringify!([<$fn_name _avx2>]), Kernel::Avx2Batch)
2013 }
2014 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2015 #[test]
2016 fn [<$fn_name _avx512>]() -> Result<(), Box<dyn Error>> {
2017 $fn_name(stringify!([<$fn_name _avx512>]), Kernel::Avx512Batch)
2018 }
2019 }
2020 };
2021 }
2022
2023 fn check_cci_cycle_accuracy(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2024 skip_if_unsupported!(kernel, test_name);
2025 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2026 let candles = read_candles_from_csv(file_path)?;
2027
2028 let input = CciCycleInput::from_candles(&candles, "close", CciCycleParams::default());
2029 let result = cci_cycle_with_kernel(&input, kernel)?;
2030
2031 let expected_last_five = [
2032 9.25177192,
2033 20.49219826,
2034 35.42917181,
2035 55.57843075,
2036 77.78921538,
2037 ];
2038
2039 let start = result.values.len().saturating_sub(5);
2040 for (i, &val) in result.values[start..].iter().enumerate() {
2041 let diff = (val - expected_last_five[i]).abs();
2042 assert!(
2043 diff < 1e-6,
2044 "[{}] CCI_CYCLE {:?} mismatch at idx {}: got {}, expected {}",
2045 test_name,
2046 kernel,
2047 i,
2048 val,
2049 expected_last_five[i]
2050 );
2051 }
2052 Ok(())
2053 }
2054
2055 #[test]
2056 fn test_cci_cycle_into_matches_api() -> Result<(), Box<dyn Error>> {
2057 let n = 256usize;
2058 let mut data: Vec<f64> = (0..n)
2059 .map(|i| ((i as f64) * 0.037).sin() * 2.0 + (i as f64) * 0.01)
2060 .collect();
2061 data[0] = f64::NAN;
2062 data[1] = f64::NAN;
2063 data[2] = f64::NAN;
2064
2065 let params = CciCycleParams::default();
2066 let input = CciCycleInput::from_slice(&data, params);
2067
2068 let baseline = cci_cycle(&input)?.values;
2069
2070 let mut out = vec![0.0; data.len()];
2071 cci_cycle_into(&input, &mut out)?;
2072
2073 assert_eq!(baseline.len(), out.len());
2074
2075 #[inline]
2076 fn eq_or_both_nan(a: f64, b: f64) -> bool {
2077 (a.is_nan() && b.is_nan()) || (a == b)
2078 }
2079
2080 for i in 0..out.len() {
2081 let a = baseline[i];
2082 let b = out[i];
2083 assert!(
2084 eq_or_both_nan(a, b) || (a - b).abs() <= 1e-12,
2085 "cci_cycle_into parity mismatch at {}: got {}, expected {}",
2086 i,
2087 b,
2088 a
2089 );
2090 }
2091 Ok(())
2092 }
2093
2094 fn check_cci_cycle_no_poison(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2095 skip_if_unsupported!(kernel, test_name);
2096 let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2097 let c = read_candles_from_csv(file)?;
2098
2099 let out = cci_cycle_with_kernel(&CciCycleInput::with_default_candles(&c), kernel)?.values;
2100 for (i, &v) in out.iter().enumerate() {
2101 if v.is_nan() {
2102 continue;
2103 }
2104 let b = v.to_bits();
2105 assert_ne!(
2106 b, 0x11111111_11111111,
2107 "[{}] alloc_with_nan_prefix poison at {}",
2108 test_name, i
2109 );
2110 assert_ne!(
2111 b, 0x22222222_22222222,
2112 "[{}] init_matrix_prefixes poison at {}",
2113 test_name, i
2114 );
2115 assert_ne!(
2116 b, 0x33333333_33333333,
2117 "[{}] make_uninit_matrix poison at {}",
2118 test_name, i
2119 );
2120 }
2121 Ok(())
2122 }
2123
2124 fn check_cci_cycle_partial_params(
2125 test_name: &str,
2126 kernel: Kernel,
2127 ) -> Result<(), Box<dyn Error>> {
2128 skip_if_unsupported!(kernel, test_name);
2129 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2130 let candles = read_candles_from_csv(file_path)?;
2131
2132 let default_params = CciCycleParams {
2133 length: None,
2134 factor: None,
2135 };
2136 let input = CciCycleInput::from_candles(&candles, "close", default_params);
2137 let output = cci_cycle_with_kernel(&input, kernel)?;
2138 assert_eq!(output.values.len(), candles.close.len());
2139
2140 Ok(())
2141 }
2142
2143 fn check_cci_cycle_default_candles(
2144 test_name: &str,
2145 kernel: Kernel,
2146 ) -> Result<(), Box<dyn Error>> {
2147 skip_if_unsupported!(kernel, test_name);
2148 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2149 let candles = read_candles_from_csv(file_path)?;
2150
2151 let input = CciCycleInput::with_default_candles(&candles);
2152 match input.data {
2153 CciCycleData::Candles { source, .. } => assert_eq!(source, "close"),
2154 _ => panic!("Expected CciCycleData::Candles"),
2155 }
2156 let output = cci_cycle_with_kernel(&input, kernel)?;
2157 assert_eq!(output.values.len(), candles.close.len());
2158
2159 Ok(())
2160 }
2161
2162 fn check_cci_cycle_zero_period(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2163 skip_if_unsupported!(kernel, test_name);
2164 let input_data = [10.0, 20.0, 30.0];
2165 let params = CciCycleParams {
2166 length: Some(0),
2167 factor: None,
2168 };
2169 let input = CciCycleInput::from_slice(&input_data, params);
2170 let res = cci_cycle_with_kernel(&input, kernel);
2171 assert!(
2172 res.is_err(),
2173 "[{}] CCI_CYCLE should fail with zero period",
2174 test_name
2175 );
2176 Ok(())
2177 }
2178
2179 fn check_cci_cycle_period_exceeds_length(
2180 test_name: &str,
2181 kernel: Kernel,
2182 ) -> Result<(), Box<dyn Error>> {
2183 skip_if_unsupported!(kernel, test_name);
2184 let data_small = [10.0, 20.0, 30.0];
2185 let params = CciCycleParams {
2186 length: Some(10),
2187 factor: None,
2188 };
2189 let input = CciCycleInput::from_slice(&data_small, params);
2190 let res = cci_cycle_with_kernel(&input, kernel);
2191 assert!(
2192 res.is_err(),
2193 "[{}] CCI_CYCLE should fail with period exceeding length",
2194 test_name
2195 );
2196 Ok(())
2197 }
2198
2199 fn check_cci_cycle_very_small_dataset(
2200 test_name: &str,
2201 kernel: Kernel,
2202 ) -> Result<(), Box<dyn Error>> {
2203 skip_if_unsupported!(kernel, test_name);
2204 let single_point = [42.0];
2205 let params = CciCycleParams::default();
2206 let input = CciCycleInput::from_slice(&single_point, params);
2207 let res = cci_cycle_with_kernel(&input, kernel);
2208 assert!(
2209 res.is_err(),
2210 "[{}] CCI_CYCLE should fail with insufficient data",
2211 test_name
2212 );
2213 Ok(())
2214 }
2215
2216 fn check_cci_cycle_empty_input(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2217 skip_if_unsupported!(kernel, test_name);
2218 let empty: [f64; 0] = [];
2219 let params = CciCycleParams::default();
2220 let input = CciCycleInput::from_slice(&empty, params);
2221 let res = cci_cycle_with_kernel(&input, kernel);
2222 assert!(
2223 res.is_err(),
2224 "[{}] CCI_CYCLE should fail with empty input",
2225 test_name
2226 );
2227 Ok(())
2228 }
2229
2230 fn check_cci_cycle_all_nan(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2231 skip_if_unsupported!(kernel, test_name);
2232 let nan_data = [f64::NAN, f64::NAN, f64::NAN];
2233 let params = CciCycleParams::default();
2234 let input = CciCycleInput::from_slice(&nan_data, params);
2235 let res = cci_cycle_with_kernel(&input, kernel);
2236 assert!(
2237 res.is_err(),
2238 "[{}] CCI_CYCLE should fail with all NaN values",
2239 test_name
2240 );
2241 Ok(())
2242 }
2243
2244 fn check_cci_cycle_reinput(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2245 skip_if_unsupported!(kernel, test_name);
2246 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2247 let candles = read_candles_from_csv(file_path)?;
2248
2249 let input = CciCycleInput::from_candles(&candles, "close", CciCycleParams::default());
2250 let output1 = cci_cycle_with_kernel(&input, kernel)?;
2251
2252 let input2 = CciCycleInput::from_slice(&output1.values, CciCycleParams::default());
2253 let output2 = cci_cycle_with_kernel(&input2, kernel)?;
2254
2255 assert_eq!(output2.values.len(), output1.values.len());
2256
2257 let non_nan_count = output2.values.iter().filter(|&&v| !v.is_nan()).count();
2258 assert!(
2259 non_nan_count > 0,
2260 "[{}] Reinput produced all NaN values",
2261 test_name
2262 );
2263
2264 Ok(())
2265 }
2266
2267 fn check_cci_cycle_nan_handling(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2268 skip_if_unsupported!(kernel, test_name);
2269
2270 let data_with_nans = vec![
2271 1.0,
2272 2.0,
2273 3.0,
2274 4.0,
2275 5.0,
2276 6.0,
2277 7.0,
2278 8.0,
2279 9.0,
2280 10.0,
2281 11.0,
2282 12.0,
2283 f64::NAN,
2284 14.0,
2285 15.0,
2286 16.0,
2287 17.0,
2288 18.0,
2289 19.0,
2290 20.0,
2291 21.0,
2292 22.0,
2293 23.0,
2294 24.0,
2295 25.0,
2296 26.0,
2297 27.0,
2298 28.0,
2299 29.0,
2300 30.0,
2301 31.0,
2302 32.0,
2303 33.0,
2304 34.0,
2305 35.0,
2306 36.0,
2307 37.0,
2308 38.0,
2309 39.0,
2310 40.0,
2311 ];
2312
2313 let params = CciCycleParams {
2314 length: Some(5),
2315 factor: Some(0.5),
2316 };
2317 let input = CciCycleInput::from_slice(&data_with_nans, params.clone());
2318 let result = cci_cycle_with_kernel(&input, kernel);
2319
2320 assert!(
2321 result.is_ok(),
2322 "[{}] Should handle data with some NaN values",
2323 test_name
2324 );
2325
2326 if let Ok(output) = result {
2327 assert_eq!(output.values.len(), data_with_nans.len());
2328
2329 let valid_count = output.values.iter().filter(|&&v| !v.is_nan()).count();
2330 assert!(
2331 valid_count > 0,
2332 "[{}] Should produce some valid values",
2333 test_name
2334 );
2335 }
2336
2337 let mostly_nans = vec![f64::NAN; 20];
2338 let input2 = CciCycleInput::from_slice(&mostly_nans, params);
2339 let result2 = cci_cycle_with_kernel(&input2, kernel);
2340 assert!(
2341 result2.is_err(),
2342 "[{}] Should fail with all NaN values",
2343 test_name
2344 );
2345
2346 Ok(())
2347 }
2348
2349 fn check_cci_cycle_streaming(test_name: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
2350 let params = CciCycleParams {
2351 length: Some(10),
2352 factor: Some(0.5),
2353 };
2354
2355 let stream_result = CciCycleStream::try_new(params.clone());
2356 assert!(
2357 stream_result.is_ok(),
2358 "[{}] Stream creation should succeed",
2359 test_name
2360 );
2361
2362 let mut stream = stream_result?;
2363
2364 let test_data = vec![
2365 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0,
2366 17.0, 18.0, 19.0, 20.0,
2367 ];
2368
2369 for &value in &test_data {
2370 let _ = stream.update(value);
2371 }
2372
2373 Ok(())
2374 }
2375
2376 fn check_batch_default_row(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2377 skip_if_unsupported!(kernel, test_name);
2378
2379 let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2380 let candles = read_candles_from_csv(file)?;
2381
2382 let output = CciCycleBatchBuilder::new()
2383 .kernel(kernel)
2384 .apply_candles(&candles, "close")?;
2385
2386 let default_params = CciCycleParams::default();
2387 let row = output.values_for(&default_params);
2388
2389 assert!(
2390 row.is_some(),
2391 "[{}] Default parameters not found in batch output",
2392 test_name
2393 );
2394
2395 if let Some(values) = row {
2396 assert_eq!(values.len(), candles.close.len());
2397
2398 let non_nan_count = values.iter().filter(|&&v| !v.is_nan()).count();
2399 assert!(
2400 non_nan_count > 0,
2401 "[{}] Default row has no valid values",
2402 test_name
2403 );
2404 }
2405
2406 assert_eq!(output.cols, candles.close.len());
2407
2408 Ok(())
2409 }
2410
2411 fn check_batch_sweep(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2412 skip_if_unsupported!(kernel, test_name);
2413
2414 let data = vec![1.0; 100];
2415
2416 let output = CciCycleBatchBuilder::new()
2417 .kernel(kernel)
2418 .length_range(10, 20, 5)
2419 .factor_range(0.3, 0.7, 0.2)
2420 .apply_slice(&data)?;
2421
2422 assert_eq!(
2423 output.combos.len(),
2424 9,
2425 "[{}] Unexpected number of parameter combinations",
2426 test_name
2427 );
2428 assert_eq!(output.rows, 9);
2429 assert_eq!(output.cols, 100);
2430 assert_eq!(output.values.len(), 900);
2431
2432 Ok(())
2433 }
2434
2435 generate_all_cci_cycle_tests!(
2436 check_cci_cycle_accuracy,
2437 check_cci_cycle_no_poison,
2438 check_cci_cycle_partial_params,
2439 check_cci_cycle_default_candles,
2440 check_cci_cycle_zero_period,
2441 check_cci_cycle_period_exceeds_length,
2442 check_cci_cycle_very_small_dataset,
2443 check_cci_cycle_empty_input,
2444 check_cci_cycle_all_nan,
2445 check_cci_cycle_reinput,
2446 check_cci_cycle_nan_handling,
2447 check_cci_cycle_streaming
2448 );
2449
2450 gen_batch_tests!(check_batch_default_row);
2451 gen_batch_tests!(check_batch_sweep);
2452
2453 #[cfg(feature = "proptest")]
2454 proptest! {
2455 #[test]
2456 fn test_cci_cycle_no_panic(data: Vec<f64>, length in 1usize..100) {
2457 let params = CciCycleParams {
2458 length: Some(length),
2459 factor: Some(0.5),
2460 };
2461 let input = CciCycleInput::from_slice(&data, params);
2462 let _ = cci_cycle(&input);
2463 }
2464
2465 #[test]
2466 fn test_cci_cycle_length_preservation(size in 10usize..100) {
2467 let data: Vec<f64> = (0..size).map(|i| i as f64).collect();
2468 let params = CciCycleParams::default();
2469 let input = CciCycleInput::from_slice(&data, params);
2470
2471 if let Ok(output) = cci_cycle(&input) {
2472 prop_assert_eq!(output.values.len(), size);
2473 }
2474 }
2475 }
2476}