Skip to main content

vector_ta/indicators/
goertzel_cycle_composite_wave.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, detect_best_kernel, init_matrix_prefixes,
19    make_uninit_matrix,
20};
21#[cfg(feature = "python")]
22use crate::utilities::kernel_validation::validate_kernel;
23#[cfg(not(target_arch = "wasm32"))]
24use rayon::prelude::*;
25use std::collections::VecDeque;
26use std::convert::AsRef;
27use std::error::Error;
28use thiserror::Error;
29
30const DEFAULT_MAX_PERIOD: usize = 120;
31const DEFAULT_START_AT_CYCLE: usize = 1;
32const DEFAULT_USE_TOP_CYCLES: usize = 2;
33const DEFAULT_BAR_TO_CALCULATE: usize = 1;
34const DEFAULT_DT_ZL_PER1: usize = 10;
35const DEFAULT_DT_ZL_PER2: usize = 40;
36const DEFAULT_DT_HP_PER1: usize = 20;
37const DEFAULT_DT_HP_PER2: usize = 80;
38const DEFAULT_DT_REG_ZL_SMOOTH_PER: usize = 5;
39const DEFAULT_HP_SMOOTH_PER: usize = 20;
40const DEFAULT_ZLMA_SMOOTH_PER: usize = 10;
41const DEFAULT_BART_NO_CYCLES: usize = 5;
42const DEFAULT_BART_SMOOTH_PER: usize = 2;
43const DEFAULT_BART_SIG_LIMIT: usize = 50;
44
45impl<'a> AsRef<[f64]> for GoertzelCycleCompositeWaveInput<'a> {
46    #[inline(always)]
47    fn as_ref(&self) -> &[f64] {
48        match &self.data {
49            GoertzelCycleCompositeWaveData::Slice(slice) => slice,
50            GoertzelCycleCompositeWaveData::Candles { candles, source } => {
51                source_type(candles, source)
52            }
53        }
54    }
55}
56
57#[derive(Debug, Clone)]
58pub enum GoertzelCycleCompositeWaveData<'a> {
59    Candles {
60        candles: &'a Candles,
61        source: &'a str,
62    },
63    Slice(&'a [f64]),
64}
65
66#[derive(Debug, Clone)]
67pub struct GoertzelCycleCompositeWaveOutput {
68    pub values: Vec<f64>,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72#[cfg_attr(
73    all(target_arch = "wasm32", feature = "wasm"),
74    derive(Serialize, Deserialize)
75)]
76#[cfg_attr(
77    all(target_arch = "wasm32", feature = "wasm"),
78    serde(rename_all = "snake_case")
79)]
80pub enum GoertzelDetrendMode {
81    None,
82    HodrickPrescottSmoothing,
83    ZeroLagSmoothing,
84    HodrickPrescottDetrending,
85    ZeroLagDetrending,
86    LogZeroLagRegressionDetrending,
87}
88
89impl Default for GoertzelDetrendMode {
90    fn default() -> Self {
91        Self::HodrickPrescottDetrending
92    }
93}
94
95impl GoertzelDetrendMode {
96    #[inline(always)]
97    pub fn parse(value: &str) -> Option<Self> {
98        match value.trim().to_ascii_lowercase().as_str() {
99            "none" => Some(Self::None),
100            "hodrick_prescott_smoothing" | "hp_smoothing" | "hpsmth" => {
101                Some(Self::HodrickPrescottSmoothing)
102            }
103            "zero_lag_smoothing" | "zl_smoothing" | "zlagsmth" => Some(Self::ZeroLagSmoothing),
104            "hodrick_prescott_detrending" | "hp_detrending" | "hpsmthdt" => {
105                Some(Self::HodrickPrescottDetrending)
106            }
107            "zero_lag_detrending" | "zl_detrending" | "zlagsmthdt" => Some(Self::ZeroLagDetrending),
108            "log_zero_lag_regression_detrending" | "log_zl_regression" | "logzlagregression" => {
109                Some(Self::LogZeroLagRegressionDetrending)
110            }
111            _ => None,
112        }
113    }
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117#[cfg_attr(
118    all(target_arch = "wasm32", feature = "wasm"),
119    derive(Serialize, Deserialize)
120)]
121pub struct GoertzelCycleCompositeWaveParams {
122    pub max_period: Option<usize>,
123    pub start_at_cycle: Option<usize>,
124    pub use_top_cycles: Option<usize>,
125    pub bar_to_calculate: Option<usize>,
126    pub detrend_mode: Option<GoertzelDetrendMode>,
127    pub dt_zl_per1: Option<usize>,
128    pub dt_zl_per2: Option<usize>,
129    pub dt_hp_per1: Option<usize>,
130    pub dt_hp_per2: Option<usize>,
131    pub dt_reg_zl_smooth_per: Option<usize>,
132    pub hp_smooth_per: Option<usize>,
133    pub zlma_smooth_per: Option<usize>,
134    pub filter_bartels: Option<bool>,
135    pub bart_no_cycles: Option<usize>,
136    pub bart_smooth_per: Option<usize>,
137    pub bart_sig_limit: Option<usize>,
138    pub sort_bartels: Option<bool>,
139    pub squared_amp: Option<bool>,
140    pub use_cosine: Option<bool>,
141    pub subtract_noise: Option<bool>,
142    pub use_cycle_strength: Option<bool>,
143}
144
145impl Default for GoertzelCycleCompositeWaveParams {
146    fn default() -> Self {
147        Self {
148            max_period: Some(DEFAULT_MAX_PERIOD),
149            start_at_cycle: Some(DEFAULT_START_AT_CYCLE),
150            use_top_cycles: Some(DEFAULT_USE_TOP_CYCLES),
151            bar_to_calculate: Some(DEFAULT_BAR_TO_CALCULATE),
152            detrend_mode: Some(GoertzelDetrendMode::HodrickPrescottDetrending),
153            dt_zl_per1: Some(DEFAULT_DT_ZL_PER1),
154            dt_zl_per2: Some(DEFAULT_DT_ZL_PER2),
155            dt_hp_per1: Some(DEFAULT_DT_HP_PER1),
156            dt_hp_per2: Some(DEFAULT_DT_HP_PER2),
157            dt_reg_zl_smooth_per: Some(DEFAULT_DT_REG_ZL_SMOOTH_PER),
158            hp_smooth_per: Some(DEFAULT_HP_SMOOTH_PER),
159            zlma_smooth_per: Some(DEFAULT_ZLMA_SMOOTH_PER),
160            filter_bartels: Some(false),
161            bart_no_cycles: Some(DEFAULT_BART_NO_CYCLES),
162            bart_smooth_per: Some(DEFAULT_BART_SMOOTH_PER),
163            bart_sig_limit: Some(DEFAULT_BART_SIG_LIMIT),
164            sort_bartels: Some(false),
165            squared_amp: Some(true),
166            use_cosine: Some(true),
167            subtract_noise: Some(false),
168            use_cycle_strength: Some(true),
169        }
170    }
171}
172
173#[derive(Debug, Clone)]
174pub struct GoertzelCycleCompositeWaveInput<'a> {
175    pub data: GoertzelCycleCompositeWaveData<'a>,
176    pub params: GoertzelCycleCompositeWaveParams,
177}
178
179impl<'a> GoertzelCycleCompositeWaveInput<'a> {
180    #[inline]
181    pub fn from_candles(
182        candles: &'a Candles,
183        source: &'a str,
184        params: GoertzelCycleCompositeWaveParams,
185    ) -> Self {
186        Self {
187            data: GoertzelCycleCompositeWaveData::Candles { candles, source },
188            params,
189        }
190    }
191
192    #[inline]
193    pub fn from_slice(data: &'a [f64], params: GoertzelCycleCompositeWaveParams) -> Self {
194        Self {
195            data: GoertzelCycleCompositeWaveData::Slice(data),
196            params,
197        }
198    }
199
200    #[inline]
201    pub fn with_default_candles(candles: &'a Candles) -> Self {
202        Self::from_candles(
203            candles,
204            "close",
205            GoertzelCycleCompositeWaveParams::default(),
206        )
207    }
208}
209
210#[derive(Copy, Clone, Debug)]
211pub struct GoertzelCycleCompositeWaveBuilder {
212    params: GoertzelCycleCompositeWaveParams,
213    kernel: Kernel,
214}
215
216impl Default for GoertzelCycleCompositeWaveBuilder {
217    fn default() -> Self {
218        Self {
219            params: GoertzelCycleCompositeWaveParams::default(),
220            kernel: Kernel::Auto,
221        }
222    }
223}
224
225impl GoertzelCycleCompositeWaveBuilder {
226    #[inline(always)]
227    pub fn new() -> Self {
228        Self::default()
229    }
230
231    #[inline(always)]
232    pub fn params(mut self, value: GoertzelCycleCompositeWaveParams) -> Self {
233        self.params = value;
234        self
235    }
236
237    #[inline(always)]
238    pub fn max_period(mut self, value: usize) -> Self {
239        self.params.max_period = Some(value);
240        self
241    }
242
243    #[inline(always)]
244    pub fn start_at_cycle(mut self, value: usize) -> Self {
245        self.params.start_at_cycle = Some(value);
246        self
247    }
248
249    #[inline(always)]
250    pub fn use_top_cycles(mut self, value: usize) -> Self {
251        self.params.use_top_cycles = Some(value);
252        self
253    }
254
255    #[inline(always)]
256    pub fn kernel(mut self, value: Kernel) -> Self {
257        self.kernel = value;
258        self
259    }
260
261    #[inline(always)]
262    pub fn apply(
263        self,
264        candles: &Candles,
265    ) -> Result<GoertzelCycleCompositeWaveOutput, GoertzelCycleCompositeWaveError> {
266        goertzel_cycle_composite_wave_with_kernel(
267            &GoertzelCycleCompositeWaveInput::from_candles(candles, "close", self.params),
268            self.kernel,
269        )
270    }
271
272    #[inline(always)]
273    pub fn apply_slice(
274        self,
275        data: &[f64],
276    ) -> Result<GoertzelCycleCompositeWaveOutput, GoertzelCycleCompositeWaveError> {
277        goertzel_cycle_composite_wave_with_kernel(
278            &GoertzelCycleCompositeWaveInput::from_slice(data, self.params),
279            self.kernel,
280        )
281    }
282
283    #[inline(always)]
284    pub fn into_stream(
285        self,
286    ) -> Result<GoertzelCycleCompositeWaveStream, GoertzelCycleCompositeWaveError> {
287        GoertzelCycleCompositeWaveStream::try_new(self.params)
288    }
289}
290
291#[derive(Debug, Error)]
292pub enum GoertzelCycleCompositeWaveError {
293    #[error("goertzel_cycle_composite_wave: Input data slice is empty.")]
294    EmptyInputData,
295    #[error("goertzel_cycle_composite_wave: All values are NaN.")]
296    AllValuesNaN,
297    #[error("goertzel_cycle_composite_wave: Invalid parameter {name}: {value}")]
298    InvalidParameter { name: &'static str, value: usize },
299    #[error(
300        "goertzel_cycle_composite_wave: Not enough valid data: needed = {needed}, valid = {valid}"
301    )]
302    NotEnoughValidData { needed: usize, valid: usize },
303    #[error(
304        "goertzel_cycle_composite_wave: Output length mismatch: expected = {expected}, got = {got}"
305    )]
306    OutputLengthMismatch { expected: usize, got: usize },
307    #[error("goertzel_cycle_composite_wave: Invalid range: start={start}, end={end}, step={step}")]
308    InvalidRange {
309        start: usize,
310        end: usize,
311        step: usize,
312    },
313    #[error("goertzel_cycle_composite_wave: Invalid kernel for batch: {0:?}")]
314    InvalidKernelForBatch(Kernel),
315    #[error(
316        "goertzel_cycle_composite_wave: Output length mismatch: dst = {dst_len}, expected = {expected_len}"
317    )]
318    MismatchedOutputLen { dst_len: usize, expected_len: usize },
319    #[error("goertzel_cycle_composite_wave: Invalid input: {msg}")]
320    InvalidInput { msg: String },
321    #[error("goertzel_cycle_composite_wave: Invalid detrend mode: {0}")]
322    InvalidDetrendMode(String),
323}
324
325#[derive(Debug, Clone, Copy)]
326struct CycleInfo {
327    cycle: usize,
328    amplitude: f64,
329    phase: f64,
330    bartels: f64,
331}
332
333#[inline(always)]
334fn sample_size_for_params(params: &GoertzelCycleCompositeWaveParams) -> usize {
335    let max_period = params.max_period.unwrap_or(DEFAULT_MAX_PERIOD);
336    let bar_to_calculate = params.bar_to_calculate.unwrap_or(DEFAULT_BAR_TO_CALCULATE);
337    let bart_no_cycles = params.bart_no_cycles.unwrap_or(DEFAULT_BART_NO_CYCLES);
338    let cycle_span = (2 * max_period).max(bart_no_cycles.saturating_mul(max_period));
339    cycle_span.saturating_add(bar_to_calculate)
340}
341
342#[inline(always)]
343fn longest_valid_run(data: &[f64]) -> usize {
344    let mut best = 0usize;
345    let mut cur = 0usize;
346    for &value in data {
347        if value.is_finite() {
348            cur += 1;
349            best = best.max(cur);
350        } else {
351            cur = 0;
352        }
353    }
354    best
355}
356
357#[inline(always)]
358fn validate_positive(
359    name: &'static str,
360    value: usize,
361) -> Result<(), GoertzelCycleCompositeWaveError> {
362    if value == 0 {
363        return Err(GoertzelCycleCompositeWaveError::InvalidParameter { name, value });
364    }
365    Ok(())
366}
367
368#[inline(always)]
369fn validate_hp_period(
370    name: &'static str,
371    value: usize,
372) -> Result<(), GoertzelCycleCompositeWaveError> {
373    if value < 2 {
374        return Err(GoertzelCycleCompositeWaveError::InvalidParameter { name, value });
375    }
376    Ok(())
377}
378
379#[inline(always)]
380fn validate_params(
381    params: &GoertzelCycleCompositeWaveParams,
382) -> Result<(), GoertzelCycleCompositeWaveError> {
383    validate_hp_period(
384        "max_period",
385        params.max_period.unwrap_or(DEFAULT_MAX_PERIOD),
386    )?;
387    validate_positive(
388        "start_at_cycle",
389        params.start_at_cycle.unwrap_or(DEFAULT_START_AT_CYCLE),
390    )?;
391    validate_positive(
392        "use_top_cycles",
393        params.use_top_cycles.unwrap_or(DEFAULT_USE_TOP_CYCLES),
394    )?;
395    validate_positive(
396        "dt_zl_per1",
397        params.dt_zl_per1.unwrap_or(DEFAULT_DT_ZL_PER1),
398    )?;
399    validate_positive(
400        "dt_zl_per2",
401        params.dt_zl_per2.unwrap_or(DEFAULT_DT_ZL_PER2),
402    )?;
403    validate_hp_period(
404        "dt_hp_per1",
405        params.dt_hp_per1.unwrap_or(DEFAULT_DT_HP_PER1),
406    )?;
407    validate_hp_period(
408        "dt_hp_per2",
409        params.dt_hp_per2.unwrap_or(DEFAULT_DT_HP_PER2),
410    )?;
411    validate_positive(
412        "dt_reg_zl_smooth_per",
413        params
414            .dt_reg_zl_smooth_per
415            .unwrap_or(DEFAULT_DT_REG_ZL_SMOOTH_PER),
416    )?;
417    validate_hp_period(
418        "hp_smooth_per",
419        params.hp_smooth_per.unwrap_or(DEFAULT_HP_SMOOTH_PER),
420    )?;
421    validate_positive(
422        "zlma_smooth_per",
423        params.zlma_smooth_per.unwrap_or(DEFAULT_ZLMA_SMOOTH_PER),
424    )?;
425    validate_positive(
426        "bart_no_cycles",
427        params.bart_no_cycles.unwrap_or(DEFAULT_BART_NO_CYCLES),
428    )?;
429    validate_positive(
430        "bart_smooth_per",
431        params.bart_smooth_per.unwrap_or(DEFAULT_BART_SMOOTH_PER),
432    )?;
433    Ok(())
434}
435
436#[inline(always)]
437fn validate_common(
438    data: &[f64],
439    params: &GoertzelCycleCompositeWaveParams,
440) -> Result<usize, GoertzelCycleCompositeWaveError> {
441    if data.is_empty() {
442        return Err(GoertzelCycleCompositeWaveError::EmptyInputData);
443    }
444    validate_params(params)?;
445    let max_run = longest_valid_run(data);
446    if max_run == 0 {
447        return Err(GoertzelCycleCompositeWaveError::AllValuesNaN);
448    }
449    let needed = sample_size_for_params(params);
450    if max_run < needed {
451        return Err(GoertzelCycleCompositeWaveError::NotEnoughValidData {
452            needed,
453            valid: max_run,
454        });
455    }
456    Ok(needed)
457}
458
459#[inline(always)]
460fn hp_lambda(period: usize) -> f64 {
461    0.0625 / (std::f64::consts::PI / period as f64).sin().powi(4)
462}
463
464fn zero_lag_ma(src: &[f64], smooth_per: usize) -> Vec<f64> {
465    let bars_taken = src.len();
466    let mut lwma1 = vec![0.0; bars_taken];
467    let mut output = vec![0.0; bars_taken];
468
469    for i in (0..bars_taken).rev() {
470        let mut sum = 0.0;
471        let mut sumw = 0.0;
472        for k in 0..smooth_per {
473            let idx = i + k;
474            if idx < bars_taken {
475                let weight = (smooth_per - k) as f64;
476                sumw += weight;
477                sum += weight * src[idx];
478            }
479        }
480        lwma1[i] = if sumw != 0.0 { sum / sumw } else { 0.0 };
481    }
482
483    for i in 0..bars_taken {
484        let mut sum = 0.0;
485        let mut sumw = 0.0;
486        for k in 0..smooth_per {
487            if i >= k {
488                let weight = (smooth_per - k) as f64;
489                sumw += weight;
490                sum += weight * lwma1[i - k];
491            }
492        }
493        output[i] = if sumw != 0.0 { sum / sumw } else { 0.0 };
494    }
495
496    output
497}
498
499fn hodrick_prescott_filter(src: &[f64], lambda: f64) -> Vec<f64> {
500    let per = src.len();
501    let mut a = vec![0.0; per];
502    let mut b = vec![0.0; per];
503    let mut c = vec![0.0; per];
504    let mut output = src.to_vec();
505
506    if per == 0 {
507        return output;
508    }
509
510    a[0] = 1.0 + lambda;
511    b[0] = -2.0 * lambda;
512    c[0] = lambda;
513    for i in 1..per.saturating_sub(2) {
514        a[i] = 6.0 * lambda + 1.0;
515        b[i] = -4.0 * lambda;
516        c[i] = lambda;
517    }
518    if per > 1 {
519        a[1] = 5.0 * lambda + 1.0;
520        a[per - 2] = 5.0 * lambda + 1.0;
521        a[per - 1] = 1.0 + lambda;
522        b[per - 2] = -2.0 * lambda;
523    }
524
525    let mut h1 = 0.0;
526    let mut h2 = 0.0;
527    let mut h3 = 0.0;
528    let mut h4 = 0.0;
529    let mut h5 = 0.0;
530    let mut hh1 = 0.0;
531    let mut hh2 = 0.0;
532    let mut hh3 = 0.0;
533    let mut hh5 = 0.0;
534
535    for i in 0..per {
536        let z = a[i] - h4 * h1 - hh5 * h2;
537        if z.abs() <= f64::EPSILON {
538            break;
539        }
540        let hb = b[i];
541        hh1 = h1;
542        h1 = (hb - h4 * h2) / z;
543        b[i] = h1;
544        let hc = c[i];
545        hh2 = h2;
546        h2 = hc / z;
547        c[i] = h2;
548        a[i] = (src[i] - hh3 * hh5 - h3 * h4) / z;
549        hh3 = h3;
550        h3 = a[i];
551        h4 = hb - h5 * hh1;
552        hh5 = h5;
553        h5 = hc;
554    }
555
556    let mut h1b = a[per - 1];
557    let mut h2b = 0.0;
558    output[per - 1] = h1b;
559    for i in (0..per.saturating_sub(1)).rev() {
560        output[i] = a[i] - b[i] * h1b - c[i] * h2b;
561        h2b = h1b;
562        h1b = output[i];
563    }
564
565    output
566}
567
568fn detrend_ln_zero_lag_regression(src: &[f64], smooth_per: usize) -> Option<Vec<f64>> {
569    let mut calc_values = zero_lag_ma(src, smooth_per);
570    for value in &mut calc_values {
571        if *value <= 0.0 || !value.is_finite() {
572            return None;
573        }
574        *value = value.ln() * 100.0;
575    }
576
577    let bars_taken = calc_values.len();
578    let mut sumy = 0.0;
579    let mut sumx = 0.0;
580    let mut sumxy = 0.0;
581    let mut sumx2 = 0.0;
582    for (i, &value) in calc_values.iter().enumerate() {
583        let x = i as f64;
584        sumy += value;
585        sumx += x;
586        sumxy += x * value;
587        sumx2 += x * x;
588    }
589
590    let denom = sumx2 * bars_taken as f64 - sumx * sumx;
591    if denom.abs() <= f64::EPSILON {
592        return None;
593    }
594    let slope = (sumxy * bars_taken as f64 - sumx * sumy) / denom;
595    let intercept = (sumy - sumx * slope) / bars_taken as f64;
596
597    let mut output = vec![0.0; bars_taken];
598    for i in 0..bars_taken {
599        output[i] = calc_values[i] - (intercept + slope * i as f64);
600    }
601    Some(output)
602}
603
604fn apply_detrend_mode(
605    src_rev: &[f64],
606    params: &GoertzelCycleCompositeWaveParams,
607) -> Option<Vec<f64>> {
608    let mode = params
609        .detrend_mode
610        .unwrap_or(GoertzelDetrendMode::HodrickPrescottDetrending);
611    let out = match mode {
612        GoertzelDetrendMode::None => src_rev.to_vec(),
613        GoertzelDetrendMode::HodrickPrescottSmoothing => {
614            hodrick_prescott_filter(src_rev, hp_lambda(params.hp_smooth_per.unwrap_or(20)))
615        }
616        GoertzelDetrendMode::ZeroLagSmoothing => {
617            zero_lag_ma(src_rev, params.zlma_smooth_per.unwrap_or(10))
618        }
619        GoertzelDetrendMode::HodrickPrescottDetrending => {
620            let fast = hodrick_prescott_filter(src_rev, hp_lambda(params.dt_hp_per1.unwrap_or(20)));
621            let slow = hodrick_prescott_filter(src_rev, hp_lambda(params.dt_hp_per2.unwrap_or(80)));
622            fast.iter().zip(slow.iter()).map(|(a, b)| a - b).collect()
623        }
624        GoertzelDetrendMode::ZeroLagDetrending => {
625            let fast = zero_lag_ma(src_rev, params.dt_zl_per1.unwrap_or(10));
626            let slow = zero_lag_ma(src_rev, params.dt_zl_per2.unwrap_or(40));
627            fast.iter().zip(slow.iter()).map(|(a, b)| a - b).collect()
628        }
629        GoertzelDetrendMode::LogZeroLagRegressionDetrending => {
630            detrend_ln_zero_lag_regression(src_rev, params.dt_reg_zl_smooth_per.unwrap_or(5))?
631        }
632    };
633    if out.iter().all(|v| v.is_finite()) {
634        Some(out)
635    } else {
636        None
637    }
638}
639
640fn bartels_prob(n: usize, cycle_count: usize, values: &[f64]) -> f64 {
641    if n == 0 || cycle_count == 0 || values.len() < n * cycle_count {
642        return 1.0;
643    }
644
645    let mut avg_coeff_a = 0.0;
646    let mut avg_coeff_b = 0.0;
647    let mut avg_ind_amplit = 0.0;
648    let mut vsin = vec![0.0; n];
649    let mut vcos = vec![0.0; n];
650
651    for i in 0..n {
652        let theta = (i + 1) as f64 / n as f64 * 2.0 * std::f64::consts::PI;
653        vsin[i] = theta.sin();
654        vcos[i] = theta.cos();
655    }
656
657    for t in 0..cycle_count {
658        let mut coeff_a = 0.0;
659        let mut coeff_b = 0.0;
660        let base = t * n;
661        for i in 0..n {
662            let value = values[base + i];
663            coeff_a += vsin[i] * value;
664            coeff_b += vcos[i] * value;
665        }
666        avg_coeff_a += coeff_a;
667        avg_coeff_b += coeff_b;
668        avg_ind_amplit += coeff_a * coeff_a + coeff_b * coeff_b;
669    }
670
671    avg_coeff_a /= cycle_count as f64;
672    avg_coeff_b /= cycle_count as f64;
673    let avg_ampl = (avg_coeff_a * avg_coeff_a + avg_coeff_b * avg_coeff_b).sqrt();
674    let avg_ind_amplit = (avg_ind_amplit / cycle_count as f64).sqrt();
675    let expected_ampl = avg_ind_amplit / (cycle_count as f64).sqrt();
676    if expected_ampl <= f64::EPSILON {
677        return 1.0;
678    }
679    let a_ratio = avg_ampl / expected_ampl;
680    (-a_ratio * a_ratio).exp()
681}
682
683fn apply_bartels(
684    src_rev: &[f64],
685    cycles: &mut Vec<CycleInfo>,
686    params: &GoertzelCycleCompositeWaveParams,
687) {
688    let bart_smooth_per = params.bart_smooth_per.unwrap_or(DEFAULT_BART_SMOOTH_PER);
689    let bart_no_cycles = params.bart_no_cycles.unwrap_or(DEFAULT_BART_NO_CYCLES);
690    let bart_sig_limit = params.bart_sig_limit.unwrap_or(DEFAULT_BART_SIG_LIMIT) as f64;
691
692    for cycle in cycles.iter_mut() {
693        let bars_taken = cycle.cycle.saturating_mul(bart_no_cycles);
694        if bars_taken == 0 || bars_taken > src_rev.len() {
695            cycle.bartels = 0.0;
696            continue;
697        }
698        cycle.bartels = detrend_ln_zero_lag_regression(&src_rev[..bars_taken], bart_smooth_per)
699            .map(|values| (1.0 - bartels_prob(cycle.cycle, bart_no_cycles, &values)) * 100.0)
700            .unwrap_or(0.0);
701    }
702
703    cycles.retain(|cycle| cycle.bartels > bart_sig_limit);
704    if params.sort_bartels.unwrap_or(false) {
705        cycles.sort_by(|a, b| {
706            b.bartels
707                .partial_cmp(&a.bartels)
708                .unwrap_or(std::cmp::Ordering::Equal)
709        });
710    }
711}
712
713fn extract_cycles(src_rev: &[f64], params: &GoertzelCycleCompositeWaveParams) -> Vec<CycleInfo> {
714    let per = params.max_period.unwrap_or(DEFAULT_MAX_PERIOD);
715    let for_bar = params.bar_to_calculate.unwrap_or(DEFAULT_BAR_TO_CALCULATE);
716    let sample = 2 * per;
717    if src_rev.len() < for_bar + sample || sample < 2 {
718        return Vec::new();
719    }
720
721    let mut amp_work = vec![0.0; sample + 1];
722    let mut phase_work = vec![0.0; sample + 1];
723    let mut mark_work = vec![0.0; sample + 1];
724    let mut detrended = vec![0.0; sample + 1];
725
726    let temp1 = src_rev[for_bar + sample - 1];
727    let trend_slope = (src_rev[for_bar] - temp1) / (sample as f64 - 1.0);
728    for k in (1..sample).rev() {
729        detrended[k] = src_rev[for_bar + k - 1] - (temp1 + trend_slope * (sample - k) as f64);
730    }
731
732    for k in 2..=per {
733        let z = 1.0 / k as f64;
734        let coeff = 2.0 * (2.0 * std::f64::consts::PI * z).cos();
735        let mut w = 0.0;
736        let mut x = 0.0;
737        let mut y = 0.0;
738        for i in (1..=sample).rev() {
739            w = coeff * x - y + detrended[i];
740            y = x;
741            x = w;
742        }
743        let mut real = x - y * coeff / 2.0;
744        if real.abs() <= f64::EPSILON {
745            real = 1e-7;
746        }
747        let imag = y * (2.0 * std::f64::consts::PI * z).sin();
748        let amplitude = if params.squared_amp.unwrap_or(true) {
749            real * real + imag * imag
750        } else {
751            (real * real + imag * imag).sqrt()
752        };
753        amp_work[k] = if params.use_cycle_strength.unwrap_or(true) {
754            amplitude / k as f64
755        } else {
756            amplitude
757        };
758        let mut phase = (imag / real).atan();
759        if real < 0.0 {
760            phase += std::f64::consts::PI;
761        } else if imag < 0.0 {
762            phase += 2.0 * std::f64::consts::PI;
763        }
764        phase_work[k] = phase;
765    }
766
767    for k in 3..per {
768        if amp_work[k] > amp_work[k - 1] && amp_work[k] > amp_work[k + 1] {
769            mark_work[k] = k as f64 * 1e-4;
770        }
771    }
772
773    let mut cycles = Vec::new();
774    for i in 0..=per + 1 {
775        if i < mark_work.len() && mark_work[i] > 0.0 {
776            cycles.push(CycleInfo {
777                cycle: (10000.0 * mark_work[i]).round() as usize,
778                amplitude: amp_work[i],
779                phase: phase_work[i],
780                bartels: 0.0,
781            });
782        }
783    }
784
785    cycles.sort_by(|a, b| {
786        b.amplitude
787            .partial_cmp(&a.amplitude)
788            .unwrap_or(std::cmp::Ordering::Equal)
789    });
790    if params.filter_bartels.unwrap_or(false) {
791        apply_bartels(src_rev, &mut cycles, params);
792    }
793    cycles
794}
795
796fn current_wave_from_cycles(
797    cycles: &[CycleInfo],
798    params: &GoertzelCycleCompositeWaveParams,
799) -> f64 {
800    if cycles.is_empty() {
801        return 0.0;
802    }
803    let start = params
804        .start_at_cycle
805        .unwrap_or(DEFAULT_START_AT_CYCLE)
806        .saturating_sub(1);
807    if start >= cycles.len() {
808        return 0.0;
809    }
810    let count = params.use_top_cycles.unwrap_or(DEFAULT_USE_TOP_CYCLES);
811    let end = (start + count).min(cycles.len());
812    let trig = |cycle: &CycleInfo| {
813        if params.use_cosine.unwrap_or(true) {
814            cycle.amplitude * cycle.phase.cos()
815        } else {
816            cycle.amplitude * cycle.phase.sin()
817        }
818    };
819    let mut out = cycles[start..end].iter().map(trig).sum::<f64>();
820    if params.subtract_noise.unwrap_or(false) && end < cycles.len() {
821        out -= cycles[end..].iter().map(trig).sum::<f64>();
822    }
823    out
824}
825
826#[inline(always)]
827fn compute_window_wave(src_rev: &[f64], params: &GoertzelCycleCompositeWaveParams) -> Option<f64> {
828    let processed = apply_detrend_mode(src_rev, params)?;
829    let cycles = extract_cycles(&processed, params);
830    Some(current_wave_from_cycles(&cycles, params))
831}
832
833#[derive(Debug, Clone)]
834pub struct GoertzelCycleCompositeWaveStream {
835    params: GoertzelCycleCompositeWaveParams,
836    sample_size: usize,
837    window: VecDeque<f64>,
838}
839
840impl GoertzelCycleCompositeWaveStream {
841    #[inline(always)]
842    pub fn try_new(
843        params: GoertzelCycleCompositeWaveParams,
844    ) -> Result<Self, GoertzelCycleCompositeWaveError> {
845        validate_params(&params)?;
846        let sample_size = sample_size_for_params(&params);
847        Ok(Self {
848            params,
849            sample_size,
850            window: VecDeque::with_capacity(sample_size.max(1)),
851        })
852    }
853
854    #[inline(always)]
855    pub fn reset(&mut self) {
856        self.window.clear();
857    }
858
859    #[inline(always)]
860    pub fn update(&mut self, value: f64) -> Option<f64> {
861        if !value.is_finite() {
862            self.reset();
863            return None;
864        }
865        self.window.push_back(value);
866        if self.window.len() > self.sample_size {
867            self.window.pop_front();
868        }
869        if self.window.len() < self.sample_size {
870            return None;
871        }
872
873        let mut src_rev = Vec::with_capacity(self.sample_size);
874        for &value in self.window.iter().rev() {
875            src_rev.push(value);
876        }
877        compute_window_wave(&src_rev, &self.params)
878    }
879
880    #[inline(always)]
881    pub fn get_warmup_period(&self) -> usize {
882        self.sample_size.saturating_sub(1)
883    }
884}
885
886fn compute_row(data: &[f64], params: &GoertzelCycleCompositeWaveParams, out: &mut [f64]) {
887    out.fill(f64::NAN);
888    let sample_size = sample_size_for_params(params);
889    let mut src_rev = Vec::with_capacity(sample_size);
890
891    for end in sample_size.saturating_sub(1)..data.len() {
892        let window = &data[end + 1 - sample_size..=end];
893        if window.iter().any(|v| !v.is_finite()) {
894            continue;
895        }
896        src_rev.clear();
897        src_rev.extend(window.iter().rev().copied());
898        if let Some(value) = compute_window_wave(&src_rev, params) {
899            out[end] = value;
900        }
901    }
902}
903
904pub fn goertzel_cycle_composite_wave(
905    input: &GoertzelCycleCompositeWaveInput,
906) -> Result<GoertzelCycleCompositeWaveOutput, GoertzelCycleCompositeWaveError> {
907    goertzel_cycle_composite_wave_with_kernel(input, Kernel::Auto)
908}
909
910pub fn goertzel_cycle_composite_wave_with_kernel(
911    input: &GoertzelCycleCompositeWaveInput,
912    kernel: Kernel,
913) -> Result<GoertzelCycleCompositeWaveOutput, GoertzelCycleCompositeWaveError> {
914    let data = input.as_ref();
915    let needed = validate_common(data, &input.params)?;
916    let _chosen = match kernel {
917        Kernel::Auto => detect_best_kernel(),
918        other => other,
919    };
920
921    let mut values = alloc_with_nan_prefix(data.len(), needed.saturating_sub(1));
922    compute_row(data, &input.params, &mut values);
923    Ok(GoertzelCycleCompositeWaveOutput { values })
924}
925
926pub fn goertzel_cycle_composite_wave_into_slice(
927    dst: &mut [f64],
928    input: &GoertzelCycleCompositeWaveInput,
929    kernel: Kernel,
930) -> Result<(), GoertzelCycleCompositeWaveError> {
931    let data = input.as_ref();
932    validate_common(data, &input.params)?;
933    if dst.len() != data.len() {
934        return Err(GoertzelCycleCompositeWaveError::OutputLengthMismatch {
935            expected: data.len(),
936            got: dst.len(),
937        });
938    }
939
940    let _chosen = match kernel {
941        Kernel::Auto => detect_best_kernel(),
942        other => other,
943    };
944    compute_row(data, &input.params, dst);
945    Ok(())
946}
947
948#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
949pub fn goertzel_cycle_composite_wave_into(
950    input: &GoertzelCycleCompositeWaveInput,
951    dst: &mut [f64],
952) -> Result<(), GoertzelCycleCompositeWaveError> {
953    goertzel_cycle_composite_wave_into_slice(dst, input, Kernel::Auto)
954}
955
956#[derive(Debug, Clone, Copy)]
957pub struct GoertzelCycleCompositeWaveBatchRange {
958    pub max_period: (usize, usize, usize),
959    pub start_at_cycle: (usize, usize, usize),
960    pub use_top_cycles: (usize, usize, usize),
961    pub base_params: GoertzelCycleCompositeWaveParams,
962}
963
964impl Default for GoertzelCycleCompositeWaveBatchRange {
965    fn default() -> Self {
966        Self {
967            max_period: (DEFAULT_MAX_PERIOD, DEFAULT_MAX_PERIOD, 0),
968            start_at_cycle: (DEFAULT_START_AT_CYCLE, DEFAULT_START_AT_CYCLE, 0),
969            use_top_cycles: (DEFAULT_USE_TOP_CYCLES, DEFAULT_USE_TOP_CYCLES, 0),
970            base_params: GoertzelCycleCompositeWaveParams::default(),
971        }
972    }
973}
974
975#[derive(Debug, Clone)]
976pub struct GoertzelCycleCompositeWaveBatchOutput {
977    pub values: Vec<f64>,
978    pub combos: Vec<GoertzelCycleCompositeWaveParams>,
979    pub rows: usize,
980    pub cols: usize,
981}
982
983impl GoertzelCycleCompositeWaveBatchOutput {
984    pub fn values_for(&self, params: &GoertzelCycleCompositeWaveParams) -> Option<&[f64]> {
985        self.combos
986            .iter()
987            .position(|combo| combo == params)
988            .and_then(|row| {
989                let start = row.checked_mul(self.cols)?;
990                self.values.get(start..start + self.cols)
991            })
992    }
993}
994
995#[derive(Debug, Clone, Copy)]
996pub struct GoertzelCycleCompositeWaveBatchBuilder {
997    range: GoertzelCycleCompositeWaveBatchRange,
998    kernel: Kernel,
999}
1000
1001impl Default for GoertzelCycleCompositeWaveBatchBuilder {
1002    fn default() -> Self {
1003        Self {
1004            range: GoertzelCycleCompositeWaveBatchRange::default(),
1005            kernel: Kernel::Auto,
1006        }
1007    }
1008}
1009
1010impl GoertzelCycleCompositeWaveBatchBuilder {
1011    #[inline(always)]
1012    pub fn new() -> Self {
1013        Self::default()
1014    }
1015
1016    #[inline(always)]
1017    pub fn kernel(mut self, value: Kernel) -> Self {
1018        self.kernel = value;
1019        self
1020    }
1021
1022    #[inline(always)]
1023    pub fn max_period_range(mut self, value: (usize, usize, usize)) -> Self {
1024        self.range.max_period = value;
1025        self
1026    }
1027
1028    #[inline(always)]
1029    pub fn start_at_cycle_range(mut self, value: (usize, usize, usize)) -> Self {
1030        self.range.start_at_cycle = value;
1031        self
1032    }
1033
1034    #[inline(always)]
1035    pub fn use_top_cycles_range(mut self, value: (usize, usize, usize)) -> Self {
1036        self.range.use_top_cycles = value;
1037        self
1038    }
1039
1040    #[inline(always)]
1041    pub fn params(mut self, value: GoertzelCycleCompositeWaveParams) -> Self {
1042        self.range.base_params = value;
1043        self
1044    }
1045
1046    #[inline(always)]
1047    pub fn apply_slice(
1048        self,
1049        data: &[f64],
1050    ) -> Result<GoertzelCycleCompositeWaveBatchOutput, GoertzelCycleCompositeWaveError> {
1051        goertzel_cycle_composite_wave_batch_with_kernel(data, &self.range, self.kernel)
1052    }
1053}
1054
1055#[inline(always)]
1056fn expand_axis(
1057    range: (usize, usize, usize),
1058) -> Result<Vec<usize>, GoertzelCycleCompositeWaveError> {
1059    let (start, end, step) = range;
1060    if start == 0 {
1061        return Err(GoertzelCycleCompositeWaveError::InvalidRange { start, end, step });
1062    }
1063    if step == 0 {
1064        return Ok(vec![start]);
1065    }
1066    if start > end {
1067        return Err(GoertzelCycleCompositeWaveError::InvalidRange { start, end, step });
1068    }
1069    let mut out = Vec::new();
1070    let mut cur = start;
1071    loop {
1072        out.push(cur);
1073        if cur >= end {
1074            break;
1075        }
1076        let next =
1077            cur.checked_add(step)
1078                .ok_or_else(|| GoertzelCycleCompositeWaveError::InvalidInput {
1079                    msg: "goertzel_cycle_composite_wave: range step overflow".to_string(),
1080                })?;
1081        if next <= cur {
1082            return Err(GoertzelCycleCompositeWaveError::InvalidRange { start, end, step });
1083        }
1084        cur = next.min(end);
1085    }
1086    Ok(out)
1087}
1088
1089#[inline(always)]
1090fn expand_grid_checked(
1091    range: &GoertzelCycleCompositeWaveBatchRange,
1092) -> Result<Vec<GoertzelCycleCompositeWaveParams>, GoertzelCycleCompositeWaveError> {
1093    let max_periods = expand_axis(range.max_period)?;
1094    let start_cycles = expand_axis(range.start_at_cycle)?;
1095    let top_cycles = expand_axis(range.use_top_cycles)?;
1096    let mut combos = Vec::with_capacity(max_periods.len() * start_cycles.len() * top_cycles.len());
1097    for max_period in max_periods {
1098        for start_at_cycle in &start_cycles {
1099            for use_top_cycles in &top_cycles {
1100                let mut params = range.base_params;
1101                params.max_period = Some(max_period);
1102                params.start_at_cycle = Some(*start_at_cycle);
1103                params.use_top_cycles = Some(*use_top_cycles);
1104                combos.push(params);
1105            }
1106        }
1107    }
1108    Ok(combos)
1109}
1110
1111pub fn expand_grid_goertzel_cycle_composite_wave(
1112    range: &GoertzelCycleCompositeWaveBatchRange,
1113) -> Vec<GoertzelCycleCompositeWaveParams> {
1114    expand_grid_checked(range).unwrap_or_default()
1115}
1116
1117pub fn goertzel_cycle_composite_wave_batch_with_kernel(
1118    data: &[f64],
1119    sweep: &GoertzelCycleCompositeWaveBatchRange,
1120    kernel: Kernel,
1121) -> Result<GoertzelCycleCompositeWaveBatchOutput, GoertzelCycleCompositeWaveError> {
1122    goertzel_cycle_composite_wave_batch_inner(data, sweep, kernel, true)
1123}
1124
1125pub fn goertzel_cycle_composite_wave_batch_slice(
1126    data: &[f64],
1127    sweep: &GoertzelCycleCompositeWaveBatchRange,
1128    kernel: Kernel,
1129) -> Result<GoertzelCycleCompositeWaveBatchOutput, GoertzelCycleCompositeWaveError> {
1130    goertzel_cycle_composite_wave_batch_inner(data, sweep, kernel, false)
1131}
1132
1133pub fn goertzel_cycle_composite_wave_batch_par_slice(
1134    data: &[f64],
1135    sweep: &GoertzelCycleCompositeWaveBatchRange,
1136    kernel: Kernel,
1137) -> Result<GoertzelCycleCompositeWaveBatchOutput, GoertzelCycleCompositeWaveError> {
1138    goertzel_cycle_composite_wave_batch_inner(data, sweep, kernel, true)
1139}
1140
1141fn goertzel_cycle_composite_wave_batch_inner(
1142    data: &[f64],
1143    sweep: &GoertzelCycleCompositeWaveBatchRange,
1144    kernel: Kernel,
1145    parallel: bool,
1146) -> Result<GoertzelCycleCompositeWaveBatchOutput, GoertzelCycleCompositeWaveError> {
1147    let combos = expand_grid_checked(sweep)?;
1148    let rows = combos.len();
1149    let cols = data.len();
1150    let total =
1151        rows.checked_mul(cols)
1152            .ok_or_else(|| GoertzelCycleCompositeWaveError::InvalidInput {
1153                msg: "goertzel_cycle_composite_wave: rows*cols overflow in batch".to_string(),
1154            })?;
1155
1156    if data.is_empty() {
1157        return Err(GoertzelCycleCompositeWaveError::EmptyInputData);
1158    }
1159    let max_run = longest_valid_run(data);
1160    if max_run == 0 {
1161        return Err(GoertzelCycleCompositeWaveError::AllValuesNaN);
1162    }
1163    let mut max_needed = 0usize;
1164    let warmups: Vec<usize> = combos
1165        .iter()
1166        .map(|params| {
1167            let needed = sample_size_for_params(params);
1168            max_needed = max_needed.max(needed);
1169            needed.saturating_sub(1)
1170        })
1171        .collect();
1172    if max_run < max_needed {
1173        return Err(GoertzelCycleCompositeWaveError::NotEnoughValidData {
1174            needed: max_needed,
1175            valid: max_run,
1176        });
1177    }
1178
1179    let mut values_mu = make_uninit_matrix(rows, cols);
1180    init_matrix_prefixes(&mut values_mu, cols, &warmups);
1181    let mut values = unsafe {
1182        Vec::from_raw_parts(
1183            values_mu.as_mut_ptr() as *mut f64,
1184            values_mu.len(),
1185            values_mu.capacity(),
1186        )
1187    };
1188    std::mem::forget(values_mu);
1189    debug_assert_eq!(values.len(), total);
1190
1191    goertzel_cycle_composite_wave_batch_inner_into(data, sweep, kernel, parallel, &mut values)?;
1192
1193    Ok(GoertzelCycleCompositeWaveBatchOutput {
1194        values,
1195        combos,
1196        rows,
1197        cols,
1198    })
1199}
1200
1201fn goertzel_cycle_composite_wave_batch_inner_into(
1202    data: &[f64],
1203    sweep: &GoertzelCycleCompositeWaveBatchRange,
1204    kernel: Kernel,
1205    parallel: bool,
1206    out: &mut [f64],
1207) -> Result<Vec<GoertzelCycleCompositeWaveParams>, GoertzelCycleCompositeWaveError> {
1208    match kernel {
1209        Kernel::Auto
1210        | Kernel::Scalar
1211        | Kernel::ScalarBatch
1212        | Kernel::Avx2
1213        | Kernel::Avx2Batch
1214        | Kernel::Avx512
1215        | Kernel::Avx512Batch => {}
1216        other => {
1217            return Err(GoertzelCycleCompositeWaveError::InvalidKernelForBatch(
1218                other,
1219            ))
1220        }
1221    }
1222
1223    let combos = expand_grid_checked(sweep)?;
1224    let len = data.len();
1225    if len == 0 {
1226        return Err(GoertzelCycleCompositeWaveError::EmptyInputData);
1227    }
1228    let total = combos.len().checked_mul(len).ok_or_else(|| {
1229        GoertzelCycleCompositeWaveError::InvalidInput {
1230            msg: "goertzel_cycle_composite_wave: rows*cols overflow in batch_into".to_string(),
1231        }
1232    })?;
1233    if out.len() != total {
1234        return Err(GoertzelCycleCompositeWaveError::MismatchedOutputLen {
1235            dst_len: out.len(),
1236            expected_len: total,
1237        });
1238    }
1239
1240    let max_run = longest_valid_run(data);
1241    if max_run == 0 {
1242        return Err(GoertzelCycleCompositeWaveError::AllValuesNaN);
1243    }
1244    let max_needed = combos.iter().map(sample_size_for_params).max().unwrap_or(0);
1245    if max_run < max_needed {
1246        return Err(GoertzelCycleCompositeWaveError::NotEnoughValidData {
1247            needed: max_needed,
1248            valid: max_run,
1249        });
1250    }
1251
1252    let _chosen = match kernel {
1253        Kernel::Auto => detect_best_batch_kernel(),
1254        other => other,
1255    };
1256
1257    let worker = |row: usize, dst: &mut [f64]| compute_row(data, &combos[row], dst);
1258
1259    if parallel && combos.len() > 1 {
1260        #[cfg(not(target_arch = "wasm32"))]
1261        {
1262            out.par_chunks_mut(len)
1263                .enumerate()
1264                .for_each(|(row, dst)| worker(row, dst));
1265        }
1266        #[cfg(target_arch = "wasm32")]
1267        {
1268            for (row, dst) in out.chunks_mut(len).enumerate() {
1269                worker(row, dst);
1270            }
1271        }
1272    } else {
1273        for (row, dst) in out.chunks_mut(len).enumerate() {
1274            worker(row, dst);
1275        }
1276    }
1277
1278    Ok(combos)
1279}
1280
1281#[cfg(feature = "python")]
1282fn parse_mode_py(value: &str) -> PyResult<GoertzelDetrendMode> {
1283    GoertzelDetrendMode::parse(value)
1284        .ok_or_else(|| PyValueError::new_err(format!("Invalid detrend mode: {value}")))
1285}
1286
1287#[cfg(feature = "python")]
1288fn build_params_py(
1289    max_period: usize,
1290    start_at_cycle: usize,
1291    use_top_cycles: usize,
1292    bar_to_calculate: usize,
1293    detrend_mode: &str,
1294    dt_zl_per1: usize,
1295    dt_zl_per2: usize,
1296    dt_hp_per1: usize,
1297    dt_hp_per2: usize,
1298    dt_reg_zl_smooth_per: usize,
1299    hp_smooth_per: usize,
1300    zlma_smooth_per: usize,
1301    filter_bartels: bool,
1302    bart_no_cycles: usize,
1303    bart_smooth_per: usize,
1304    bart_sig_limit: usize,
1305    sort_bartels: bool,
1306    squared_amp: bool,
1307    use_cosine: bool,
1308    subtract_noise: bool,
1309    use_cycle_strength: bool,
1310) -> PyResult<GoertzelCycleCompositeWaveParams> {
1311    Ok(GoertzelCycleCompositeWaveParams {
1312        max_period: Some(max_period),
1313        start_at_cycle: Some(start_at_cycle),
1314        use_top_cycles: Some(use_top_cycles),
1315        bar_to_calculate: Some(bar_to_calculate),
1316        detrend_mode: Some(parse_mode_py(detrend_mode)?),
1317        dt_zl_per1: Some(dt_zl_per1),
1318        dt_zl_per2: Some(dt_zl_per2),
1319        dt_hp_per1: Some(dt_hp_per1),
1320        dt_hp_per2: Some(dt_hp_per2),
1321        dt_reg_zl_smooth_per: Some(dt_reg_zl_smooth_per),
1322        hp_smooth_per: Some(hp_smooth_per),
1323        zlma_smooth_per: Some(zlma_smooth_per),
1324        filter_bartels: Some(filter_bartels),
1325        bart_no_cycles: Some(bart_no_cycles),
1326        bart_smooth_per: Some(bart_smooth_per),
1327        bart_sig_limit: Some(bart_sig_limit),
1328        sort_bartels: Some(sort_bartels),
1329        squared_amp: Some(squared_amp),
1330        use_cosine: Some(use_cosine),
1331        subtract_noise: Some(subtract_noise),
1332        use_cycle_strength: Some(use_cycle_strength),
1333    })
1334}
1335
1336#[cfg(feature = "python")]
1337#[pyfunction(name = "goertzel_cycle_composite_wave")]
1338#[pyo3(signature = (
1339    data,
1340    max_period=DEFAULT_MAX_PERIOD,
1341    start_at_cycle=DEFAULT_START_AT_CYCLE,
1342    use_top_cycles=DEFAULT_USE_TOP_CYCLES,
1343    bar_to_calculate=DEFAULT_BAR_TO_CALCULATE,
1344    detrend_mode="hodrick_prescott_detrending",
1345    dt_zl_per1=DEFAULT_DT_ZL_PER1,
1346    dt_zl_per2=DEFAULT_DT_ZL_PER2,
1347    dt_hp_per1=DEFAULT_DT_HP_PER1,
1348    dt_hp_per2=DEFAULT_DT_HP_PER2,
1349    dt_reg_zl_smooth_per=DEFAULT_DT_REG_ZL_SMOOTH_PER,
1350    hp_smooth_per=DEFAULT_HP_SMOOTH_PER,
1351    zlma_smooth_per=DEFAULT_ZLMA_SMOOTH_PER,
1352    filter_bartels=false,
1353    bart_no_cycles=DEFAULT_BART_NO_CYCLES,
1354    bart_smooth_per=DEFAULT_BART_SMOOTH_PER,
1355    bart_sig_limit=DEFAULT_BART_SIG_LIMIT,
1356    sort_bartels=false,
1357    squared_amp=true,
1358    use_cosine=true,
1359    subtract_noise=false,
1360    use_cycle_strength=true,
1361    kernel=None
1362))]
1363pub fn goertzel_cycle_composite_wave_py<'py>(
1364    py: Python<'py>,
1365    data: PyReadonlyArray1<'py, f64>,
1366    max_period: usize,
1367    start_at_cycle: usize,
1368    use_top_cycles: usize,
1369    bar_to_calculate: usize,
1370    detrend_mode: &str,
1371    dt_zl_per1: usize,
1372    dt_zl_per2: usize,
1373    dt_hp_per1: usize,
1374    dt_hp_per2: usize,
1375    dt_reg_zl_smooth_per: usize,
1376    hp_smooth_per: usize,
1377    zlma_smooth_per: usize,
1378    filter_bartels: bool,
1379    bart_no_cycles: usize,
1380    bart_smooth_per: usize,
1381    bart_sig_limit: usize,
1382    sort_bartels: bool,
1383    squared_amp: bool,
1384    use_cosine: bool,
1385    subtract_noise: bool,
1386    use_cycle_strength: bool,
1387    kernel: Option<&str>,
1388) -> PyResult<Bound<'py, PyArray1<f64>>> {
1389    let data = data.as_slice()?;
1390    let kern = validate_kernel(kernel, true)?;
1391    let params = build_params_py(
1392        max_period,
1393        start_at_cycle,
1394        use_top_cycles,
1395        bar_to_calculate,
1396        detrend_mode,
1397        dt_zl_per1,
1398        dt_zl_per2,
1399        dt_hp_per1,
1400        dt_hp_per2,
1401        dt_reg_zl_smooth_per,
1402        hp_smooth_per,
1403        zlma_smooth_per,
1404        filter_bartels,
1405        bart_no_cycles,
1406        bart_smooth_per,
1407        bart_sig_limit,
1408        sort_bartels,
1409        squared_amp,
1410        use_cosine,
1411        subtract_noise,
1412        use_cycle_strength,
1413    )?;
1414    let input = GoertzelCycleCompositeWaveInput::from_slice(data, params);
1415    let out = py
1416        .allow_threads(|| goertzel_cycle_composite_wave_with_kernel(&input, kern))
1417        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1418    Ok(out.values.into_pyarray(py))
1419}
1420
1421#[cfg(feature = "python")]
1422#[pyclass(name = "GoertzelCycleCompositeWaveStream")]
1423pub struct GoertzelCycleCompositeWaveStreamPy {
1424    stream: GoertzelCycleCompositeWaveStream,
1425}
1426
1427#[cfg(feature = "python")]
1428#[pymethods]
1429impl GoertzelCycleCompositeWaveStreamPy {
1430    #[new]
1431    #[pyo3(signature = (
1432        max_period=DEFAULT_MAX_PERIOD,
1433        start_at_cycle=DEFAULT_START_AT_CYCLE,
1434        use_top_cycles=DEFAULT_USE_TOP_CYCLES,
1435        bar_to_calculate=DEFAULT_BAR_TO_CALCULATE,
1436        detrend_mode="hodrick_prescott_detrending",
1437        dt_zl_per1=DEFAULT_DT_ZL_PER1,
1438        dt_zl_per2=DEFAULT_DT_ZL_PER2,
1439        dt_hp_per1=DEFAULT_DT_HP_PER1,
1440        dt_hp_per2=DEFAULT_DT_HP_PER2,
1441        dt_reg_zl_smooth_per=DEFAULT_DT_REG_ZL_SMOOTH_PER,
1442        hp_smooth_per=DEFAULT_HP_SMOOTH_PER,
1443        zlma_smooth_per=DEFAULT_ZLMA_SMOOTH_PER,
1444        filter_bartels=false,
1445        bart_no_cycles=DEFAULT_BART_NO_CYCLES,
1446        bart_smooth_per=DEFAULT_BART_SMOOTH_PER,
1447        bart_sig_limit=DEFAULT_BART_SIG_LIMIT,
1448        sort_bartels=false,
1449        squared_amp=true,
1450        use_cosine=true,
1451        subtract_noise=false,
1452        use_cycle_strength=true
1453    ))]
1454    fn new(
1455        max_period: usize,
1456        start_at_cycle: usize,
1457        use_top_cycles: usize,
1458        bar_to_calculate: usize,
1459        detrend_mode: &str,
1460        dt_zl_per1: usize,
1461        dt_zl_per2: usize,
1462        dt_hp_per1: usize,
1463        dt_hp_per2: usize,
1464        dt_reg_zl_smooth_per: usize,
1465        hp_smooth_per: usize,
1466        zlma_smooth_per: usize,
1467        filter_bartels: bool,
1468        bart_no_cycles: usize,
1469        bart_smooth_per: usize,
1470        bart_sig_limit: usize,
1471        sort_bartels: bool,
1472        squared_amp: bool,
1473        use_cosine: bool,
1474        subtract_noise: bool,
1475        use_cycle_strength: bool,
1476    ) -> PyResult<Self> {
1477        let params = build_params_py(
1478            max_period,
1479            start_at_cycle,
1480            use_top_cycles,
1481            bar_to_calculate,
1482            detrend_mode,
1483            dt_zl_per1,
1484            dt_zl_per2,
1485            dt_hp_per1,
1486            dt_hp_per2,
1487            dt_reg_zl_smooth_per,
1488            hp_smooth_per,
1489            zlma_smooth_per,
1490            filter_bartels,
1491            bart_no_cycles,
1492            bart_smooth_per,
1493            bart_sig_limit,
1494            sort_bartels,
1495            squared_amp,
1496            use_cosine,
1497            subtract_noise,
1498            use_cycle_strength,
1499        )?;
1500        let stream = GoertzelCycleCompositeWaveStream::try_new(params)
1501            .map_err(|e| PyValueError::new_err(e.to_string()))?;
1502        Ok(Self { stream })
1503    }
1504
1505    fn update(&mut self, value: f64) -> Option<f64> {
1506        self.stream.update(value)
1507    }
1508
1509    fn reset(&mut self) {
1510        self.stream.reset();
1511    }
1512}
1513
1514#[cfg(feature = "python")]
1515#[pyfunction(name = "goertzel_cycle_composite_wave_batch")]
1516#[pyo3(signature = (
1517    data,
1518    max_period_range=(DEFAULT_MAX_PERIOD, DEFAULT_MAX_PERIOD, 0),
1519    start_at_cycle_range=(DEFAULT_START_AT_CYCLE, DEFAULT_START_AT_CYCLE, 0),
1520    use_top_cycles_range=(DEFAULT_USE_TOP_CYCLES, DEFAULT_USE_TOP_CYCLES, 0),
1521    bar_to_calculate=DEFAULT_BAR_TO_CALCULATE,
1522    detrend_mode="hodrick_prescott_detrending",
1523    dt_zl_per1=DEFAULT_DT_ZL_PER1,
1524    dt_zl_per2=DEFAULT_DT_ZL_PER2,
1525    dt_hp_per1=DEFAULT_DT_HP_PER1,
1526    dt_hp_per2=DEFAULT_DT_HP_PER2,
1527    dt_reg_zl_smooth_per=DEFAULT_DT_REG_ZL_SMOOTH_PER,
1528    hp_smooth_per=DEFAULT_HP_SMOOTH_PER,
1529    zlma_smooth_per=DEFAULT_ZLMA_SMOOTH_PER,
1530    filter_bartels=false,
1531    bart_no_cycles=DEFAULT_BART_NO_CYCLES,
1532    bart_smooth_per=DEFAULT_BART_SMOOTH_PER,
1533    bart_sig_limit=DEFAULT_BART_SIG_LIMIT,
1534    sort_bartels=false,
1535    squared_amp=true,
1536    use_cosine=true,
1537    subtract_noise=false,
1538    use_cycle_strength=true,
1539    kernel=None
1540))]
1541pub fn goertzel_cycle_composite_wave_batch_py<'py>(
1542    py: Python<'py>,
1543    data: PyReadonlyArray1<'py, f64>,
1544    max_period_range: (usize, usize, usize),
1545    start_at_cycle_range: (usize, usize, usize),
1546    use_top_cycles_range: (usize, usize, usize),
1547    bar_to_calculate: usize,
1548    detrend_mode: &str,
1549    dt_zl_per1: usize,
1550    dt_zl_per2: usize,
1551    dt_hp_per1: usize,
1552    dt_hp_per2: usize,
1553    dt_reg_zl_smooth_per: usize,
1554    hp_smooth_per: usize,
1555    zlma_smooth_per: usize,
1556    filter_bartels: bool,
1557    bart_no_cycles: usize,
1558    bart_smooth_per: usize,
1559    bart_sig_limit: usize,
1560    sort_bartels: bool,
1561    squared_amp: bool,
1562    use_cosine: bool,
1563    subtract_noise: bool,
1564    use_cycle_strength: bool,
1565    kernel: Option<&str>,
1566) -> PyResult<Bound<'py, PyDict>> {
1567    let data = data.as_slice()?;
1568    let kern = validate_kernel(kernel, true)?;
1569    let base_params = build_params_py(
1570        DEFAULT_MAX_PERIOD,
1571        DEFAULT_START_AT_CYCLE,
1572        DEFAULT_USE_TOP_CYCLES,
1573        bar_to_calculate,
1574        detrend_mode,
1575        dt_zl_per1,
1576        dt_zl_per2,
1577        dt_hp_per1,
1578        dt_hp_per2,
1579        dt_reg_zl_smooth_per,
1580        hp_smooth_per,
1581        zlma_smooth_per,
1582        filter_bartels,
1583        bart_no_cycles,
1584        bart_smooth_per,
1585        bart_sig_limit,
1586        sort_bartels,
1587        squared_amp,
1588        use_cosine,
1589        subtract_noise,
1590        use_cycle_strength,
1591    )?;
1592    let output = py
1593        .allow_threads(|| {
1594            goertzel_cycle_composite_wave_batch_with_kernel(
1595                data,
1596                &GoertzelCycleCompositeWaveBatchRange {
1597                    max_period: max_period_range,
1598                    start_at_cycle: start_at_cycle_range,
1599                    use_top_cycles: use_top_cycles_range,
1600                    base_params,
1601                },
1602                kern,
1603            )
1604        })
1605        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1606
1607    let dict = PyDict::new(py);
1608    dict.set_item(
1609        "values",
1610        output
1611            .values
1612            .into_pyarray(py)
1613            .reshape((output.rows, output.cols))?,
1614    )?;
1615    dict.set_item(
1616        "max_periods",
1617        output
1618            .combos
1619            .iter()
1620            .map(|params| params.max_period.unwrap_or(DEFAULT_MAX_PERIOD) as u64)
1621            .collect::<Vec<_>>()
1622            .into_pyarray(py),
1623    )?;
1624    dict.set_item(
1625        "start_at_cycles",
1626        output
1627            .combos
1628            .iter()
1629            .map(|params| params.start_at_cycle.unwrap_or(DEFAULT_START_AT_CYCLE) as u64)
1630            .collect::<Vec<_>>()
1631            .into_pyarray(py),
1632    )?;
1633    dict.set_item(
1634        "use_top_cycles",
1635        output
1636            .combos
1637            .iter()
1638            .map(|params| params.use_top_cycles.unwrap_or(DEFAULT_USE_TOP_CYCLES) as u64)
1639            .collect::<Vec<_>>()
1640            .into_pyarray(py),
1641    )?;
1642    dict.set_item("rows", output.rows)?;
1643    dict.set_item("cols", output.cols)?;
1644    Ok(dict)
1645}
1646
1647#[cfg(feature = "python")]
1648pub fn register_goertzel_cycle_composite_wave_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
1649    m.add_function(wrap_pyfunction!(goertzel_cycle_composite_wave_py, m)?)?;
1650    m.add_function(wrap_pyfunction!(goertzel_cycle_composite_wave_batch_py, m)?)?;
1651    m.add_class::<GoertzelCycleCompositeWaveStreamPy>()?;
1652    Ok(())
1653}
1654
1655#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1656#[derive(Debug, Clone, Serialize, Deserialize)]
1657pub struct GoertzelCycleCompositeWaveJsConfig {
1658    pub max_period: Option<usize>,
1659    pub start_at_cycle: Option<usize>,
1660    pub use_top_cycles: Option<usize>,
1661    pub bar_to_calculate: Option<usize>,
1662    pub detrend_mode: Option<GoertzelDetrendMode>,
1663    pub dt_zl_per1: Option<usize>,
1664    pub dt_zl_per2: Option<usize>,
1665    pub dt_hp_per1: Option<usize>,
1666    pub dt_hp_per2: Option<usize>,
1667    pub dt_reg_zl_smooth_per: Option<usize>,
1668    pub hp_smooth_per: Option<usize>,
1669    pub zlma_smooth_per: Option<usize>,
1670    pub filter_bartels: Option<bool>,
1671    pub bart_no_cycles: Option<usize>,
1672    pub bart_smooth_per: Option<usize>,
1673    pub bart_sig_limit: Option<usize>,
1674    pub sort_bartels: Option<bool>,
1675    pub squared_amp: Option<bool>,
1676    pub use_cosine: Option<bool>,
1677    pub subtract_noise: Option<bool>,
1678    pub use_cycle_strength: Option<bool>,
1679}
1680
1681#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1682impl Default for GoertzelCycleCompositeWaveJsConfig {
1683    fn default() -> Self {
1684        let params = GoertzelCycleCompositeWaveParams::default();
1685        Self {
1686            max_period: params.max_period,
1687            start_at_cycle: params.start_at_cycle,
1688            use_top_cycles: params.use_top_cycles,
1689            bar_to_calculate: params.bar_to_calculate,
1690            detrend_mode: params.detrend_mode,
1691            dt_zl_per1: params.dt_zl_per1,
1692            dt_zl_per2: params.dt_zl_per2,
1693            dt_hp_per1: params.dt_hp_per1,
1694            dt_hp_per2: params.dt_hp_per2,
1695            dt_reg_zl_smooth_per: params.dt_reg_zl_smooth_per,
1696            hp_smooth_per: params.hp_smooth_per,
1697            zlma_smooth_per: params.zlma_smooth_per,
1698            filter_bartels: params.filter_bartels,
1699            bart_no_cycles: params.bart_no_cycles,
1700            bart_smooth_per: params.bart_smooth_per,
1701            bart_sig_limit: params.bart_sig_limit,
1702            sort_bartels: params.sort_bartels,
1703            squared_amp: params.squared_amp,
1704            use_cosine: params.use_cosine,
1705            subtract_noise: params.subtract_noise,
1706            use_cycle_strength: params.use_cycle_strength,
1707        }
1708    }
1709}
1710
1711#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1712impl From<GoertzelCycleCompositeWaveJsConfig> for GoertzelCycleCompositeWaveParams {
1713    fn from(value: GoertzelCycleCompositeWaveJsConfig) -> Self {
1714        Self {
1715            max_period: value.max_period,
1716            start_at_cycle: value.start_at_cycle,
1717            use_top_cycles: value.use_top_cycles,
1718            bar_to_calculate: value.bar_to_calculate,
1719            detrend_mode: value.detrend_mode,
1720            dt_zl_per1: value.dt_zl_per1,
1721            dt_zl_per2: value.dt_zl_per2,
1722            dt_hp_per1: value.dt_hp_per1,
1723            dt_hp_per2: value.dt_hp_per2,
1724            dt_reg_zl_smooth_per: value.dt_reg_zl_smooth_per,
1725            hp_smooth_per: value.hp_smooth_per,
1726            zlma_smooth_per: value.zlma_smooth_per,
1727            filter_bartels: value.filter_bartels,
1728            bart_no_cycles: value.bart_no_cycles,
1729            bart_smooth_per: value.bart_smooth_per,
1730            bart_sig_limit: value.bart_sig_limit,
1731            sort_bartels: value.sort_bartels,
1732            squared_amp: value.squared_amp,
1733            use_cosine: value.use_cosine,
1734            subtract_noise: value.subtract_noise,
1735            use_cycle_strength: value.use_cycle_strength,
1736        }
1737    }
1738}
1739
1740#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1741#[derive(Debug, Clone, Serialize, Deserialize)]
1742pub struct GoertzelCycleCompositeWaveBatchConfig {
1743    pub max_period_range: Vec<usize>,
1744    pub start_at_cycle_range: Vec<usize>,
1745    pub use_top_cycles_range: Vec<usize>,
1746    #[serde(flatten)]
1747    pub base: GoertzelCycleCompositeWaveJsConfig,
1748}
1749
1750#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1751#[derive(Debug, Clone, Serialize, Deserialize)]
1752pub struct GoertzelCycleCompositeWaveBatchJsOutput {
1753    pub values: Vec<f64>,
1754    pub combos: Vec<GoertzelCycleCompositeWaveParams>,
1755    pub rows: usize,
1756    pub cols: usize,
1757}
1758
1759#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1760#[wasm_bindgen]
1761pub fn goertzel_cycle_composite_wave_js(
1762    data: &[f64],
1763    config: JsValue,
1764) -> Result<Vec<f64>, JsValue> {
1765    let config: GoertzelCycleCompositeWaveJsConfig = if config.is_undefined() || config.is_null() {
1766        GoertzelCycleCompositeWaveJsConfig::default()
1767    } else {
1768        serde_wasm_bindgen::from_value(config)
1769            .map_err(|e| JsValue::from_str(&format!("Invalid config: {e}")))?
1770    };
1771    let input = GoertzelCycleCompositeWaveInput::from_slice(data, config.into());
1772    let out = goertzel_cycle_composite_wave_with_kernel(&input, Kernel::Auto)
1773        .map_err(|e| JsValue::from_str(&e.to_string()))?;
1774    Ok(out.values)
1775}
1776
1777#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1778#[wasm_bindgen]
1779pub fn goertzel_cycle_composite_wave_batch_js(
1780    data: &[f64],
1781    config: JsValue,
1782) -> Result<JsValue, JsValue> {
1783    let config: GoertzelCycleCompositeWaveBatchConfig = serde_wasm_bindgen::from_value(config)
1784        .map_err(|e| JsValue::from_str(&format!("Invalid config: {e}")))?;
1785    if config.max_period_range.len() != 3
1786        || config.start_at_cycle_range.len() != 3
1787        || config.use_top_cycles_range.len() != 3
1788    {
1789        return Err(JsValue::from_str(
1790            "Invalid config: ranges must have exactly 3 elements [start, end, step]",
1791        ));
1792    }
1793    let out = goertzel_cycle_composite_wave_batch_with_kernel(
1794        data,
1795        &GoertzelCycleCompositeWaveBatchRange {
1796            max_period: (
1797                config.max_period_range[0],
1798                config.max_period_range[1],
1799                config.max_period_range[2],
1800            ),
1801            start_at_cycle: (
1802                config.start_at_cycle_range[0],
1803                config.start_at_cycle_range[1],
1804                config.start_at_cycle_range[2],
1805            ),
1806            use_top_cycles: (
1807                config.use_top_cycles_range[0],
1808                config.use_top_cycles_range[1],
1809                config.use_top_cycles_range[2],
1810            ),
1811            base_params: config.base.into(),
1812        },
1813        Kernel::Auto,
1814    )
1815    .map_err(|e| JsValue::from_str(&e.to_string()))?;
1816
1817    serde_wasm_bindgen::to_value(&GoertzelCycleCompositeWaveBatchJsOutput {
1818        values: out.values,
1819        combos: out.combos,
1820        rows: out.rows,
1821        cols: out.cols,
1822    })
1823    .map_err(|e| JsValue::from_str(&format!("Serialization error: {e}")))
1824}
1825
1826#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1827#[wasm_bindgen]
1828pub fn goertzel_cycle_composite_wave_alloc(len: usize) -> *mut f64 {
1829    let mut vec = Vec::<f64>::with_capacity(len);
1830    let ptr = vec.as_mut_ptr();
1831    std::mem::forget(vec);
1832    ptr
1833}
1834
1835#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1836#[wasm_bindgen]
1837pub fn goertzel_cycle_composite_wave_free(ptr: *mut f64, len: usize) {
1838    if !ptr.is_null() {
1839        unsafe {
1840            let _ = Vec::from_raw_parts(ptr, len, len);
1841        }
1842    }
1843}
1844
1845#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1846#[wasm_bindgen]
1847pub fn goertzel_cycle_composite_wave_into(
1848    in_ptr: *const f64,
1849    out_ptr: *mut f64,
1850    len: usize,
1851    max_period: usize,
1852    start_at_cycle: usize,
1853    use_top_cycles: usize,
1854    bar_to_calculate: usize,
1855) -> Result<(), JsValue> {
1856    if in_ptr.is_null() || out_ptr.is_null() {
1857        return Err(JsValue::from_str(
1858            "null pointer passed to goertzel_cycle_composite_wave_into",
1859        ));
1860    }
1861    unsafe {
1862        let data = std::slice::from_raw_parts(in_ptr, len);
1863        let input = GoertzelCycleCompositeWaveInput::from_slice(
1864            data,
1865            GoertzelCycleCompositeWaveParams {
1866                max_period: Some(max_period),
1867                start_at_cycle: Some(start_at_cycle),
1868                use_top_cycles: Some(use_top_cycles),
1869                bar_to_calculate: Some(bar_to_calculate),
1870                ..GoertzelCycleCompositeWaveParams::default()
1871            },
1872        );
1873        goertzel_cycle_composite_wave_into_slice(
1874            std::slice::from_raw_parts_mut(out_ptr, len),
1875            &input,
1876            Kernel::Scalar,
1877        )
1878        .map_err(|e| JsValue::from_str(&e.to_string()))
1879    }
1880}
1881
1882#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1883#[wasm_bindgen]
1884pub fn goertzel_cycle_composite_wave_batch_into(
1885    in_ptr: *const f64,
1886    out_ptr: *mut f64,
1887    len: usize,
1888    max_period_start: usize,
1889    max_period_end: usize,
1890    max_period_step: usize,
1891    start_at_cycle_start: usize,
1892    start_at_cycle_end: usize,
1893    start_at_cycle_step: usize,
1894    use_top_cycles_start: usize,
1895    use_top_cycles_end: usize,
1896    use_top_cycles_step: usize,
1897) -> Result<usize, JsValue> {
1898    if in_ptr.is_null() || out_ptr.is_null() {
1899        return Err(JsValue::from_str(
1900            "null pointer passed to goertzel_cycle_composite_wave_batch_into",
1901        ));
1902    }
1903    let sweep = GoertzelCycleCompositeWaveBatchRange {
1904        max_period: (max_period_start, max_period_end, max_period_step),
1905        start_at_cycle: (
1906            start_at_cycle_start,
1907            start_at_cycle_end,
1908            start_at_cycle_step,
1909        ),
1910        use_top_cycles: (
1911            use_top_cycles_start,
1912            use_top_cycles_end,
1913            use_top_cycles_step,
1914        ),
1915        base_params: GoertzelCycleCompositeWaveParams::default(),
1916    };
1917    let combos = expand_grid_checked(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
1918    let rows = combos.len();
1919    unsafe {
1920        let data = std::slice::from_raw_parts(in_ptr, len);
1921        goertzel_cycle_composite_wave_batch_inner_into(
1922            data,
1923            &sweep,
1924            Kernel::ScalarBatch,
1925            false,
1926            std::slice::from_raw_parts_mut(out_ptr, rows * len),
1927        )
1928        .map_err(|e| JsValue::from_str(&e.to_string()))?;
1929    }
1930    Ok(rows)
1931}
1932
1933#[cfg(test)]
1934mod tests {
1935    use super::*;
1936    use crate::indicators::dispatch::{
1937        compute_cpu, IndicatorComputeRequest, IndicatorDataRef, ParamKV, ParamValue,
1938    };
1939
1940    fn sample_data(len: usize) -> Vec<f64> {
1941        (0..len)
1942            .map(|i| {
1943                let x = i as f64;
1944                100.0
1945                    + (2.0 * std::f64::consts::PI * x / 28.0).sin() * 2.0
1946                    + (2.0 * std::f64::consts::PI * x / 14.0).cos() * 0.8
1947                    + x * 0.01
1948            })
1949            .collect()
1950    }
1951
1952    #[test]
1953    fn goertzel_cycle_composite_wave_produces_finite_tail() -> Result<(), Box<dyn Error>> {
1954        let data = sample_data(320);
1955        let out = goertzel_cycle_composite_wave(&GoertzelCycleCompositeWaveInput::from_slice(
1956            &data,
1957            GoertzelCycleCompositeWaveParams {
1958                max_period: Some(32),
1959                ..GoertzelCycleCompositeWaveParams::default()
1960            },
1961        ))?;
1962        assert!(out.values.iter().filter(|v| v.is_finite()).count() > 16);
1963        Ok(())
1964    }
1965
1966    #[test]
1967    fn goertzel_cycle_composite_wave_into_matches_api() -> Result<(), Box<dyn Error>> {
1968        let data = sample_data(420);
1969        let params = GoertzelCycleCompositeWaveParams {
1970            max_period: Some(32),
1971            ..GoertzelCycleCompositeWaveParams::default()
1972        };
1973        let input = GoertzelCycleCompositeWaveInput::from_slice(&data, params);
1974        let baseline = goertzel_cycle_composite_wave(&input)?.values;
1975        let mut out = vec![0.0; data.len()];
1976        goertzel_cycle_composite_wave_into_slice(&mut out, &input, Kernel::Auto)?;
1977        for (a, b) in baseline.iter().zip(out.iter()) {
1978            assert!((a.is_nan() && b.is_nan()) || (*a - *b).abs() <= 1e-12);
1979        }
1980        Ok(())
1981    }
1982
1983    #[test]
1984    fn goertzel_cycle_composite_wave_stream_matches_batch() -> Result<(), Box<dyn Error>> {
1985        let data = sample_data(420);
1986        let params = GoertzelCycleCompositeWaveParams {
1987            max_period: Some(32),
1988            ..GoertzelCycleCompositeWaveParams::default()
1989        };
1990        let batch = goertzel_cycle_composite_wave(&GoertzelCycleCompositeWaveInput::from_slice(
1991            &data, params,
1992        ))?
1993        .values;
1994        let mut stream = GoertzelCycleCompositeWaveStream::try_new(params)?;
1995        let mut stream_values = Vec::with_capacity(data.len());
1996        for value in data {
1997            stream_values.push(stream.update(value).unwrap_or(f64::NAN));
1998        }
1999        for (a, b) in batch.iter().zip(stream_values.iter()) {
2000            assert!((a.is_nan() && b.is_nan()) || (*a - *b).abs() <= 1e-12);
2001        }
2002        Ok(())
2003    }
2004
2005    #[test]
2006    fn goertzel_cycle_composite_wave_batch_single_matches_single() -> Result<(), Box<dyn Error>> {
2007        let data = sample_data(420);
2008        let params = GoertzelCycleCompositeWaveParams {
2009            max_period: Some(32),
2010            ..GoertzelCycleCompositeWaveParams::default()
2011        };
2012        let single = goertzel_cycle_composite_wave(&GoertzelCycleCompositeWaveInput::from_slice(
2013            &data, params,
2014        ))?
2015        .values;
2016        let batch = goertzel_cycle_composite_wave_batch_with_kernel(
2017            &data,
2018            &GoertzelCycleCompositeWaveBatchRange {
2019                max_period: (32, 32, 0),
2020                start_at_cycle: (DEFAULT_START_AT_CYCLE, DEFAULT_START_AT_CYCLE, 0),
2021                use_top_cycles: (DEFAULT_USE_TOP_CYCLES, DEFAULT_USE_TOP_CYCLES, 0),
2022                base_params: params,
2023            },
2024            Kernel::Auto,
2025        )?;
2026        assert_eq!(batch.rows, 1);
2027        assert_eq!(batch.cols, data.len());
2028        let row = batch.values_for(&params).unwrap();
2029        for (a, b) in row.iter().zip(single.iter()) {
2030            assert!((a.is_nan() && b.is_nan()) || (*a - *b).abs() <= 1e-12);
2031        }
2032        Ok(())
2033    }
2034
2035    #[test]
2036    fn goertzel_cycle_composite_wave_rejects_invalid_params() {
2037        let data = sample_data(128);
2038        let err = goertzel_cycle_composite_wave(&GoertzelCycleCompositeWaveInput::from_slice(
2039            &data,
2040            GoertzelCycleCompositeWaveParams {
2041                max_period: Some(1),
2042                ..GoertzelCycleCompositeWaveParams::default()
2043            },
2044        ))
2045        .unwrap_err();
2046        assert!(matches!(
2047            err,
2048            GoertzelCycleCompositeWaveError::InvalidParameter {
2049                name: "max_period",
2050                ..
2051            }
2052        ));
2053    }
2054
2055    #[test]
2056    fn goertzel_cycle_composite_wave_dispatch_compute_returns_value() -> Result<(), Box<dyn Error>>
2057    {
2058        let data = sample_data(420);
2059        let params = [ParamKV {
2060            key: "max_period",
2061            value: ParamValue::Int(32),
2062        }];
2063        let out = compute_cpu(IndicatorComputeRequest {
2064            indicator_id: "goertzel_cycle_composite_wave",
2065            data: IndicatorDataRef::Slice { values: &data },
2066            params: &params,
2067            output_id: Some("wave"),
2068            kernel: Kernel::Auto,
2069        })?;
2070        assert_eq!(out.output_id, "wave");
2071        assert_eq!(out.rows, 1);
2072        assert_eq!(out.cols, data.len());
2073        Ok(())
2074    }
2075}