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