Skip to main content

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