Skip to main content

vector_ta/indicators/
cycle_channel_oscillator.rs

1#[cfg(feature = "python")]
2use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
3#[cfg(feature = "python")]
4use pyo3::exceptions::PyValueError;
5#[cfg(feature = "python")]
6use pyo3::prelude::*;
7#[cfg(feature = "python")]
8use pyo3::types::PyDict;
9
10#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
11use serde::{Deserialize, Serialize};
12#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
13use wasm_bindgen::prelude::*;
14
15use crate::utilities::data_loader::{source_type, Candles};
16use crate::utilities::enums::Kernel;
17use crate::utilities::helpers::{
18    alloc_with_nan_prefix, detect_best_batch_kernel, init_matrix_prefixes, make_uninit_matrix,
19};
20#[cfg(feature = "python")]
21use crate::utilities::kernel_validation::validate_kernel;
22#[cfg(not(target_arch = "wasm32"))]
23use rayon::prelude::*;
24use std::collections::VecDeque;
25use std::mem::ManuallyDrop;
26use thiserror::Error;
27
28const DEFAULT_SOURCE: &str = "close";
29const DEFAULT_SHORT_CYCLE_LENGTH: usize = 10;
30const DEFAULT_MEDIUM_CYCLE_LENGTH: usize = 30;
31const DEFAULT_SHORT_MULTIPLIER: f64 = 1.0;
32const DEFAULT_MEDIUM_MULTIPLIER: f64 = 3.0;
33
34#[derive(Debug, Clone)]
35pub enum CycleChannelOscillatorData<'a> {
36    Candles {
37        candles: &'a Candles,
38        source: &'a str,
39    },
40    Slices {
41        source: &'a [f64],
42        high: &'a [f64],
43        low: &'a [f64],
44        close: &'a [f64],
45    },
46}
47
48#[derive(Debug, Clone)]
49pub struct CycleChannelOscillatorOutput {
50    pub fast: Vec<f64>,
51    pub slow: Vec<f64>,
52}
53
54#[derive(Debug, Clone)]
55#[cfg_attr(
56    all(target_arch = "wasm32", feature = "wasm"),
57    derive(Serialize, Deserialize)
58)]
59pub struct CycleChannelOscillatorParams {
60    pub short_cycle_length: Option<usize>,
61    pub medium_cycle_length: Option<usize>,
62    pub short_multiplier: Option<f64>,
63    pub medium_multiplier: Option<f64>,
64}
65
66impl Default for CycleChannelOscillatorParams {
67    fn default() -> Self {
68        Self {
69            short_cycle_length: Some(DEFAULT_SHORT_CYCLE_LENGTH),
70            medium_cycle_length: Some(DEFAULT_MEDIUM_CYCLE_LENGTH),
71            short_multiplier: Some(DEFAULT_SHORT_MULTIPLIER),
72            medium_multiplier: Some(DEFAULT_MEDIUM_MULTIPLIER),
73        }
74    }
75}
76
77#[derive(Debug, Clone)]
78pub struct CycleChannelOscillatorInput<'a> {
79    pub data: CycleChannelOscillatorData<'a>,
80    pub params: CycleChannelOscillatorParams,
81}
82
83impl<'a> CycleChannelOscillatorInput<'a> {
84    #[inline]
85    pub fn from_candles(
86        candles: &'a Candles,
87        source: &'a str,
88        params: CycleChannelOscillatorParams,
89    ) -> Self {
90        Self {
91            data: CycleChannelOscillatorData::Candles { candles, source },
92            params,
93        }
94    }
95
96    #[inline]
97    pub fn from_slices(
98        source: &'a [f64],
99        high: &'a [f64],
100        low: &'a [f64],
101        close: &'a [f64],
102        params: CycleChannelOscillatorParams,
103    ) -> Self {
104        Self {
105            data: CycleChannelOscillatorData::Slices {
106                source,
107                high,
108                low,
109                close,
110            },
111            params,
112        }
113    }
114
115    #[inline]
116    pub fn with_default_candles(candles: &'a Candles) -> Self {
117        Self::from_candles(
118            candles,
119            DEFAULT_SOURCE,
120            CycleChannelOscillatorParams::default(),
121        )
122    }
123}
124
125#[derive(Copy, Clone, Debug)]
126pub struct CycleChannelOscillatorBuilder {
127    source: Option<&'static str>,
128    short_cycle_length: Option<usize>,
129    medium_cycle_length: Option<usize>,
130    short_multiplier: Option<f64>,
131    medium_multiplier: Option<f64>,
132    kernel: Kernel,
133}
134
135impl Default for CycleChannelOscillatorBuilder {
136    fn default() -> Self {
137        Self {
138            source: None,
139            short_cycle_length: None,
140            medium_cycle_length: None,
141            short_multiplier: None,
142            medium_multiplier: None,
143            kernel: Kernel::Auto,
144        }
145    }
146}
147
148impl CycleChannelOscillatorBuilder {
149    #[inline(always)]
150    pub fn new() -> Self {
151        Self::default()
152    }
153
154    #[inline(always)]
155    pub fn source(mut self, value: &'static str) -> Self {
156        self.source = Some(value);
157        self
158    }
159
160    #[inline(always)]
161    pub fn short_cycle_length(mut self, value: usize) -> Self {
162        self.short_cycle_length = Some(value);
163        self
164    }
165
166    #[inline(always)]
167    pub fn medium_cycle_length(mut self, value: usize) -> Self {
168        self.medium_cycle_length = Some(value);
169        self
170    }
171
172    #[inline(always)]
173    pub fn short_multiplier(mut self, value: f64) -> Self {
174        self.short_multiplier = Some(value);
175        self
176    }
177
178    #[inline(always)]
179    pub fn medium_multiplier(mut self, value: f64) -> Self {
180        self.medium_multiplier = Some(value);
181        self
182    }
183
184    #[inline(always)]
185    pub fn kernel(mut self, value: Kernel) -> Self {
186        self.kernel = value;
187        self
188    }
189
190    #[inline(always)]
191    pub fn apply(
192        self,
193        candles: &Candles,
194    ) -> Result<CycleChannelOscillatorOutput, CycleChannelOscillatorError> {
195        let input = CycleChannelOscillatorInput::from_candles(
196            candles,
197            self.source.unwrap_or(DEFAULT_SOURCE),
198            CycleChannelOscillatorParams {
199                short_cycle_length: self.short_cycle_length,
200                medium_cycle_length: self.medium_cycle_length,
201                short_multiplier: self.short_multiplier,
202                medium_multiplier: self.medium_multiplier,
203            },
204        );
205        cycle_channel_oscillator_with_kernel(&input, self.kernel)
206    }
207
208    #[inline(always)]
209    pub fn apply_slices(
210        self,
211        source: &[f64],
212        high: &[f64],
213        low: &[f64],
214        close: &[f64],
215    ) -> Result<CycleChannelOscillatorOutput, CycleChannelOscillatorError> {
216        let input = CycleChannelOscillatorInput::from_slices(
217            source,
218            high,
219            low,
220            close,
221            CycleChannelOscillatorParams {
222                short_cycle_length: self.short_cycle_length,
223                medium_cycle_length: self.medium_cycle_length,
224                short_multiplier: self.short_multiplier,
225                medium_multiplier: self.medium_multiplier,
226            },
227        );
228        cycle_channel_oscillator_with_kernel(&input, self.kernel)
229    }
230
231    #[inline(always)]
232    pub fn into_stream(self) -> Result<CycleChannelOscillatorStream, CycleChannelOscillatorError> {
233        CycleChannelOscillatorStream::try_new(CycleChannelOscillatorParams {
234            short_cycle_length: self.short_cycle_length,
235            medium_cycle_length: self.medium_cycle_length,
236            short_multiplier: self.short_multiplier,
237            medium_multiplier: self.medium_multiplier,
238        })
239    }
240}
241
242#[derive(Debug, Error)]
243pub enum CycleChannelOscillatorError {
244    #[error("cycle_channel_oscillator: Input data slice is empty.")]
245    EmptyInputData,
246    #[error("cycle_channel_oscillator: All values are NaN.")]
247    AllValuesNaN,
248    #[error("cycle_channel_oscillator: Inconsistent slice lengths: source={source_len}, high={high_len}, low={low_len}, close={close_len}")]
249    InconsistentSliceLengths {
250        source_len: usize,
251        high_len: usize,
252        low_len: usize,
253        close_len: usize,
254    },
255    #[error("cycle_channel_oscillator: Invalid cycle length `{name}`: {value}")]
256    InvalidCycleLength { name: &'static str, value: usize },
257    #[error("cycle_channel_oscillator: Invalid multiplier `{name}`: {value}")]
258    InvalidMultiplier { name: &'static str, value: f64 },
259    #[error("cycle_channel_oscillator: Not enough valid data: needed={needed}, valid={valid}")]
260    NotEnoughValidData { needed: usize, valid: usize },
261    #[error("cycle_channel_oscillator: Output length mismatch: expected={expected}, got={got}")]
262    OutputLengthMismatch { expected: usize, got: usize },
263    #[error("cycle_channel_oscillator: Invalid range: start={start}, end={end}, step={step}")]
264    InvalidRange {
265        start: String,
266        end: String,
267        step: String,
268    },
269    #[error("cycle_channel_oscillator: Invalid kernel for batch: {0:?}")]
270    InvalidKernelForBatch(Kernel),
271}
272
273#[derive(Debug, Clone, Copy)]
274struct ResolvedParams {
275    short_cycle_length: usize,
276    medium_cycle_length: usize,
277    short_multiplier: f64,
278    medium_multiplier: f64,
279    short_period: usize,
280    medium_period: usize,
281    short_delay: usize,
282    medium_delay: usize,
283}
284
285#[inline(always)]
286fn extract_slices<'a>(
287    input: &'a CycleChannelOscillatorInput<'a>,
288) -> Result<(&'a [f64], &'a [f64], &'a [f64], &'a [f64]), CycleChannelOscillatorError> {
289    let (source, high, low, close) = match &input.data {
290        CycleChannelOscillatorData::Candles { candles, source } => (
291            source_type(candles, source),
292            candles.high.as_slice(),
293            candles.low.as_slice(),
294            candles.close.as_slice(),
295        ),
296        CycleChannelOscillatorData::Slices {
297            source,
298            high,
299            low,
300            close,
301        } => (*source, *high, *low, *close),
302    };
303
304    if source.is_empty() || high.is_empty() || low.is_empty() || close.is_empty() {
305        return Err(CycleChannelOscillatorError::EmptyInputData);
306    }
307    if source.len() != high.len() || source.len() != low.len() || source.len() != close.len() {
308        return Err(CycleChannelOscillatorError::InconsistentSliceLengths {
309            source_len: source.len(),
310            high_len: high.len(),
311            low_len: low.len(),
312            close_len: close.len(),
313        });
314    }
315    Ok((source, high, low, close))
316}
317
318#[inline(always)]
319fn first_valid_quad(source: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Option<usize> {
320    (0..source.len()).find(|&i| {
321        source[i].is_finite() && high[i].is_finite() && low[i].is_finite() && close[i].is_finite()
322    })
323}
324
325#[inline(always)]
326fn resolve_params(
327    params: &CycleChannelOscillatorParams,
328) -> Result<ResolvedParams, CycleChannelOscillatorError> {
329    let short_cycle_length = params
330        .short_cycle_length
331        .unwrap_or(DEFAULT_SHORT_CYCLE_LENGTH);
332    let medium_cycle_length = params
333        .medium_cycle_length
334        .unwrap_or(DEFAULT_MEDIUM_CYCLE_LENGTH);
335    let short_multiplier = params.short_multiplier.unwrap_or(DEFAULT_SHORT_MULTIPLIER);
336    let medium_multiplier = params
337        .medium_multiplier
338        .unwrap_or(DEFAULT_MEDIUM_MULTIPLIER);
339
340    if short_cycle_length < 2 {
341        return Err(CycleChannelOscillatorError::InvalidCycleLength {
342            name: "short_cycle_length",
343            value: short_cycle_length,
344        });
345    }
346    if medium_cycle_length < 2 {
347        return Err(CycleChannelOscillatorError::InvalidCycleLength {
348            name: "medium_cycle_length",
349            value: medium_cycle_length,
350        });
351    }
352    if !short_multiplier.is_finite() || short_multiplier < 0.0 {
353        return Err(CycleChannelOscillatorError::InvalidMultiplier {
354            name: "short_multiplier",
355            value: short_multiplier,
356        });
357    }
358    if !medium_multiplier.is_finite() || medium_multiplier < 0.0 {
359        return Err(CycleChannelOscillatorError::InvalidMultiplier {
360            name: "medium_multiplier",
361            value: medium_multiplier,
362        });
363    }
364
365    let short_period = short_cycle_length / 2;
366    let medium_period = medium_cycle_length / 2;
367    let short_delay = short_period / 2;
368    let medium_delay = medium_period / 2;
369
370    Ok(ResolvedParams {
371        short_cycle_length,
372        medium_cycle_length,
373        short_multiplier,
374        medium_multiplier,
375        short_period,
376        medium_period,
377        short_delay,
378        medium_delay,
379    })
380}
381
382#[derive(Debug, Clone)]
383struct RmaState {
384    length: usize,
385    count: usize,
386    sum: f64,
387    value: f64,
388}
389
390impl RmaState {
391    #[inline(always)]
392    fn new(length: usize) -> Self {
393        Self {
394            length,
395            count: 0,
396            sum: 0.0,
397            value: f64::NAN,
398        }
399    }
400
401    #[inline(always)]
402    fn update(&mut self, input: f64) -> f64 {
403        if self.count < self.length {
404            self.sum += input;
405            self.count += 1;
406            if self.count == self.length {
407                self.value = self.sum / self.length as f64;
408            }
409        } else {
410            self.value = self.value + (input - self.value) / self.length as f64;
411            self.count += 1;
412        }
413        self.value
414    }
415}
416
417#[derive(Debug, Clone)]
418struct AtrState {
419    rma: RmaState,
420    prev_close: Option<f64>,
421}
422
423impl AtrState {
424    #[inline(always)]
425    fn new(length: usize) -> Self {
426        Self {
427            rma: RmaState::new(length),
428            prev_close: None,
429        }
430    }
431
432    #[inline(always)]
433    fn update(&mut self, high: f64, low: f64, close: f64) -> f64 {
434        let tr = match self.prev_close {
435            Some(prev_close) => (high - low)
436                .max((high - prev_close).abs())
437                .max((low - prev_close).abs()),
438            None => high - low,
439        };
440        self.prev_close = Some(close);
441        self.rma.update(tr)
442    }
443}
444
445#[inline(always)]
446fn delayed_or_source(history: &VecDeque<f64>, delay: usize, source: f64) -> f64 {
447    if history.len() > delay {
448        let idx = history.len() - 1 - delay;
449        match history.get(idx).copied() {
450            Some(value) if value.is_finite() => value,
451            _ => source,
452        }
453    } else {
454        source
455    }
456}
457
458#[derive(Debug, Clone)]
459struct CycleChannelOscillatorCore {
460    short_rma: RmaState,
461    medium_rma: RmaState,
462    medium_atr: AtrState,
463    short_delay: usize,
464    medium_delay: usize,
465    medium_multiplier: f64,
466    short_history: VecDeque<f64>,
467    medium_history: VecDeque<f64>,
468}
469
470impl CycleChannelOscillatorCore {
471    #[inline(always)]
472    fn new(resolved: ResolvedParams) -> Self {
473        let _ = resolved.short_cycle_length;
474        let _ = resolved.medium_cycle_length;
475        let _ = resolved.short_multiplier;
476        Self {
477            short_rma: RmaState::new(resolved.short_period),
478            medium_rma: RmaState::new(resolved.medium_period),
479            medium_atr: AtrState::new(resolved.medium_period),
480            short_delay: resolved.short_delay,
481            medium_delay: resolved.medium_delay,
482            medium_multiplier: resolved.medium_multiplier,
483            short_history: VecDeque::with_capacity(resolved.short_delay + 2),
484            medium_history: VecDeque::with_capacity(resolved.medium_delay + 2),
485        }
486    }
487
488    #[inline(always)]
489    fn update(&mut self, source: f64, high: f64, low: f64, close: f64) -> (f64, f64) {
490        if !(source.is_finite() && high.is_finite() && low.is_finite() && close.is_finite()) {
491            return (f64::NAN, f64::NAN);
492        }
493
494        let short_ma = self.short_rma.update(source);
495        let medium_ma = self.medium_rma.update(source);
496        let medium_atr = self.medium_atr.update(high, low, close);
497
498        self.short_history.push_back(short_ma);
499        if self.short_history.len() > self.short_delay + 1 {
500            self.short_history.pop_front();
501        }
502        self.medium_history.push_back(medium_ma);
503        if self.medium_history.len() > self.medium_delay + 1 {
504            self.medium_history.pop_front();
505        }
506
507        let short_center = delayed_or_source(&self.short_history, self.short_delay, source);
508        let medium_center = delayed_or_source(&self.medium_history, self.medium_delay, source);
509        let offset = self.medium_multiplier * medium_atr;
510        let denom = 2.0 * offset;
511        if !denom.is_finite() || denom == 0.0 {
512            return (f64::NAN, f64::NAN);
513        }
514
515        let medium_bottom = medium_center - offset;
516        (
517            (source - medium_bottom) / denom,
518            (short_center - medium_bottom) / denom,
519        )
520    }
521}
522
523#[inline(always)]
524fn validate_input<'a>(
525    input: &'a CycleChannelOscillatorInput<'a>,
526    kernel: Kernel,
527) -> Result<
528    (
529        &'a [f64],
530        &'a [f64],
531        &'a [f64],
532        &'a [f64],
533        ResolvedParams,
534        usize,
535        Kernel,
536    ),
537    CycleChannelOscillatorError,
538> {
539    let (source, high, low, close) = extract_slices(input)?;
540    let resolved = resolve_params(&input.params)?;
541    let first = first_valid_quad(source, high, low, close)
542        .ok_or(CycleChannelOscillatorError::AllValuesNaN)?;
543    let valid = source.len().saturating_sub(first);
544    if valid < resolved.medium_period {
545        return Err(CycleChannelOscillatorError::NotEnoughValidData {
546            needed: resolved.medium_period,
547            valid,
548        });
549    }
550    Ok((
551        source,
552        high,
553        low,
554        close,
555        resolved,
556        first,
557        kernel.to_non_batch(),
558    ))
559}
560
561#[inline(always)]
562fn compute_cycle_channel_oscillator_into(
563    source: &[f64],
564    high: &[f64],
565    low: &[f64],
566    close: &[f64],
567    resolved: ResolvedParams,
568    out_fast: &mut [f64],
569    out_slow: &mut [f64],
570) -> Result<(), CycleChannelOscillatorError> {
571    let n = source.len();
572    if out_fast.len() != n || out_slow.len() != n {
573        return Err(CycleChannelOscillatorError::OutputLengthMismatch {
574            expected: n,
575            got: out_fast.len().max(out_slow.len()),
576        });
577    }
578    let mut core = CycleChannelOscillatorCore::new(resolved);
579    for i in 0..n {
580        let (fast, slow) = core.update(source[i], high[i], low[i], close[i]);
581        out_fast[i] = fast;
582        out_slow[i] = slow;
583    }
584    Ok(())
585}
586
587#[inline]
588pub fn cycle_channel_oscillator(
589    input: &CycleChannelOscillatorInput,
590) -> Result<CycleChannelOscillatorOutput, CycleChannelOscillatorError> {
591    cycle_channel_oscillator_with_kernel(input, Kernel::Auto)
592}
593
594#[inline]
595pub fn cycle_channel_oscillator_with_kernel(
596    input: &CycleChannelOscillatorInput,
597    kernel: Kernel,
598) -> Result<CycleChannelOscillatorOutput, CycleChannelOscillatorError> {
599    let (source, high, low, close, resolved, first, chosen) = validate_input(input, kernel)?;
600    let _ = chosen;
601    let warm = first + resolved.medium_period - 1;
602    let mut fast = alloc_with_nan_prefix(source.len(), warm.min(source.len()));
603    let mut slow = alloc_with_nan_prefix(source.len(), warm.min(source.len()));
604    compute_cycle_channel_oscillator_into(
605        source, high, low, close, resolved, &mut fast, &mut slow,
606    )?;
607    Ok(CycleChannelOscillatorOutput { fast, slow })
608}
609
610#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
611#[inline]
612pub fn cycle_channel_oscillator_into(
613    out_fast: &mut [f64],
614    out_slow: &mut [f64],
615    input: &CycleChannelOscillatorInput,
616    kernel: Kernel,
617) -> Result<(), CycleChannelOscillatorError> {
618    cycle_channel_oscillator_into_slice(out_fast, out_slow, input, kernel)
619}
620
621#[inline]
622pub fn cycle_channel_oscillator_into_slice(
623    out_fast: &mut [f64],
624    out_slow: &mut [f64],
625    input: &CycleChannelOscillatorInput,
626    kernel: Kernel,
627) -> Result<(), CycleChannelOscillatorError> {
628    let (source, high, low, close, resolved, _first, chosen) = validate_input(input, kernel)?;
629    let _ = chosen;
630    out_fast.fill(f64::NAN);
631    out_slow.fill(f64::NAN);
632    compute_cycle_channel_oscillator_into(source, high, low, close, resolved, out_fast, out_slow)
633}
634
635#[derive(Debug, Clone)]
636pub struct CycleChannelOscillatorStream {
637    core: CycleChannelOscillatorCore,
638}
639
640impl CycleChannelOscillatorStream {
641    pub fn try_new(
642        params: CycleChannelOscillatorParams,
643    ) -> Result<Self, CycleChannelOscillatorError> {
644        Ok(Self {
645            core: CycleChannelOscillatorCore::new(resolve_params(&params)?),
646        })
647    }
648
649    #[inline(always)]
650    pub fn update(&mut self, source: f64, high: f64, low: f64, close: f64) -> (f64, f64) {
651        self.core.update(source, high, low, close)
652    }
653}
654
655#[derive(Debug, Clone)]
656pub struct CycleChannelOscillatorBatchRange {
657    pub short_cycle_length: (usize, usize, usize),
658    pub medium_cycle_length: (usize, usize, usize),
659    pub short_multiplier: (f64, f64, f64),
660    pub medium_multiplier: (f64, f64, f64),
661}
662
663impl Default for CycleChannelOscillatorBatchRange {
664    fn default() -> Self {
665        Self {
666            short_cycle_length: (DEFAULT_SHORT_CYCLE_LENGTH, DEFAULT_SHORT_CYCLE_LENGTH, 0),
667            medium_cycle_length: (DEFAULT_MEDIUM_CYCLE_LENGTH, DEFAULT_MEDIUM_CYCLE_LENGTH, 0),
668            short_multiplier: (DEFAULT_SHORT_MULTIPLIER, DEFAULT_SHORT_MULTIPLIER, 0.0),
669            medium_multiplier: (DEFAULT_MEDIUM_MULTIPLIER, DEFAULT_MEDIUM_MULTIPLIER, 0.0),
670        }
671    }
672}
673
674#[derive(Debug, Clone)]
675pub struct CycleChannelOscillatorBatchOutput {
676    pub fast: Vec<f64>,
677    pub slow: Vec<f64>,
678    pub combos: Vec<CycleChannelOscillatorParams>,
679    pub rows: usize,
680    pub cols: usize,
681}
682
683#[derive(Clone, Debug)]
684pub struct CycleChannelOscillatorBatchBuilder {
685    source: Option<&'static str>,
686    range: CycleChannelOscillatorBatchRange,
687    kernel: Kernel,
688}
689
690impl Default for CycleChannelOscillatorBatchBuilder {
691    fn default() -> Self {
692        Self {
693            source: None,
694            range: CycleChannelOscillatorBatchRange::default(),
695            kernel: Kernel::Auto,
696        }
697    }
698}
699
700impl CycleChannelOscillatorBatchBuilder {
701    #[inline(always)]
702    pub fn new() -> Self {
703        Self::default()
704    }
705
706    #[inline(always)]
707    pub fn source(mut self, value: &'static str) -> Self {
708        self.source = Some(value);
709        self
710    }
711
712    #[inline(always)]
713    pub fn short_cycle_length_range(mut self, value: (usize, usize, usize)) -> Self {
714        self.range.short_cycle_length = value;
715        self
716    }
717
718    #[inline(always)]
719    pub fn medium_cycle_length_range(mut self, value: (usize, usize, usize)) -> Self {
720        self.range.medium_cycle_length = value;
721        self
722    }
723
724    #[inline(always)]
725    pub fn short_multiplier_range(mut self, value: (f64, f64, f64)) -> Self {
726        self.range.short_multiplier = value;
727        self
728    }
729
730    #[inline(always)]
731    pub fn medium_multiplier_range(mut self, value: (f64, f64, f64)) -> Self {
732        self.range.medium_multiplier = value;
733        self
734    }
735
736    #[inline(always)]
737    pub fn kernel(mut self, value: Kernel) -> Self {
738        self.kernel = value;
739        self
740    }
741
742    #[inline(always)]
743    pub fn apply(
744        self,
745        candles: &Candles,
746    ) -> Result<CycleChannelOscillatorBatchOutput, CycleChannelOscillatorError> {
747        cycle_channel_oscillator_batch_with_kernel(
748            source_type(candles, self.source.unwrap_or(DEFAULT_SOURCE)),
749            candles.high.as_slice(),
750            candles.low.as_slice(),
751            candles.close.as_slice(),
752            &self.range,
753            self.kernel,
754        )
755    }
756
757    #[inline(always)]
758    pub fn apply_slices(
759        self,
760        source: &[f64],
761        high: &[f64],
762        low: &[f64],
763        close: &[f64],
764    ) -> Result<CycleChannelOscillatorBatchOutput, CycleChannelOscillatorError> {
765        cycle_channel_oscillator_batch_with_kernel(
766            source,
767            high,
768            low,
769            close,
770            &self.range,
771            self.kernel,
772        )
773    }
774}
775
776#[inline(always)]
777fn expand_usize_range(
778    start: usize,
779    end: usize,
780    step: usize,
781) -> Result<Vec<usize>, CycleChannelOscillatorError> {
782    if start == 0 {
783        return Err(CycleChannelOscillatorError::InvalidRange {
784            start: start.to_string(),
785            end: end.to_string(),
786            step: step.to_string(),
787        });
788    }
789    let mut values = Vec::new();
790    if step == 0 {
791        if start != end {
792            return Err(CycleChannelOscillatorError::InvalidRange {
793                start: start.to_string(),
794                end: end.to_string(),
795                step: step.to_string(),
796            });
797        }
798        values.push(start);
799    } else {
800        if start > end {
801            return Err(CycleChannelOscillatorError::InvalidRange {
802                start: start.to_string(),
803                end: end.to_string(),
804                step: step.to_string(),
805            });
806        }
807        let mut current = start;
808        while current <= end {
809            values.push(current);
810            current = match current.checked_add(step) {
811                Some(next) => next,
812                None => break,
813            };
814        }
815    }
816    Ok(values)
817}
818
819#[inline(always)]
820fn expand_f64_range(
821    start: f64,
822    end: f64,
823    step: f64,
824) -> Result<Vec<f64>, CycleChannelOscillatorError> {
825    if !start.is_finite()
826        || !end.is_finite()
827        || !step.is_finite()
828        || start < 0.0
829        || end < 0.0
830        || step < 0.0
831    {
832        return Err(CycleChannelOscillatorError::InvalidRange {
833            start: start.to_string(),
834            end: end.to_string(),
835            step: step.to_string(),
836        });
837    }
838    let mut values = Vec::new();
839    if step == 0.0 {
840        if (start - end).abs() > 1e-12 {
841            return Err(CycleChannelOscillatorError::InvalidRange {
842                start: start.to_string(),
843                end: end.to_string(),
844                step: step.to_string(),
845            });
846        }
847        values.push(start);
848    } else {
849        if start > end {
850            return Err(CycleChannelOscillatorError::InvalidRange {
851                start: start.to_string(),
852                end: end.to_string(),
853                step: step.to_string(),
854            });
855        }
856        let mut current = start;
857        while current <= end + 1e-12 {
858            values.push(current);
859            current += step;
860        }
861    }
862    Ok(values)
863}
864
865pub fn expand_grid(
866    sweep: &CycleChannelOscillatorBatchRange,
867) -> Result<Vec<CycleChannelOscillatorParams>, CycleChannelOscillatorError> {
868    let short_lengths = expand_usize_range(
869        sweep.short_cycle_length.0,
870        sweep.short_cycle_length.1,
871        sweep.short_cycle_length.2,
872    )?;
873    let medium_lengths = expand_usize_range(
874        sweep.medium_cycle_length.0,
875        sweep.medium_cycle_length.1,
876        sweep.medium_cycle_length.2,
877    )?;
878    let short_multipliers = expand_f64_range(
879        sweep.short_multiplier.0,
880        sweep.short_multiplier.1,
881        sweep.short_multiplier.2,
882    )?;
883    let medium_multipliers = expand_f64_range(
884        sweep.medium_multiplier.0,
885        sweep.medium_multiplier.1,
886        sweep.medium_multiplier.2,
887    )?;
888
889    let mut combos = Vec::new();
890    for short_cycle_length in short_lengths.iter().copied() {
891        for medium_cycle_length in medium_lengths.iter().copied() {
892            for short_multiplier in short_multipliers.iter().copied() {
893                for medium_multiplier in medium_multipliers.iter().copied() {
894                    combos.push(CycleChannelOscillatorParams {
895                        short_cycle_length: Some(short_cycle_length),
896                        medium_cycle_length: Some(medium_cycle_length),
897                        short_multiplier: Some(short_multiplier),
898                        medium_multiplier: Some(medium_multiplier),
899                    });
900                }
901            }
902        }
903    }
904    Ok(combos)
905}
906
907#[inline(always)]
908fn validate_raw_slices(
909    source: &[f64],
910    high: &[f64],
911    low: &[f64],
912    close: &[f64],
913) -> Result<usize, CycleChannelOscillatorError> {
914    if source.is_empty() || high.is_empty() || low.is_empty() || close.is_empty() {
915        return Err(CycleChannelOscillatorError::EmptyInputData);
916    }
917    if source.len() != high.len() || source.len() != low.len() || source.len() != close.len() {
918        return Err(CycleChannelOscillatorError::InconsistentSliceLengths {
919            source_len: source.len(),
920            high_len: high.len(),
921            low_len: low.len(),
922            close_len: close.len(),
923        });
924    }
925    first_valid_quad(source, high, low, close).ok_or(CycleChannelOscillatorError::AllValuesNaN)
926}
927
928pub fn cycle_channel_oscillator_batch_with_kernel(
929    source: &[f64],
930    high: &[f64],
931    low: &[f64],
932    close: &[f64],
933    sweep: &CycleChannelOscillatorBatchRange,
934    kernel: Kernel,
935) -> Result<CycleChannelOscillatorBatchOutput, CycleChannelOscillatorError> {
936    let batch_kernel = match kernel {
937        Kernel::Auto => detect_best_batch_kernel(),
938        other if other.is_batch() => other,
939        _ => return Err(CycleChannelOscillatorError::InvalidKernelForBatch(kernel)),
940    };
941    cycle_channel_oscillator_batch_par_slice(
942        source,
943        high,
944        low,
945        close,
946        sweep,
947        batch_kernel.to_non_batch(),
948    )
949}
950
951#[inline(always)]
952pub fn cycle_channel_oscillator_batch_slice(
953    source: &[f64],
954    high: &[f64],
955    low: &[f64],
956    close: &[f64],
957    sweep: &CycleChannelOscillatorBatchRange,
958    kernel: Kernel,
959) -> Result<CycleChannelOscillatorBatchOutput, CycleChannelOscillatorError> {
960    cycle_channel_oscillator_batch_inner(source, high, low, close, sweep, kernel, false)
961}
962
963#[inline(always)]
964pub fn cycle_channel_oscillator_batch_par_slice(
965    source: &[f64],
966    high: &[f64],
967    low: &[f64],
968    close: &[f64],
969    sweep: &CycleChannelOscillatorBatchRange,
970    kernel: Kernel,
971) -> Result<CycleChannelOscillatorBatchOutput, CycleChannelOscillatorError> {
972    cycle_channel_oscillator_batch_inner(source, high, low, close, sweep, kernel, true)
973}
974
975fn cycle_channel_oscillator_batch_inner(
976    source: &[f64],
977    high: &[f64],
978    low: &[f64],
979    close: &[f64],
980    sweep: &CycleChannelOscillatorBatchRange,
981    kernel: Kernel,
982    parallel: bool,
983) -> Result<CycleChannelOscillatorBatchOutput, CycleChannelOscillatorError> {
984    let combos = expand_grid(sweep)?;
985    let first = validate_raw_slices(source, high, low, close)?;
986    let resolveds = combos
987        .iter()
988        .map(resolve_params)
989        .collect::<Result<Vec<_>, _>>()?;
990    let max_needed = resolveds
991        .iter()
992        .map(|resolved| resolved.medium_period)
993        .max()
994        .unwrap_or(DEFAULT_MEDIUM_CYCLE_LENGTH / 2);
995    let valid = source.len().saturating_sub(first);
996    if valid < max_needed {
997        return Err(CycleChannelOscillatorError::NotEnoughValidData {
998            needed: max_needed,
999            valid,
1000        });
1001    }
1002
1003    let rows = combos.len();
1004    let cols = source.len();
1005    let warmups: Vec<usize> = resolveds
1006        .iter()
1007        .map(|resolved| (first + resolved.medium_period - 1).min(cols))
1008        .collect();
1009
1010    let mut fast_buf = make_uninit_matrix(rows, cols);
1011    init_matrix_prefixes(&mut fast_buf, cols, &warmups);
1012    let mut fast_guard = ManuallyDrop::new(fast_buf);
1013    let out_fast: &mut [f64] = unsafe {
1014        core::slice::from_raw_parts_mut(fast_guard.as_mut_ptr() as *mut f64, fast_guard.len())
1015    };
1016
1017    let mut slow_buf = make_uninit_matrix(rows, cols);
1018    init_matrix_prefixes(&mut slow_buf, cols, &warmups);
1019    let mut slow_guard = ManuallyDrop::new(slow_buf);
1020    let out_slow: &mut [f64] = unsafe {
1021        core::slice::from_raw_parts_mut(slow_guard.as_mut_ptr() as *mut f64, slow_guard.len())
1022    };
1023
1024    cycle_channel_oscillator_batch_inner_into(
1025        source, high, low, close, sweep, kernel, parallel, out_fast, out_slow,
1026    )?;
1027
1028    let fast = unsafe {
1029        Vec::from_raw_parts(
1030            fast_guard.as_mut_ptr() as *mut f64,
1031            fast_guard.len(),
1032            fast_guard.capacity(),
1033        )
1034    };
1035    let slow = unsafe {
1036        Vec::from_raw_parts(
1037            slow_guard.as_mut_ptr() as *mut f64,
1038            slow_guard.len(),
1039            slow_guard.capacity(),
1040        )
1041    };
1042
1043    Ok(CycleChannelOscillatorBatchOutput {
1044        fast,
1045        slow,
1046        combos,
1047        rows,
1048        cols,
1049    })
1050}
1051
1052pub fn cycle_channel_oscillator_batch_into_slice(
1053    out_fast: &mut [f64],
1054    out_slow: &mut [f64],
1055    source: &[f64],
1056    high: &[f64],
1057    low: &[f64],
1058    close: &[f64],
1059    sweep: &CycleChannelOscillatorBatchRange,
1060    kernel: Kernel,
1061) -> Result<(), CycleChannelOscillatorError> {
1062    cycle_channel_oscillator_batch_inner_into(
1063        source, high, low, close, sweep, kernel, false, out_fast, out_slow,
1064    )?;
1065    Ok(())
1066}
1067
1068fn cycle_channel_oscillator_batch_inner_into(
1069    source: &[f64],
1070    high: &[f64],
1071    low: &[f64],
1072    close: &[f64],
1073    sweep: &CycleChannelOscillatorBatchRange,
1074    _kernel: Kernel,
1075    parallel: bool,
1076    out_fast: &mut [f64],
1077    out_slow: &mut [f64],
1078) -> Result<Vec<CycleChannelOscillatorParams>, CycleChannelOscillatorError> {
1079    let combos = expand_grid(sweep)?;
1080    let first = validate_raw_slices(source, high, low, close)?;
1081    let resolveds = combos
1082        .iter()
1083        .map(resolve_params)
1084        .collect::<Result<Vec<_>, _>>()?;
1085    let rows = combos.len();
1086    let cols = source.len();
1087    let expected =
1088        rows.checked_mul(cols)
1089            .ok_or_else(|| CycleChannelOscillatorError::InvalidRange {
1090                start: rows.to_string(),
1091                end: cols.to_string(),
1092                step: "rows*cols".to_string(),
1093            })?;
1094    if out_fast.len() != expected || out_slow.len() != expected {
1095        return Err(CycleChannelOscillatorError::OutputLengthMismatch {
1096            expected,
1097            got: out_fast.len().max(out_slow.len()),
1098        });
1099    }
1100    let max_needed = resolveds
1101        .iter()
1102        .map(|resolved| resolved.medium_period)
1103        .max()
1104        .unwrap_or(DEFAULT_MEDIUM_CYCLE_LENGTH / 2);
1105    let valid = cols.saturating_sub(first);
1106    if valid < max_needed {
1107        return Err(CycleChannelOscillatorError::NotEnoughValidData {
1108            needed: max_needed,
1109            valid,
1110        });
1111    }
1112
1113    let do_row = |row: usize, dst_fast: &mut [f64], dst_slow: &mut [f64]| {
1114        let resolved = resolveds[row];
1115        let warm = (first + resolved.medium_period - 1).min(cols);
1116        dst_fast[..warm].fill(f64::NAN);
1117        dst_slow[..warm].fill(f64::NAN);
1118        compute_cycle_channel_oscillator_into(
1119            source, high, low, close, resolved, dst_fast, dst_slow,
1120        )
1121        .unwrap();
1122    };
1123
1124    if parallel {
1125        #[cfg(not(target_arch = "wasm32"))]
1126        {
1127            out_fast
1128                .par_chunks_mut(cols)
1129                .zip(out_slow.par_chunks_mut(cols))
1130                .enumerate()
1131                .for_each(|(row, (dst_fast, dst_slow))| do_row(row, dst_fast, dst_slow));
1132        }
1133        #[cfg(target_arch = "wasm32")]
1134        {
1135            for ((row, dst_fast), dst_slow) in out_fast
1136                .chunks_mut(cols)
1137                .enumerate()
1138                .zip(out_slow.chunks_mut(cols))
1139            {
1140                do_row(row, dst_fast, dst_slow);
1141            }
1142        }
1143    } else {
1144        for ((row, dst_fast), dst_slow) in out_fast
1145            .chunks_mut(cols)
1146            .enumerate()
1147            .zip(out_slow.chunks_mut(cols))
1148        {
1149            do_row(row, dst_fast, dst_slow);
1150        }
1151    }
1152
1153    Ok(combos)
1154}
1155
1156#[cfg(feature = "python")]
1157#[pyfunction(name = "cycle_channel_oscillator")]
1158#[pyo3(signature = (source, high, low, close, short_cycle_length=10, medium_cycle_length=30, short_multiplier=1.0, medium_multiplier=3.0, kernel=None))]
1159pub fn cycle_channel_oscillator_py<'py>(
1160    py: Python<'py>,
1161    source: PyReadonlyArray1<'py, f64>,
1162    high: PyReadonlyArray1<'py, f64>,
1163    low: PyReadonlyArray1<'py, f64>,
1164    close: PyReadonlyArray1<'py, f64>,
1165    short_cycle_length: usize,
1166    medium_cycle_length: usize,
1167    short_multiplier: f64,
1168    medium_multiplier: f64,
1169    kernel: Option<&str>,
1170) -> PyResult<Bound<'py, PyDict>> {
1171    let source = source.as_slice()?;
1172    let high = high.as_slice()?;
1173    let low = low.as_slice()?;
1174    let close = close.as_slice()?;
1175    let input = CycleChannelOscillatorInput::from_slices(
1176        source,
1177        high,
1178        low,
1179        close,
1180        CycleChannelOscillatorParams {
1181            short_cycle_length: Some(short_cycle_length),
1182            medium_cycle_length: Some(medium_cycle_length),
1183            short_multiplier: Some(short_multiplier),
1184            medium_multiplier: Some(medium_multiplier),
1185        },
1186    );
1187    let kernel = validate_kernel(kernel, false)?;
1188    let out = py
1189        .allow_threads(|| cycle_channel_oscillator_with_kernel(&input, kernel))
1190        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1191    let dict = PyDict::new(py);
1192    dict.set_item("fast", out.fast.into_pyarray(py))?;
1193    dict.set_item("slow", out.slow.into_pyarray(py))?;
1194    Ok(dict)
1195}
1196
1197#[cfg(feature = "python")]
1198#[pyclass(name = "CycleChannelOscillatorStream")]
1199pub struct CycleChannelOscillatorStreamPy {
1200    stream: CycleChannelOscillatorStream,
1201}
1202
1203#[cfg(feature = "python")]
1204#[pymethods]
1205impl CycleChannelOscillatorStreamPy {
1206    #[new]
1207    #[pyo3(signature = (short_cycle_length=10, medium_cycle_length=30, short_multiplier=1.0, medium_multiplier=3.0))]
1208    fn new(
1209        short_cycle_length: usize,
1210        medium_cycle_length: usize,
1211        short_multiplier: f64,
1212        medium_multiplier: f64,
1213    ) -> PyResult<Self> {
1214        let stream = CycleChannelOscillatorStream::try_new(CycleChannelOscillatorParams {
1215            short_cycle_length: Some(short_cycle_length),
1216            medium_cycle_length: Some(medium_cycle_length),
1217            short_multiplier: Some(short_multiplier),
1218            medium_multiplier: Some(medium_multiplier),
1219        })
1220        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1221        Ok(Self { stream })
1222    }
1223
1224    fn update(&mut self, source: f64, high: f64, low: f64, close: f64) -> (f64, f64) {
1225        self.stream.update(source, high, low, close)
1226    }
1227}
1228
1229#[cfg(feature = "python")]
1230#[pyfunction(name = "cycle_channel_oscillator_batch")]
1231#[pyo3(signature = (source, high, low, close, short_cycle_length_range=(10,10,0), medium_cycle_length_range=(30,30,0), short_multiplier_range=(1.0,1.0,0.0), medium_multiplier_range=(3.0,3.0,0.0), kernel=None))]
1232pub fn cycle_channel_oscillator_batch_py<'py>(
1233    py: Python<'py>,
1234    source: PyReadonlyArray1<'py, f64>,
1235    high: PyReadonlyArray1<'py, f64>,
1236    low: PyReadonlyArray1<'py, f64>,
1237    close: PyReadonlyArray1<'py, f64>,
1238    short_cycle_length_range: (usize, usize, usize),
1239    medium_cycle_length_range: (usize, usize, usize),
1240    short_multiplier_range: (f64, f64, f64),
1241    medium_multiplier_range: (f64, f64, f64),
1242    kernel: Option<&str>,
1243) -> PyResult<Bound<'py, PyDict>> {
1244    let source = source.as_slice()?;
1245    let high = high.as_slice()?;
1246    let low = low.as_slice()?;
1247    let close = close.as_slice()?;
1248    let sweep = CycleChannelOscillatorBatchRange {
1249        short_cycle_length: short_cycle_length_range,
1250        medium_cycle_length: medium_cycle_length_range,
1251        short_multiplier: short_multiplier_range,
1252        medium_multiplier: medium_multiplier_range,
1253    };
1254    let combos = expand_grid(&sweep).map_err(|e| PyValueError::new_err(e.to_string()))?;
1255    let rows = combos.len();
1256    let cols = source.len();
1257    let total = rows
1258        .checked_mul(cols)
1259        .ok_or_else(|| PyValueError::new_err("rows*cols overflow"))?;
1260
1261    let out_fast = unsafe { PyArray1::<f64>::new(py, [total], false) };
1262    let out_slow = unsafe { PyArray1::<f64>::new(py, [total], false) };
1263    let fast_slice = unsafe { out_fast.as_slice_mut()? };
1264    let slow_slice = unsafe { out_slow.as_slice_mut()? };
1265    let kernel = validate_kernel(kernel, true)?;
1266
1267    py.allow_threads(|| {
1268        let batch_kernel = match kernel {
1269            Kernel::Auto => detect_best_batch_kernel(),
1270            other => other,
1271        };
1272        cycle_channel_oscillator_batch_inner_into(
1273            source,
1274            high,
1275            low,
1276            close,
1277            &sweep,
1278            batch_kernel.to_non_batch(),
1279            true,
1280            fast_slice,
1281            slow_slice,
1282        )
1283    })
1284    .map_err(|e| PyValueError::new_err(e.to_string()))?;
1285
1286    let dict = PyDict::new(py);
1287    dict.set_item("fast", out_fast.reshape((rows, cols))?)?;
1288    dict.set_item("slow", out_slow.reshape((rows, cols))?)?;
1289    dict.set_item(
1290        "short_cycle_lengths",
1291        combos
1292            .iter()
1293            .map(|combo| {
1294                combo
1295                    .short_cycle_length
1296                    .unwrap_or(DEFAULT_SHORT_CYCLE_LENGTH) as u64
1297            })
1298            .collect::<Vec<_>>()
1299            .into_pyarray(py),
1300    )?;
1301    dict.set_item(
1302        "medium_cycle_lengths",
1303        combos
1304            .iter()
1305            .map(|combo| {
1306                combo
1307                    .medium_cycle_length
1308                    .unwrap_or(DEFAULT_MEDIUM_CYCLE_LENGTH) as u64
1309            })
1310            .collect::<Vec<_>>()
1311            .into_pyarray(py),
1312    )?;
1313    dict.set_item(
1314        "short_multipliers",
1315        combos
1316            .iter()
1317            .map(|combo| combo.short_multiplier.unwrap_or(DEFAULT_SHORT_MULTIPLIER))
1318            .collect::<Vec<_>>()
1319            .into_pyarray(py),
1320    )?;
1321    dict.set_item(
1322        "medium_multipliers",
1323        combos
1324            .iter()
1325            .map(|combo| combo.medium_multiplier.unwrap_or(DEFAULT_MEDIUM_MULTIPLIER))
1326            .collect::<Vec<_>>()
1327            .into_pyarray(py),
1328    )?;
1329    dict.set_item("rows", rows)?;
1330    dict.set_item("cols", cols)?;
1331    Ok(dict)
1332}
1333
1334#[cfg(feature = "python")]
1335pub fn register_cycle_channel_oscillator_module(
1336    m: &Bound<'_, pyo3::types::PyModule>,
1337) -> PyResult<()> {
1338    m.add_function(wrap_pyfunction!(cycle_channel_oscillator_py, m)?)?;
1339    m.add_function(wrap_pyfunction!(cycle_channel_oscillator_batch_py, m)?)?;
1340    m.add_class::<CycleChannelOscillatorStreamPy>()?;
1341    Ok(())
1342}
1343
1344#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1345#[derive(Serialize, Deserialize)]
1346pub struct CycleChannelOscillatorJsOutput {
1347    pub fast: Vec<f64>,
1348    pub slow: Vec<f64>,
1349}
1350
1351#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1352#[wasm_bindgen(js_name = "cycle_channel_oscillator_js")]
1353pub fn cycle_channel_oscillator_js(
1354    source: &[f64],
1355    high: &[f64],
1356    low: &[f64],
1357    close: &[f64],
1358    short_cycle_length: usize,
1359    medium_cycle_length: usize,
1360    short_multiplier: f64,
1361    medium_multiplier: f64,
1362) -> Result<JsValue, JsValue> {
1363    let input = CycleChannelOscillatorInput::from_slices(
1364        source,
1365        high,
1366        low,
1367        close,
1368        CycleChannelOscillatorParams {
1369            short_cycle_length: Some(short_cycle_length),
1370            medium_cycle_length: Some(medium_cycle_length),
1371            short_multiplier: Some(short_multiplier),
1372            medium_multiplier: Some(medium_multiplier),
1373        },
1374    );
1375    let out = cycle_channel_oscillator_with_kernel(&input, Kernel::Auto)
1376        .map_err(|e| JsValue::from_str(&e.to_string()))?;
1377    serde_wasm_bindgen::to_value(&CycleChannelOscillatorJsOutput {
1378        fast: out.fast,
1379        slow: out.slow,
1380    })
1381    .map_err(|e| JsValue::from_str(&e.to_string()))
1382}
1383
1384#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1385#[derive(Serialize, Deserialize)]
1386pub struct CycleChannelOscillatorBatchConfig {
1387    pub short_cycle_length_range: Vec<f64>,
1388    pub medium_cycle_length_range: Vec<f64>,
1389    pub short_multiplier_range: Vec<f64>,
1390    pub medium_multiplier_range: Vec<f64>,
1391}
1392
1393#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1394#[derive(Serialize, Deserialize)]
1395pub struct CycleChannelOscillatorBatchJsOutput {
1396    pub fast: Vec<f64>,
1397    pub slow: Vec<f64>,
1398    pub short_cycle_lengths: Vec<usize>,
1399    pub medium_cycle_lengths: Vec<usize>,
1400    pub short_multipliers: Vec<f64>,
1401    pub medium_multipliers: Vec<f64>,
1402    pub rows: usize,
1403    pub cols: usize,
1404}
1405
1406#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1407fn js_vec3_to_usize(name: &str, values: &[f64]) -> Result<(usize, usize, usize), JsValue> {
1408    if values.len() != 3 {
1409        return Err(JsValue::from_str(&format!(
1410            "Invalid config: {name} must have exactly 3 elements [start, end, step]"
1411        )));
1412    }
1413    let mut out = [0usize; 3];
1414    for (i, value) in values.iter().copied().enumerate() {
1415        if !value.is_finite() || value < 0.0 {
1416            return Err(JsValue::from_str(&format!(
1417                "Invalid config: {name}[{i}] must be a finite non-negative whole number"
1418            )));
1419        }
1420        let rounded = value.round();
1421        if (value - rounded).abs() > 1e-9 {
1422            return Err(JsValue::from_str(&format!(
1423                "Invalid config: {name}[{i}] must be a whole number"
1424            )));
1425        }
1426        out[i] = rounded as usize;
1427    }
1428    Ok((out[0], out[1], out[2]))
1429}
1430
1431#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1432fn js_vec3_to_f64(name: &str, values: &[f64]) -> Result<(f64, f64, f64), JsValue> {
1433    if values.len() != 3 {
1434        return Err(JsValue::from_str(&format!(
1435            "Invalid config: {name} must have exactly 3 elements [start, end, step]"
1436        )));
1437    }
1438    if values
1439        .iter()
1440        .any(|value| !value.is_finite() || *value < 0.0)
1441    {
1442        return Err(JsValue::from_str(&format!(
1443            "Invalid config: {name} values must be finite non-negative numbers"
1444        )));
1445    }
1446    Ok((values[0], values[1], values[2]))
1447}
1448
1449#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1450#[wasm_bindgen(js_name = "cycle_channel_oscillator_batch_js")]
1451pub fn cycle_channel_oscillator_batch_js(
1452    source: &[f64],
1453    high: &[f64],
1454    low: &[f64],
1455    close: &[f64],
1456    config: JsValue,
1457) -> Result<JsValue, JsValue> {
1458    let config: CycleChannelOscillatorBatchConfig = serde_wasm_bindgen::from_value(config)
1459        .map_err(|e| JsValue::from_str(&format!("Invalid config: {e}")))?;
1460    let sweep = CycleChannelOscillatorBatchRange {
1461        short_cycle_length: js_vec3_to_usize(
1462            "short_cycle_length_range",
1463            &config.short_cycle_length_range,
1464        )?,
1465        medium_cycle_length: js_vec3_to_usize(
1466            "medium_cycle_length_range",
1467            &config.medium_cycle_length_range,
1468        )?,
1469        short_multiplier: js_vec3_to_f64("short_multiplier_range", &config.short_multiplier_range)?,
1470        medium_multiplier: js_vec3_to_f64(
1471            "medium_multiplier_range",
1472            &config.medium_multiplier_range,
1473        )?,
1474    };
1475    let out =
1476        cycle_channel_oscillator_batch_with_kernel(source, high, low, close, &sweep, Kernel::Auto)
1477            .map_err(|e| JsValue::from_str(&e.to_string()))?;
1478    serde_wasm_bindgen::to_value(&CycleChannelOscillatorBatchJsOutput {
1479        short_cycle_lengths: out
1480            .combos
1481            .iter()
1482            .map(|combo| {
1483                combo
1484                    .short_cycle_length
1485                    .unwrap_or(DEFAULT_SHORT_CYCLE_LENGTH)
1486            })
1487            .collect(),
1488        medium_cycle_lengths: out
1489            .combos
1490            .iter()
1491            .map(|combo| {
1492                combo
1493                    .medium_cycle_length
1494                    .unwrap_or(DEFAULT_MEDIUM_CYCLE_LENGTH)
1495            })
1496            .collect(),
1497        short_multipliers: out
1498            .combos
1499            .iter()
1500            .map(|combo| combo.short_multiplier.unwrap_or(DEFAULT_SHORT_MULTIPLIER))
1501            .collect(),
1502        medium_multipliers: out
1503            .combos
1504            .iter()
1505            .map(|combo| combo.medium_multiplier.unwrap_or(DEFAULT_MEDIUM_MULTIPLIER))
1506            .collect(),
1507        fast: out.fast,
1508        slow: out.slow,
1509        rows: out.rows,
1510        cols: out.cols,
1511    })
1512    .map_err(|e| JsValue::from_str(&format!("Serialization error: {e}")))
1513}
1514
1515#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1516#[wasm_bindgen]
1517pub fn cycle_channel_oscillator_alloc(len: usize) -> *mut f64 {
1518    let mut vec = Vec::<f64>::with_capacity(len);
1519    let ptr = vec.as_mut_ptr();
1520    std::mem::forget(vec);
1521    ptr
1522}
1523
1524#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1525#[wasm_bindgen]
1526pub fn cycle_channel_oscillator_free(ptr: *mut f64, len: usize) {
1527    if !ptr.is_null() {
1528        unsafe {
1529            let _ = Vec::from_raw_parts(ptr, len, len);
1530        }
1531    }
1532}
1533
1534#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1535#[wasm_bindgen]
1536pub fn cycle_channel_oscillator_into(
1537    source_ptr: *const f64,
1538    high_ptr: *const f64,
1539    low_ptr: *const f64,
1540    close_ptr: *const f64,
1541    out_fast_ptr: *mut f64,
1542    out_slow_ptr: *mut f64,
1543    len: usize,
1544    short_cycle_length: usize,
1545    medium_cycle_length: usize,
1546    short_multiplier: f64,
1547    medium_multiplier: f64,
1548) -> Result<(), JsValue> {
1549    if source_ptr.is_null()
1550        || high_ptr.is_null()
1551        || low_ptr.is_null()
1552        || close_ptr.is_null()
1553        || out_fast_ptr.is_null()
1554        || out_slow_ptr.is_null()
1555    {
1556        return Err(JsValue::from_str(
1557            "null pointer passed to cycle_channel_oscillator_into",
1558        ));
1559    }
1560    unsafe {
1561        let source = std::slice::from_raw_parts(source_ptr, len);
1562        let high = std::slice::from_raw_parts(high_ptr, len);
1563        let low = std::slice::from_raw_parts(low_ptr, len);
1564        let close = std::slice::from_raw_parts(close_ptr, len);
1565        let out_fast = std::slice::from_raw_parts_mut(out_fast_ptr, len);
1566        let out_slow = std::slice::from_raw_parts_mut(out_slow_ptr, len);
1567        let input = CycleChannelOscillatorInput::from_slices(
1568            source,
1569            high,
1570            low,
1571            close,
1572            CycleChannelOscillatorParams {
1573                short_cycle_length: Some(short_cycle_length),
1574                medium_cycle_length: Some(medium_cycle_length),
1575                short_multiplier: Some(short_multiplier),
1576                medium_multiplier: Some(medium_multiplier),
1577            },
1578        );
1579        cycle_channel_oscillator_into_slice(out_fast, out_slow, &input, Kernel::Auto)
1580            .map_err(|e| JsValue::from_str(&e.to_string()))
1581    }
1582}
1583
1584#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1585#[wasm_bindgen]
1586pub fn cycle_channel_oscillator_batch_into(
1587    source_ptr: *const f64,
1588    high_ptr: *const f64,
1589    low_ptr: *const f64,
1590    close_ptr: *const f64,
1591    out_fast_ptr: *mut f64,
1592    out_slow_ptr: *mut f64,
1593    len: usize,
1594    short_cycle_length_start: usize,
1595    short_cycle_length_end: usize,
1596    short_cycle_length_step: usize,
1597    medium_cycle_length_start: usize,
1598    medium_cycle_length_end: usize,
1599    medium_cycle_length_step: usize,
1600    short_multiplier_start: f64,
1601    short_multiplier_end: f64,
1602    short_multiplier_step: f64,
1603    medium_multiplier_start: f64,
1604    medium_multiplier_end: f64,
1605    medium_multiplier_step: f64,
1606) -> Result<usize, JsValue> {
1607    if source_ptr.is_null()
1608        || high_ptr.is_null()
1609        || low_ptr.is_null()
1610        || close_ptr.is_null()
1611        || out_fast_ptr.is_null()
1612        || out_slow_ptr.is_null()
1613    {
1614        return Err(JsValue::from_str(
1615            "null pointer passed to cycle_channel_oscillator_batch_into",
1616        ));
1617    }
1618    unsafe {
1619        let source = std::slice::from_raw_parts(source_ptr, len);
1620        let high = std::slice::from_raw_parts(high_ptr, len);
1621        let low = std::slice::from_raw_parts(low_ptr, len);
1622        let close = std::slice::from_raw_parts(close_ptr, len);
1623        let sweep = CycleChannelOscillatorBatchRange {
1624            short_cycle_length: (
1625                short_cycle_length_start,
1626                short_cycle_length_end,
1627                short_cycle_length_step,
1628            ),
1629            medium_cycle_length: (
1630                medium_cycle_length_start,
1631                medium_cycle_length_end,
1632                medium_cycle_length_step,
1633            ),
1634            short_multiplier: (
1635                short_multiplier_start,
1636                short_multiplier_end,
1637                short_multiplier_step,
1638            ),
1639            medium_multiplier: (
1640                medium_multiplier_start,
1641                medium_multiplier_end,
1642                medium_multiplier_step,
1643            ),
1644        };
1645        let combos = expand_grid(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
1646        let rows = combos.len();
1647        let total = rows.checked_mul(len).ok_or_else(|| {
1648            JsValue::from_str("rows*cols overflow in cycle_channel_oscillator_batch_into")
1649        })?;
1650        let out_fast = std::slice::from_raw_parts_mut(out_fast_ptr, total);
1651        let out_slow = std::slice::from_raw_parts_mut(out_slow_ptr, total);
1652        cycle_channel_oscillator_batch_into_slice(
1653            out_fast,
1654            out_slow,
1655            source,
1656            high,
1657            low,
1658            close,
1659            &sweep,
1660            Kernel::Auto,
1661        )
1662        .map_err(|e| JsValue::from_str(&e.to_string()))?;
1663        Ok(rows)
1664    }
1665}
1666
1667#[cfg(test)]
1668mod tests {
1669    use super::*;
1670
1671    fn sample_ohlc(n: usize) -> (Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>) {
1672        let close: Vec<f64> = (0..n)
1673            .map(|i| 100.0 + ((i as f64) * 0.11).sin() * 2.4 + (i as f64) * 0.03)
1674            .collect();
1675        let high: Vec<f64> = close
1676            .iter()
1677            .enumerate()
1678            .map(|(i, &c)| c + 1.1 + ((i as f64) * 0.17).cos().abs() * 0.4)
1679            .collect();
1680        let low: Vec<f64> = close
1681            .iter()
1682            .enumerate()
1683            .map(|(i, &c)| c - 1.0 - ((i as f64) * 0.13).sin().abs() * 0.35)
1684            .collect();
1685        let source = close.clone();
1686        (source, high, low, close)
1687    }
1688
1689    fn manual_rma(src: &[f64], length: usize) -> Vec<f64> {
1690        let mut out = vec![f64::NAN; src.len()];
1691        let mut sum = 0.0;
1692        let mut count = 0usize;
1693        let mut value = f64::NAN;
1694        for (i, &x) in src.iter().enumerate() {
1695            if !x.is_finite() {
1696                continue;
1697            }
1698            if count < length {
1699                sum += x;
1700                count += 1;
1701                if count == length {
1702                    value = sum / length as f64;
1703                    out[i] = value;
1704                }
1705            } else {
1706                value = value + (x - value) / length as f64;
1707                count += 1;
1708                out[i] = value;
1709            }
1710        }
1711        out
1712    }
1713
1714    fn manual_atr(high: &[f64], low: &[f64], close: &[f64], length: usize) -> Vec<f64> {
1715        let mut tr = vec![f64::NAN; close.len()];
1716        let mut prev_close: Option<f64> = None;
1717        for i in 0..close.len() {
1718            if !(high[i].is_finite() && low[i].is_finite() && close[i].is_finite()) {
1719                continue;
1720            }
1721            tr[i] = match prev_close {
1722                Some(prev) => (high[i] - low[i])
1723                    .max((high[i] - prev).abs())
1724                    .max((low[i] - prev).abs()),
1725                None => high[i] - low[i],
1726            };
1727            prev_close = Some(close[i]);
1728        }
1729        manual_rma(&tr, length)
1730    }
1731
1732    fn delayed_or_current(values: &[f64], idx: usize, delay: usize, current: f64) -> f64 {
1733        if idx >= delay {
1734            let value = values[idx - delay];
1735            if value.is_finite() {
1736                value
1737            } else {
1738                current
1739            }
1740        } else {
1741            current
1742        }
1743    }
1744
1745    fn manual_cycle_channel_oscillator(
1746        source: &[f64],
1747        high: &[f64],
1748        low: &[f64],
1749        close: &[f64],
1750        params: CycleChannelOscillatorParams,
1751    ) -> (Vec<f64>, Vec<f64>) {
1752        let resolved = resolve_params(&params).unwrap();
1753        let short_ma = manual_rma(source, resolved.short_period);
1754        let medium_ma = manual_rma(source, resolved.medium_period);
1755        let medium_atr = manual_atr(high, low, close, resolved.medium_period);
1756        let mut fast = vec![f64::NAN; source.len()];
1757        let mut slow = vec![f64::NAN; source.len()];
1758        for i in 0..source.len() {
1759            if !(source[i].is_finite()
1760                && high[i].is_finite()
1761                && low[i].is_finite()
1762                && close[i].is_finite())
1763            {
1764                continue;
1765            }
1766            let medium_center = delayed_or_current(&medium_ma, i, resolved.medium_delay, source[i]);
1767            let short_center = delayed_or_current(&short_ma, i, resolved.short_delay, source[i]);
1768            let offset = resolved.medium_multiplier * medium_atr[i];
1769            let denom = 2.0 * offset;
1770            if denom.is_finite() && denom != 0.0 {
1771                let medium_bottom = medium_center - offset;
1772                fast[i] = (source[i] - medium_bottom) / denom;
1773                slow[i] = (short_center - medium_bottom) / denom;
1774            }
1775        }
1776        (fast, slow)
1777    }
1778
1779    fn assert_close(lhs: &[f64], rhs: &[f64]) {
1780        assert_eq!(lhs.len(), rhs.len());
1781        for (idx, (&a, &b)) in lhs.iter().zip(rhs.iter()).enumerate() {
1782            if a.is_nan() && b.is_nan() {
1783                continue;
1784            }
1785            assert!((a - b).abs() <= 1e-12, "mismatch at {idx}: {a} vs {b}");
1786        }
1787    }
1788
1789    #[test]
1790    fn manual_reference_matches_api() {
1791        let (source, high, low, close) = sample_ohlc(160);
1792        let params = CycleChannelOscillatorParams::default();
1793        let input =
1794            CycleChannelOscillatorInput::from_slices(&source, &high, &low, &close, params.clone());
1795        let out = cycle_channel_oscillator(&input).unwrap();
1796        let (want_fast, want_slow) =
1797            manual_cycle_channel_oscillator(&source, &high, &low, &close, params);
1798        assert_close(&out.fast, &want_fast);
1799        assert_close(&out.slow, &want_slow);
1800    }
1801
1802    #[test]
1803    fn stream_matches_batch() {
1804        let (source, high, low, close) = sample_ohlc(128);
1805        let params = CycleChannelOscillatorParams::default();
1806        let input =
1807            CycleChannelOscillatorInput::from_slices(&source, &high, &low, &close, params.clone());
1808        let batch = cycle_channel_oscillator(&input).unwrap();
1809        let mut stream = CycleChannelOscillatorStream::try_new(params).unwrap();
1810        let mut got_fast = Vec::with_capacity(source.len());
1811        let mut got_slow = Vec::with_capacity(source.len());
1812        for i in 0..source.len() {
1813            let (fast, slow) = stream.update(source[i], high[i], low[i], close[i]);
1814            got_fast.push(fast);
1815            got_slow.push(slow);
1816        }
1817        assert_close(&batch.fast, &got_fast);
1818        assert_close(&batch.slow, &got_slow);
1819    }
1820
1821    #[test]
1822    fn batch_first_row_matches_single() {
1823        let (source, high, low, close) = sample_ohlc(96);
1824        let batch = cycle_channel_oscillator_batch_with_kernel(
1825            &source,
1826            &high,
1827            &low,
1828            &close,
1829            &CycleChannelOscillatorBatchRange {
1830                short_cycle_length: (10, 10, 0),
1831                medium_cycle_length: (30, 32, 2),
1832                short_multiplier: (1.0, 1.0, 0.0),
1833                medium_multiplier: (3.0, 3.0, 0.0),
1834            },
1835            Kernel::Auto,
1836        )
1837        .unwrap();
1838        let input = CycleChannelOscillatorInput::from_slices(
1839            &source,
1840            &high,
1841            &low,
1842            &close,
1843            CycleChannelOscillatorParams::default(),
1844        );
1845        let single = cycle_channel_oscillator(&input).unwrap();
1846        assert_eq!(batch.rows, 2);
1847        assert_close(&batch.fast[..96], single.fast.as_slice());
1848        assert_close(&batch.slow[..96], single.slow.as_slice());
1849    }
1850
1851    #[test]
1852    fn into_slice_matches_single() {
1853        let (source, high, low, close) = sample_ohlc(72);
1854        let input = CycleChannelOscillatorInput::from_slices(
1855            &source,
1856            &high,
1857            &low,
1858            &close,
1859            CycleChannelOscillatorParams::default(),
1860        );
1861        let single = cycle_channel_oscillator(&input).unwrap();
1862        let mut out_fast = vec![0.0; source.len()];
1863        let mut out_slow = vec![0.0; source.len()];
1864        cycle_channel_oscillator_into_slice(&mut out_fast, &mut out_slow, &input, Kernel::Auto)
1865            .unwrap();
1866        assert_close(&single.fast, &out_fast);
1867        assert_close(&single.slow, &out_slow);
1868    }
1869
1870    #[test]
1871    fn invalid_length_is_rejected() {
1872        let (source, high, low, close) = sample_ohlc(32);
1873        let input = CycleChannelOscillatorInput::from_slices(
1874            &source,
1875            &high,
1876            &low,
1877            &close,
1878            CycleChannelOscillatorParams {
1879                short_cycle_length: Some(1),
1880                ..CycleChannelOscillatorParams::default()
1881            },
1882        );
1883        let err = cycle_channel_oscillator(&input).unwrap_err();
1884        assert!(matches!(
1885            err,
1886            CycleChannelOscillatorError::InvalidCycleLength {
1887                name: "short_cycle_length",
1888                ..
1889            }
1890        ));
1891    }
1892}