Skip to main content

vector_ta/indicators/
cci_cycle.rs

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