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(¶ms)?;
846 let sample_size = sample_size_for_params(¶ms);
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(¶ms).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: ¶ms,
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}