Skip to main content

vector_ta/indicators/
impulse_macd.rs

1#[cfg(feature = "python")]
2use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
3#[cfg(feature = "python")]
4use pyo3::exceptions::PyValueError;
5#[cfg(feature = "python")]
6use pyo3::prelude::*;
7#[cfg(feature = "python")]
8use pyo3::types::PyDict;
9
10#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
11use serde::{Deserialize, Serialize};
12#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
13use wasm_bindgen::prelude::*;
14
15use crate::utilities::data_loader::Candles;
16use crate::utilities::enums::Kernel;
17use crate::utilities::helpers::{
18    alloc_with_nan_prefix, detect_best_batch_kernel, init_matrix_prefixes, make_uninit_matrix,
19};
20#[cfg(feature = "python")]
21use crate::utilities::kernel_validation::validate_kernel;
22
23#[cfg(not(target_arch = "wasm32"))]
24use rayon::prelude::*;
25use std::mem::{ManuallyDrop, MaybeUninit};
26use thiserror::Error;
27
28#[derive(Debug, Clone)]
29pub enum ImpulseMacdData<'a> {
30    Candles {
31        candles: &'a Candles,
32    },
33    Slices {
34        high: &'a [f64],
35        low: &'a [f64],
36        close: &'a [f64],
37    },
38}
39
40#[derive(Debug, Clone)]
41pub struct ImpulseMacdOutput {
42    pub impulse_macd: Vec<f64>,
43    pub impulse_histo: Vec<f64>,
44    pub signal: Vec<f64>,
45}
46
47#[derive(Debug, Clone)]
48#[cfg_attr(
49    all(target_arch = "wasm32", feature = "wasm"),
50    derive(Serialize, Deserialize)
51)]
52pub struct ImpulseMacdParams {
53    pub length_ma: Option<usize>,
54    pub length_signal: Option<usize>,
55}
56
57impl Default for ImpulseMacdParams {
58    fn default() -> Self {
59        Self {
60            length_ma: Some(34),
61            length_signal: Some(9),
62        }
63    }
64}
65
66#[derive(Debug, Clone)]
67pub struct ImpulseMacdInput<'a> {
68    pub data: ImpulseMacdData<'a>,
69    pub params: ImpulseMacdParams,
70}
71
72impl<'a> ImpulseMacdInput<'a> {
73    #[inline]
74    pub fn from_candles(candles: &'a Candles, params: ImpulseMacdParams) -> Self {
75        Self {
76            data: ImpulseMacdData::Candles { candles },
77            params,
78        }
79    }
80
81    #[inline]
82    pub fn from_slices(
83        high: &'a [f64],
84        low: &'a [f64],
85        close: &'a [f64],
86        params: ImpulseMacdParams,
87    ) -> Self {
88        Self {
89            data: ImpulseMacdData::Slices { high, low, close },
90            params,
91        }
92    }
93
94    #[inline]
95    pub fn with_default_candles(candles: &'a Candles) -> Self {
96        Self::from_candles(candles, ImpulseMacdParams::default())
97    }
98
99    #[inline]
100    pub fn get_length_ma(&self) -> usize {
101        self.params.length_ma.unwrap_or(34)
102    }
103
104    #[inline]
105    pub fn get_length_signal(&self) -> usize {
106        self.params.length_signal.unwrap_or(9)
107    }
108
109    #[inline]
110    pub fn as_refs(&'a self) -> (&'a [f64], &'a [f64], &'a [f64]) {
111        match &self.data {
112            ImpulseMacdData::Candles { candles } => (
113                candles.high.as_slice(),
114                candles.low.as_slice(),
115                candles.close.as_slice(),
116            ),
117            ImpulseMacdData::Slices { high, low, close } => (*high, *low, *close),
118        }
119    }
120}
121
122#[derive(Copy, Clone, Debug)]
123pub struct ImpulseMacdBuilder {
124    length_ma: Option<usize>,
125    length_signal: Option<usize>,
126    kernel: Kernel,
127}
128
129impl Default for ImpulseMacdBuilder {
130    fn default() -> Self {
131        Self {
132            length_ma: None,
133            length_signal: None,
134            kernel: Kernel::Auto,
135        }
136    }
137}
138
139impl ImpulseMacdBuilder {
140    #[inline(always)]
141    pub fn new() -> Self {
142        Self::default()
143    }
144
145    #[inline(always)]
146    pub fn length_ma(mut self, value: usize) -> Self {
147        self.length_ma = Some(value);
148        self
149    }
150
151    #[inline(always)]
152    pub fn length_signal(mut self, value: usize) -> Self {
153        self.length_signal = Some(value);
154        self
155    }
156
157    #[inline(always)]
158    pub fn kernel(mut self, value: Kernel) -> Self {
159        self.kernel = value;
160        self
161    }
162
163    #[inline(always)]
164    pub fn apply(self, candles: &Candles) -> Result<ImpulseMacdOutput, ImpulseMacdError> {
165        let input = ImpulseMacdInput::from_candles(
166            candles,
167            ImpulseMacdParams {
168                length_ma: self.length_ma,
169                length_signal: self.length_signal,
170            },
171        );
172        impulse_macd_with_kernel(&input, self.kernel)
173    }
174
175    #[inline(always)]
176    pub fn apply_slices(
177        self,
178        high: &[f64],
179        low: &[f64],
180        close: &[f64],
181    ) -> Result<ImpulseMacdOutput, ImpulseMacdError> {
182        let input = ImpulseMacdInput::from_slices(
183            high,
184            low,
185            close,
186            ImpulseMacdParams {
187                length_ma: self.length_ma,
188                length_signal: self.length_signal,
189            },
190        );
191        impulse_macd_with_kernel(&input, self.kernel)
192    }
193
194    #[inline(always)]
195    pub fn into_stream(self) -> Result<ImpulseMacdStream, ImpulseMacdError> {
196        ImpulseMacdStream::try_new(ImpulseMacdParams {
197            length_ma: self.length_ma,
198            length_signal: self.length_signal,
199        })
200    }
201}
202
203#[derive(Debug, Error)]
204pub enum ImpulseMacdError {
205    #[error("impulse_macd: Empty input data.")]
206    EmptyInputData,
207    #[error("impulse_macd: Data length mismatch across high, low, and close.")]
208    DataLengthMismatch,
209    #[error("impulse_macd: All OHLC values are invalid.")]
210    AllValuesNaN,
211    #[error("impulse_macd: Invalid length_ma: length_ma = {length_ma}, data length = {data_len}")]
212    InvalidLengthMa { length_ma: usize, data_len: usize },
213    #[error(
214        "impulse_macd: Invalid length_signal: length_signal = {length_signal}, data length = {data_len}"
215    )]
216    InvalidLengthSignal {
217        length_signal: usize,
218        data_len: usize,
219    },
220    #[error("impulse_macd: Not enough valid data: needed = {needed}, valid = {valid}")]
221    NotEnoughValidData { needed: usize, valid: usize },
222    #[error("impulse_macd: Output length mismatch: expected = {expected}, got = {got}")]
223    OutputLengthMismatch { expected: usize, got: usize },
224    #[error("impulse_macd: Invalid range: start={start}, end={end}, step={step}")]
225    InvalidRange {
226        start: usize,
227        end: usize,
228        step: usize,
229    },
230    #[error("impulse_macd: Invalid kernel for batch: {0:?}")]
231    InvalidKernelForBatch(Kernel),
232}
233
234#[inline(always)]
235fn valid_bar(high: f64, low: f64, close: f64) -> bool {
236    high.is_finite() && low.is_finite() && close.is_finite() && high >= low
237}
238
239#[inline(always)]
240fn first_valid_bar(high: &[f64], low: &[f64], close: &[f64]) -> Option<usize> {
241    (0..close.len()).find(|&i| valid_bar(high[i], low[i], close[i]))
242}
243
244#[inline(always)]
245fn count_valid_from(high: &[f64], low: &[f64], close: &[f64], start: usize) -> usize {
246    (start..close.len())
247        .filter(|&i| valid_bar(high[i], low[i], close[i]))
248        .count()
249}
250
251#[derive(Clone, Debug)]
252struct SmmaState {
253    period: usize,
254    count: usize,
255    sum: f64,
256    value: f64,
257    ready: bool,
258}
259
260impl SmmaState {
261    #[inline(always)]
262    fn new(period: usize) -> Self {
263        Self {
264            period,
265            count: 0,
266            sum: 0.0,
267            value: f64::NAN,
268            ready: false,
269        }
270    }
271
272    #[inline(always)]
273    fn reset(&mut self) {
274        self.count = 0;
275        self.sum = 0.0;
276        self.value = f64::NAN;
277        self.ready = false;
278    }
279
280    #[inline(always)]
281    fn update(&mut self, value: f64) -> Option<f64> {
282        if self.period == 1 {
283            self.value = value;
284            self.ready = true;
285            return Some(value);
286        }
287        if !self.ready {
288            self.sum += value;
289            self.count += 1;
290            if self.count == self.period {
291                self.value = self.sum / self.period as f64;
292                self.ready = true;
293                return Some(self.value);
294            }
295            return None;
296        }
297        let p = self.period as f64;
298        self.value = (self.value * (p - 1.0) + value) / p;
299        Some(self.value)
300    }
301}
302
303#[derive(Clone, Debug)]
304struct EmaState {
305    alpha: f64,
306    value: Option<f64>,
307}
308
309impl EmaState {
310    #[inline(always)]
311    fn new(period: usize) -> Self {
312        Self {
313            alpha: 2.0 / (period as f64 + 1.0),
314            value: None,
315        }
316    }
317
318    #[inline(always)]
319    fn reset(&mut self) {
320        self.value = None;
321    }
322
323    #[inline(always)]
324    fn update(&mut self, x: f64) -> f64 {
325        let next = match self.value {
326            Some(prev) => self.alpha.mul_add(x, (1.0 - self.alpha) * prev),
327            None => x,
328        };
329        self.value = Some(next);
330        next
331    }
332}
333
334#[derive(Clone, Debug)]
335struct SmaState {
336    period: usize,
337    buf: Vec<f64>,
338    head: usize,
339    len: usize,
340    sum: f64,
341}
342
343impl SmaState {
344    #[inline(always)]
345    fn new(period: usize) -> Self {
346        Self {
347            period,
348            buf: vec![0.0; period.max(1)],
349            head: 0,
350            len: 0,
351            sum: 0.0,
352        }
353    }
354
355    #[inline(always)]
356    fn reset(&mut self) {
357        self.head = 0;
358        self.len = 0;
359        self.sum = 0.0;
360    }
361
362    #[inline(always)]
363    fn update(&mut self, value: f64) -> Option<f64> {
364        if self.period == 1 {
365            self.buf[0] = value;
366            self.len = 1;
367            self.sum = value;
368            return Some(value);
369        }
370        if self.len < self.period {
371            self.buf[self.len] = value;
372            self.len += 1;
373            self.sum += value;
374            if self.len == self.period {
375                return Some(self.sum / self.period as f64);
376            }
377            return None;
378        }
379        let old = self.buf[self.head];
380        self.buf[self.head] = value;
381        self.head += 1;
382        if self.head == self.period {
383            self.head = 0;
384        }
385        self.sum += value - old;
386        Some(self.sum / self.period as f64)
387    }
388}
389
390#[derive(Clone, Debug)]
391pub struct ImpulseMacdStream {
392    hi_smma: SmmaState,
393    lo_smma: SmmaState,
394    ema1: EmaState,
395    ema2: EmaState,
396    signal_sma: SmaState,
397}
398
399impl ImpulseMacdStream {
400    #[inline(always)]
401    fn from_parts(length_ma: usize, length_signal: usize) -> Self {
402        Self {
403            hi_smma: SmmaState::new(length_ma),
404            lo_smma: SmmaState::new(length_ma),
405            ema1: EmaState::new(length_ma),
406            ema2: EmaState::new(length_ma),
407            signal_sma: SmaState::new(length_signal),
408        }
409    }
410
411    #[inline]
412    pub fn try_new(params: ImpulseMacdParams) -> Result<Self, ImpulseMacdError> {
413        let length_ma = params.length_ma.unwrap_or(34);
414        let length_signal = params.length_signal.unwrap_or(9);
415        if length_ma == 0 {
416            return Err(ImpulseMacdError::InvalidLengthMa {
417                length_ma,
418                data_len: 0,
419            });
420        }
421        if length_signal == 0 {
422            return Err(ImpulseMacdError::InvalidLengthSignal {
423                length_signal,
424                data_len: 0,
425            });
426        }
427        Ok(Self::from_parts(length_ma, length_signal))
428    }
429
430    #[inline(always)]
431    pub fn reset(&mut self) {
432        self.hi_smma.reset();
433        self.lo_smma.reset();
434        self.ema1.reset();
435        self.ema2.reset();
436        self.signal_sma.reset();
437    }
438
439    #[inline(always)]
440    pub fn update(&mut self, high: f64, low: f64, close: f64) -> Option<(f64, f64, f64)> {
441        let src = (high + low + close) / 3.0;
442        let hi = self.hi_smma.update(high);
443        let lo = self.lo_smma.update(low);
444        let ema1 = self.ema1.update(src);
445        let ema2 = self.ema2.update(ema1);
446        let mi = ema1 + (ema1 - ema2);
447        let md = match (hi, lo) {
448            (Some(hi), Some(lo)) if mi > hi => mi - hi,
449            (Some(_), Some(lo)) if mi < lo => mi - lo,
450            _ => 0.0,
451        };
452        let signal = self.signal_sma.update(md);
453        let signal_value = signal.unwrap_or(f64::NAN);
454        let hist = if signal_value.is_finite() {
455            md - signal_value
456        } else {
457            f64::NAN
458        };
459        Some((md, hist, signal_value))
460    }
461
462    #[inline(always)]
463    pub fn update_reset_on_nan(
464        &mut self,
465        high: f64,
466        low: f64,
467        close: f64,
468    ) -> Option<(f64, f64, f64)> {
469        if !valid_bar(high, low, close) {
470            self.reset();
471            return None;
472        }
473        self.update(high, low, close)
474    }
475}
476
477#[inline(always)]
478fn impulse_macd_warmup(first: usize) -> usize {
479    first
480}
481
482#[inline(always)]
483fn signal_warmup(first: usize, length_signal: usize) -> usize {
484    first + length_signal - 1
485}
486
487#[inline(always)]
488fn impulse_macd_prepare<'a>(
489    input: &'a ImpulseMacdInput,
490) -> Result<(&'a [f64], &'a [f64], &'a [f64], usize, usize, usize), ImpulseMacdError> {
491    let (high, low, close) = input.as_refs();
492    let data_len = close.len();
493    if data_len == 0 {
494        return Err(ImpulseMacdError::EmptyInputData);
495    }
496    if high.len() != data_len || low.len() != data_len {
497        return Err(ImpulseMacdError::DataLengthMismatch);
498    }
499
500    let length_ma = input.get_length_ma();
501    if length_ma == 0 || length_ma > data_len {
502        return Err(ImpulseMacdError::InvalidLengthMa {
503            length_ma,
504            data_len,
505        });
506    }
507
508    let length_signal = input.get_length_signal();
509    if length_signal == 0 || length_signal > data_len {
510        return Err(ImpulseMacdError::InvalidLengthSignal {
511            length_signal,
512            data_len,
513        });
514    }
515
516    let first = first_valid_bar(high, low, close).ok_or(ImpulseMacdError::AllValuesNaN)?;
517    let valid = count_valid_from(high, low, close, first);
518    let needed = length_ma.max(length_signal);
519    if valid < needed {
520        return Err(ImpulseMacdError::NotEnoughValidData { needed, valid });
521    }
522
523    Ok((high, low, close, length_ma, length_signal, first))
524}
525
526#[inline(always)]
527fn impulse_macd_compute_into(
528    high: &[f64],
529    low: &[f64],
530    close: &[f64],
531    length_ma: usize,
532    length_signal: usize,
533    _kernel: Kernel,
534    out_impulse_macd: &mut [f64],
535    out_impulse_histo: &mut [f64],
536    out_signal: &mut [f64],
537) {
538    let mut stream = ImpulseMacdStream::from_parts(length_ma, length_signal);
539    for i in 0..close.len() {
540        match stream.update_reset_on_nan(high[i], low[i], close[i]) {
541            Some((md, hist, signal)) => {
542                out_impulse_macd[i] = md;
543                out_impulse_histo[i] = hist;
544                out_signal[i] = signal;
545            }
546            None => {
547                out_impulse_macd[i] = f64::NAN;
548                out_impulse_histo[i] = f64::NAN;
549                out_signal[i] = f64::NAN;
550            }
551        }
552    }
553}
554
555#[inline]
556pub fn impulse_macd(input: &ImpulseMacdInput) -> Result<ImpulseMacdOutput, ImpulseMacdError> {
557    impulse_macd_with_kernel(input, Kernel::Auto)
558}
559
560pub fn impulse_macd_with_kernel(
561    input: &ImpulseMacdInput,
562    kernel: Kernel,
563) -> Result<ImpulseMacdOutput, ImpulseMacdError> {
564    let (high, low, close, length_ma, length_signal, first) = impulse_macd_prepare(input)?;
565    let mut impulse_macd_values =
566        alloc_with_nan_prefix(close.len(), impulse_macd_warmup(first).min(close.len()));
567    let mut impulse_histo = alloc_with_nan_prefix(
568        close.len(),
569        signal_warmup(first, length_signal).min(close.len()),
570    );
571    let mut signal = alloc_with_nan_prefix(
572        close.len(),
573        signal_warmup(first, length_signal).min(close.len()),
574    );
575
576    impulse_macd_compute_into(
577        high,
578        low,
579        close,
580        length_ma,
581        length_signal,
582        kernel,
583        &mut impulse_macd_values,
584        &mut impulse_histo,
585        &mut signal,
586    );
587
588    Ok(ImpulseMacdOutput {
589        impulse_macd: impulse_macd_values,
590        impulse_histo,
591        signal,
592    })
593}
594
595#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
596#[inline]
597pub fn impulse_macd_into(
598    input: &ImpulseMacdInput,
599    out_impulse_macd: &mut [f64],
600    out_impulse_histo: &mut [f64],
601    out_signal: &mut [f64],
602) -> Result<(), ImpulseMacdError> {
603    impulse_macd_into_slice(
604        out_impulse_macd,
605        out_impulse_histo,
606        out_signal,
607        input,
608        Kernel::Auto,
609    )
610}
611
612pub fn impulse_macd_into_slice(
613    out_impulse_macd: &mut [f64],
614    out_impulse_histo: &mut [f64],
615    out_signal: &mut [f64],
616    input: &ImpulseMacdInput,
617    kernel: Kernel,
618) -> Result<(), ImpulseMacdError> {
619    let (high, low, close, length_ma, length_signal, _first) = impulse_macd_prepare(input)?;
620    if out_impulse_macd.len() != close.len()
621        || out_impulse_histo.len() != close.len()
622        || out_signal.len() != close.len()
623    {
624        return Err(ImpulseMacdError::OutputLengthMismatch {
625            expected: close.len(),
626            got: out_impulse_macd
627                .len()
628                .max(out_impulse_histo.len())
629                .max(out_signal.len()),
630        });
631    }
632
633    impulse_macd_compute_into(
634        high,
635        low,
636        close,
637        length_ma,
638        length_signal,
639        kernel,
640        out_impulse_macd,
641        out_impulse_histo,
642        out_signal,
643    );
644    Ok(())
645}
646
647#[derive(Clone, Debug)]
648pub struct ImpulseMacdBatchRange {
649    pub length_ma: (usize, usize, usize),
650    pub length_signal: (usize, usize, usize),
651}
652
653impl Default for ImpulseMacdBatchRange {
654    fn default() -> Self {
655        Self {
656            length_ma: (34, 34, 0),
657            length_signal: (9, 9, 0),
658        }
659    }
660}
661
662#[derive(Clone, Debug, Default)]
663pub struct ImpulseMacdBatchBuilder {
664    range: ImpulseMacdBatchRange,
665    kernel: Kernel,
666}
667
668impl ImpulseMacdBatchBuilder {
669    pub fn new() -> Self {
670        Self::default()
671    }
672
673    pub fn kernel(mut self, kernel: Kernel) -> Self {
674        self.kernel = kernel;
675        self
676    }
677
678    #[inline]
679    pub fn length_ma_range(mut self, start: usize, end: usize, step: usize) -> Self {
680        self.range.length_ma = (start, end, step);
681        self
682    }
683
684    #[inline]
685    pub fn length_signal_range(mut self, start: usize, end: usize, step: usize) -> Self {
686        self.range.length_signal = (start, end, step);
687        self
688    }
689
690    #[inline]
691    pub fn length_ma_static(mut self, value: usize) -> Self {
692        self.range.length_ma = (value, value, 0);
693        self
694    }
695
696    #[inline]
697    pub fn length_signal_static(mut self, value: usize) -> Self {
698        self.range.length_signal = (value, value, 0);
699        self
700    }
701
702    #[inline]
703    pub fn apply_slices(
704        self,
705        high: &[f64],
706        low: &[f64],
707        close: &[f64],
708    ) -> Result<ImpulseMacdBatchOutput, ImpulseMacdError> {
709        impulse_macd_batch_with_kernel(high, low, close, &self.range, self.kernel)
710    }
711
712    #[inline]
713    pub fn apply_candles(
714        self,
715        candles: &Candles,
716    ) -> Result<ImpulseMacdBatchOutput, ImpulseMacdError> {
717        self.apply_slices(
718            candles.high.as_slice(),
719            candles.low.as_slice(),
720            candles.close.as_slice(),
721        )
722    }
723}
724
725#[derive(Clone, Debug)]
726pub struct ImpulseMacdBatchOutput {
727    pub impulse_macd: Vec<f64>,
728    pub impulse_histo: Vec<f64>,
729    pub signal: Vec<f64>,
730    pub combos: Vec<ImpulseMacdParams>,
731    pub rows: usize,
732    pub cols: usize,
733}
734
735impl ImpulseMacdBatchOutput {
736    pub fn row_for_params(&self, params: &ImpulseMacdParams) -> Option<usize> {
737        let target_length_ma = params.length_ma.unwrap_or(34);
738        let target_length_signal = params.length_signal.unwrap_or(9);
739        self.combos.iter().position(|combo| {
740            combo.length_ma.unwrap_or(34) == target_length_ma
741                && combo.length_signal.unwrap_or(9) == target_length_signal
742        })
743    }
744}
745
746fn axis_usize(range: (usize, usize, usize)) -> Result<Vec<usize>, ImpulseMacdError> {
747    let (start, end, step) = range;
748    if start == 0 || end == 0 {
749        return Err(ImpulseMacdError::InvalidRange { start, end, step });
750    }
751    if step == 0 || start == end {
752        return Ok(vec![start]);
753    }
754
755    let mut out = Vec::new();
756    if start < end {
757        let mut value = start;
758        while value <= end {
759            out.push(value);
760            match value.checked_add(step) {
761                Some(next) if next > value => value = next,
762                _ => break,
763            }
764        }
765    } else {
766        let mut value = start;
767        while value >= end {
768            out.push(value);
769            if value < end.saturating_add(step) {
770                break;
771            }
772            value = value.saturating_sub(step);
773            if value == 0 {
774                break;
775            }
776        }
777    }
778
779    if out.is_empty() {
780        return Err(ImpulseMacdError::InvalidRange { start, end, step });
781    }
782    Ok(out)
783}
784
785pub fn expand_grid_impulse_macd(
786    sweep: &ImpulseMacdBatchRange,
787) -> Result<Vec<ImpulseMacdParams>, ImpulseMacdError> {
788    let length_mas = axis_usize(sweep.length_ma)?;
789    let length_signals = axis_usize(sweep.length_signal)?;
790    let mut out = Vec::with_capacity(length_mas.len() * length_signals.len());
791    for length_ma in length_mas {
792        for &length_signal in &length_signals {
793            out.push(ImpulseMacdParams {
794                length_ma: Some(length_ma),
795                length_signal: Some(length_signal),
796            });
797        }
798    }
799    Ok(out)
800}
801
802pub fn impulse_macd_batch_with_kernel(
803    high: &[f64],
804    low: &[f64],
805    close: &[f64],
806    sweep: &ImpulseMacdBatchRange,
807    kernel: Kernel,
808) -> Result<ImpulseMacdBatchOutput, ImpulseMacdError> {
809    let batch_kernel = match kernel {
810        Kernel::Auto => Kernel::ScalarBatch,
811        other if other.is_batch() => other,
812        other => return Err(ImpulseMacdError::InvalidKernelForBatch(other)),
813    };
814    impulse_macd_batch_impl(high, low, close, sweep, batch_kernel.to_non_batch(), true)
815}
816
817pub fn impulse_macd_batch_slice(
818    high: &[f64],
819    low: &[f64],
820    close: &[f64],
821    sweep: &ImpulseMacdBatchRange,
822) -> Result<ImpulseMacdBatchOutput, ImpulseMacdError> {
823    impulse_macd_batch_impl(high, low, close, sweep, Kernel::Scalar, false)
824}
825
826pub fn impulse_macd_batch_par_slice(
827    high: &[f64],
828    low: &[f64],
829    close: &[f64],
830    sweep: &ImpulseMacdBatchRange,
831) -> Result<ImpulseMacdBatchOutput, ImpulseMacdError> {
832    impulse_macd_batch_impl(high, low, close, sweep, Kernel::Scalar, true)
833}
834
835fn impulse_macd_batch_impl(
836    high: &[f64],
837    low: &[f64],
838    close: &[f64],
839    sweep: &ImpulseMacdBatchRange,
840    kernel: Kernel,
841    parallel: bool,
842) -> Result<ImpulseMacdBatchOutput, ImpulseMacdError> {
843    let combos = expand_grid_impulse_macd(sweep)?;
844    let rows = combos.len();
845    let cols = close.len();
846    if cols == 0 {
847        return Err(ImpulseMacdError::EmptyInputData);
848    }
849    if high.len() != cols || low.len() != cols {
850        return Err(ImpulseMacdError::DataLengthMismatch);
851    }
852
853    for params in &combos {
854        let input = ImpulseMacdInput::from_slices(high, low, close, params.clone());
855        impulse_macd_prepare(&input)?;
856    }
857
858    let first = first_valid_bar(high, low, close).unwrap_or(cols);
859    let md_warmups: Vec<usize> = combos
860        .iter()
861        .map(|_| impulse_macd_warmup(first).min(cols))
862        .collect();
863    let signal_warmups: Vec<usize> = combos
864        .iter()
865        .map(|params| signal_warmup(first, params.length_signal.unwrap_or(9)).min(cols))
866        .collect();
867
868    let mut md_matrix = make_uninit_matrix(rows, cols);
869    init_matrix_prefixes(&mut md_matrix, cols, &md_warmups);
870    let mut hist_matrix = make_uninit_matrix(rows, cols);
871    init_matrix_prefixes(&mut hist_matrix, cols, &signal_warmups);
872    let mut signal_matrix = make_uninit_matrix(rows, cols);
873    init_matrix_prefixes(&mut signal_matrix, cols, &signal_warmups);
874
875    let mut md_guard = ManuallyDrop::new(md_matrix);
876    let mut hist_guard = ManuallyDrop::new(hist_matrix);
877    let mut signal_guard = ManuallyDrop::new(signal_matrix);
878    let md_mu: &mut [MaybeUninit<f64>] =
879        unsafe { std::slice::from_raw_parts_mut(md_guard.as_mut_ptr(), md_guard.len()) };
880    let hist_mu: &mut [MaybeUninit<f64>] =
881        unsafe { std::slice::from_raw_parts_mut(hist_guard.as_mut_ptr(), hist_guard.len()) };
882    let signal_mu: &mut [MaybeUninit<f64>] =
883        unsafe { std::slice::from_raw_parts_mut(signal_guard.as_mut_ptr(), signal_guard.len()) };
884
885    let do_row = |row: usize,
886                  row_md_mu: &mut [MaybeUninit<f64>],
887                  row_hist_mu: &mut [MaybeUninit<f64>],
888                  row_signal_mu: &mut [MaybeUninit<f64>]| {
889        let params = &combos[row];
890        let dst_md = unsafe {
891            std::slice::from_raw_parts_mut(row_md_mu.as_mut_ptr() as *mut f64, row_md_mu.len())
892        };
893        let dst_hist = unsafe {
894            std::slice::from_raw_parts_mut(row_hist_mu.as_mut_ptr() as *mut f64, row_hist_mu.len())
895        };
896        let dst_signal = unsafe {
897            std::slice::from_raw_parts_mut(
898                row_signal_mu.as_mut_ptr() as *mut f64,
899                row_signal_mu.len(),
900            )
901        };
902        impulse_macd_compute_into(
903            high,
904            low,
905            close,
906            params.length_ma.unwrap_or(34),
907            params.length_signal.unwrap_or(9),
908            kernel,
909            dst_md,
910            dst_hist,
911            dst_signal,
912        );
913    };
914
915    if parallel {
916        #[cfg(not(target_arch = "wasm32"))]
917        md_mu
918            .par_chunks_mut(cols)
919            .zip(hist_mu.par_chunks_mut(cols))
920            .zip(signal_mu.par_chunks_mut(cols))
921            .enumerate()
922            .for_each(|(row, ((row_md, row_hist), row_signal))| {
923                do_row(row, row_md, row_hist, row_signal)
924            });
925        #[cfg(target_arch = "wasm32")]
926        for (row, ((row_md, row_hist), row_signal)) in md_mu
927            .chunks_mut(cols)
928            .zip(hist_mu.chunks_mut(cols))
929            .zip(signal_mu.chunks_mut(cols))
930            .enumerate()
931        {
932            do_row(row, row_md, row_hist, row_signal);
933        }
934    } else {
935        for (row, ((row_md, row_hist), row_signal)) in md_mu
936            .chunks_mut(cols)
937            .zip(hist_mu.chunks_mut(cols))
938            .zip(signal_mu.chunks_mut(cols))
939            .enumerate()
940        {
941            do_row(row, row_md, row_hist, row_signal);
942        }
943    }
944
945    let impulse_macd = unsafe {
946        Vec::from_raw_parts(
947            md_guard.as_mut_ptr() as *mut f64,
948            md_guard.len(),
949            md_guard.capacity(),
950        )
951    };
952    let impulse_histo = unsafe {
953        Vec::from_raw_parts(
954            hist_guard.as_mut_ptr() as *mut f64,
955            hist_guard.len(),
956            hist_guard.capacity(),
957        )
958    };
959    let signal = unsafe {
960        Vec::from_raw_parts(
961            signal_guard.as_mut_ptr() as *mut f64,
962            signal_guard.len(),
963            signal_guard.capacity(),
964        )
965    };
966
967    Ok(ImpulseMacdBatchOutput {
968        impulse_macd,
969        impulse_histo,
970        signal,
971        combos,
972        rows,
973        cols,
974    })
975}
976
977fn impulse_macd_batch_inner_into(
978    high: &[f64],
979    low: &[f64],
980    close: &[f64],
981    sweep: &ImpulseMacdBatchRange,
982    kernel: Kernel,
983    parallel: bool,
984    out_impulse_macd: &mut [f64],
985    out_impulse_histo: &mut [f64],
986    out_signal: &mut [f64],
987) -> Result<(), ImpulseMacdError> {
988    let combos = expand_grid_impulse_macd(sweep)?;
989    let rows = combos.len();
990    let cols = close.len();
991    if cols == 0 {
992        return Err(ImpulseMacdError::EmptyInputData);
993    }
994    if high.len() != cols || low.len() != cols {
995        return Err(ImpulseMacdError::DataLengthMismatch);
996    }
997    for params in &combos {
998        let input = ImpulseMacdInput::from_slices(high, low, close, params.clone());
999        impulse_macd_prepare(&input)?;
1000    }
1001    let expected = rows * cols;
1002    if out_impulse_macd.len() != expected
1003        || out_impulse_histo.len() != expected
1004        || out_signal.len() != expected
1005    {
1006        return Err(ImpulseMacdError::OutputLengthMismatch {
1007            expected,
1008            got: out_impulse_macd
1009                .len()
1010                .max(out_impulse_histo.len())
1011                .max(out_signal.len()),
1012        });
1013    }
1014
1015    let do_row = |row: usize, dst_md: &mut [f64], dst_hist: &mut [f64], dst_signal: &mut [f64]| {
1016        let params = &combos[row];
1017        impulse_macd_compute_into(
1018            high,
1019            low,
1020            close,
1021            params.length_ma.unwrap_or(34),
1022            params.length_signal.unwrap_or(9),
1023            kernel,
1024            dst_md,
1025            dst_hist,
1026            dst_signal,
1027        );
1028    };
1029
1030    if parallel {
1031        #[cfg(not(target_arch = "wasm32"))]
1032        out_impulse_macd
1033            .par_chunks_mut(cols)
1034            .zip(out_impulse_histo.par_chunks_mut(cols))
1035            .zip(out_signal.par_chunks_mut(cols))
1036            .enumerate()
1037            .for_each(|(row, ((dst_md, dst_hist), dst_signal))| {
1038                do_row(row, dst_md, dst_hist, dst_signal)
1039            });
1040        #[cfg(target_arch = "wasm32")]
1041        for (row, ((dst_md, dst_hist), dst_signal)) in out_impulse_macd
1042            .chunks_mut(cols)
1043            .zip(out_impulse_histo.chunks_mut(cols))
1044            .zip(out_signal.chunks_mut(cols))
1045            .enumerate()
1046        {
1047            do_row(row, dst_md, dst_hist, dst_signal);
1048        }
1049    } else {
1050        for (row, ((dst_md, dst_hist), dst_signal)) in out_impulse_macd
1051            .chunks_mut(cols)
1052            .zip(out_impulse_histo.chunks_mut(cols))
1053            .zip(out_signal.chunks_mut(cols))
1054            .enumerate()
1055        {
1056            do_row(row, dst_md, dst_hist, dst_signal);
1057        }
1058    }
1059    Ok(())
1060}
1061
1062#[cfg(feature = "python")]
1063#[pyfunction(name = "impulse_macd")]
1064#[pyo3(signature = (high, low, close, length_ma=34, length_signal=9, kernel=None))]
1065pub fn impulse_macd_py<'py>(
1066    py: Python<'py>,
1067    high: PyReadonlyArray1<'py, f64>,
1068    low: PyReadonlyArray1<'py, f64>,
1069    close: PyReadonlyArray1<'py, f64>,
1070    length_ma: usize,
1071    length_signal: usize,
1072    kernel: Option<&str>,
1073) -> PyResult<(
1074    Bound<'py, PyArray1<f64>>,
1075    Bound<'py, PyArray1<f64>>,
1076    Bound<'py, PyArray1<f64>>,
1077)> {
1078    let high = high.as_slice()?;
1079    let low = low.as_slice()?;
1080    let close = close.as_slice()?;
1081    let input = ImpulseMacdInput::from_slices(
1082        high,
1083        low,
1084        close,
1085        ImpulseMacdParams {
1086            length_ma: Some(length_ma),
1087            length_signal: Some(length_signal),
1088        },
1089    );
1090    let kernel = validate_kernel(kernel, false)?;
1091    let out = py
1092        .allow_threads(|| impulse_macd_with_kernel(&input, kernel))
1093        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1094    Ok((
1095        out.impulse_macd.into_pyarray(py),
1096        out.impulse_histo.into_pyarray(py),
1097        out.signal.into_pyarray(py),
1098    ))
1099}
1100
1101#[cfg(feature = "python")]
1102#[pyclass(name = "ImpulseMacdStream")]
1103pub struct ImpulseMacdStreamPy {
1104    stream: ImpulseMacdStream,
1105}
1106
1107#[cfg(feature = "python")]
1108#[pymethods]
1109impl ImpulseMacdStreamPy {
1110    #[new]
1111    #[pyo3(signature = (length_ma=34, length_signal=9))]
1112    fn new(length_ma: usize, length_signal: usize) -> PyResult<Self> {
1113        let stream = ImpulseMacdStream::try_new(ImpulseMacdParams {
1114            length_ma: Some(length_ma),
1115            length_signal: Some(length_signal),
1116        })
1117        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1118        Ok(Self { stream })
1119    }
1120
1121    fn update(&mut self, high: f64, low: f64, close: f64) -> Option<(f64, f64, f64)> {
1122        self.stream.update_reset_on_nan(high, low, close)
1123    }
1124}
1125
1126#[cfg(feature = "python")]
1127#[pyfunction(name = "impulse_macd_batch")]
1128#[pyo3(signature = (high, low, close, length_ma_range, length_signal_range, kernel=None))]
1129pub fn impulse_macd_batch_py<'py>(
1130    py: Python<'py>,
1131    high: PyReadonlyArray1<'py, f64>,
1132    low: PyReadonlyArray1<'py, f64>,
1133    close: PyReadonlyArray1<'py, f64>,
1134    length_ma_range: (usize, usize, usize),
1135    length_signal_range: (usize, usize, usize),
1136    kernel: Option<&str>,
1137) -> PyResult<Bound<'py, PyDict>> {
1138    let high = high.as_slice()?;
1139    let low = low.as_slice()?;
1140    let close = close.as_slice()?;
1141    let sweep = ImpulseMacdBatchRange {
1142        length_ma: length_ma_range,
1143        length_signal: length_signal_range,
1144    };
1145    let combos =
1146        expand_grid_impulse_macd(&sweep).map_err(|e| PyValueError::new_err(e.to_string()))?;
1147    let rows = combos.len();
1148    let cols = close.len();
1149    let total = rows
1150        .checked_mul(cols)
1151        .ok_or_else(|| PyValueError::new_err("rows*cols overflow"))?;
1152    let arr_md = unsafe { PyArray1::<f64>::new(py, [total], false) };
1153    let arr_hist = unsafe { PyArray1::<f64>::new(py, [total], false) };
1154    let arr_signal = unsafe { PyArray1::<f64>::new(py, [total], false) };
1155    let out_md = unsafe { arr_md.as_slice_mut()? };
1156    let out_hist = unsafe { arr_hist.as_slice_mut()? };
1157    let out_signal = unsafe { arr_signal.as_slice_mut()? };
1158    let kernel = validate_kernel(kernel, true)?;
1159
1160    py.allow_threads(|| {
1161        let batch_kernel = match kernel {
1162            Kernel::Auto => detect_best_batch_kernel(),
1163            other => other,
1164        };
1165        impulse_macd_batch_inner_into(
1166            high,
1167            low,
1168            close,
1169            &sweep,
1170            batch_kernel.to_non_batch(),
1171            true,
1172            out_md,
1173            out_hist,
1174            out_signal,
1175        )
1176    })
1177    .map_err(|e| PyValueError::new_err(e.to_string()))?;
1178
1179    let dict = PyDict::new(py);
1180    dict.set_item("impulse_macd", arr_md.reshape((rows, cols))?)?;
1181    dict.set_item("impulse_histo", arr_hist.reshape((rows, cols))?)?;
1182    dict.set_item("signal", arr_signal.reshape((rows, cols))?)?;
1183    dict.set_item(
1184        "length_mas",
1185        combos
1186            .iter()
1187            .map(|params| params.length_ma.unwrap_or(34) as u64)
1188            .collect::<Vec<_>>()
1189            .into_pyarray(py),
1190    )?;
1191    dict.set_item(
1192        "length_signals",
1193        combos
1194            .iter()
1195            .map(|params| params.length_signal.unwrap_or(9) as u64)
1196            .collect::<Vec<_>>()
1197            .into_pyarray(py),
1198    )?;
1199    dict.set_item("rows", rows)?;
1200    dict.set_item("cols", cols)?;
1201    Ok(dict)
1202}
1203
1204#[cfg(feature = "python")]
1205pub fn register_impulse_macd_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
1206    m.add_function(wrap_pyfunction!(impulse_macd_py, m)?)?;
1207    m.add_function(wrap_pyfunction!(impulse_macd_batch_py, m)?)?;
1208    m.add_class::<ImpulseMacdStreamPy>()?;
1209    Ok(())
1210}
1211
1212#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1213#[derive(Debug, Clone, Serialize, Deserialize)]
1214struct ImpulseMacdJsOutput {
1215    impulse_macd: Vec<f64>,
1216    impulse_histo: Vec<f64>,
1217    signal: Vec<f64>,
1218}
1219
1220#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1221#[derive(Debug, Clone, Serialize, Deserialize)]
1222struct ImpulseMacdBatchConfig {
1223    length_ma_range: Vec<usize>,
1224    length_signal_range: Vec<usize>,
1225}
1226
1227#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1228#[derive(Debug, Clone, Serialize, Deserialize)]
1229struct ImpulseMacdBatchJsOutput {
1230    impulse_macd: Vec<f64>,
1231    impulse_histo: Vec<f64>,
1232    signal: Vec<f64>,
1233    rows: usize,
1234    cols: usize,
1235    combos: Vec<ImpulseMacdParams>,
1236}
1237
1238#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1239#[wasm_bindgen(js_name = "impulse_macd_js")]
1240pub fn impulse_macd_js(
1241    high: &[f64],
1242    low: &[f64],
1243    close: &[f64],
1244    length_ma: usize,
1245    length_signal: usize,
1246) -> Result<JsValue, JsValue> {
1247    let input = ImpulseMacdInput::from_slices(
1248        high,
1249        low,
1250        close,
1251        ImpulseMacdParams {
1252            length_ma: Some(length_ma),
1253            length_signal: Some(length_signal),
1254        },
1255    );
1256    let out = impulse_macd_with_kernel(&input, Kernel::Auto)
1257        .map_err(|e| JsValue::from_str(&e.to_string()))?;
1258    serde_wasm_bindgen::to_value(&ImpulseMacdJsOutput {
1259        impulse_macd: out.impulse_macd,
1260        impulse_histo: out.impulse_histo,
1261        signal: out.signal,
1262    })
1263    .map_err(|e| JsValue::from_str(&format!("Serialization error: {e}")))
1264}
1265
1266#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1267#[wasm_bindgen(js_name = "impulse_macd_batch_js")]
1268pub fn impulse_macd_batch_js(
1269    high: &[f64],
1270    low: &[f64],
1271    close: &[f64],
1272    config: JsValue,
1273) -> Result<JsValue, JsValue> {
1274    let config: ImpulseMacdBatchConfig = serde_wasm_bindgen::from_value(config)
1275        .map_err(|e| JsValue::from_str(&format!("Invalid config: {e}")))?;
1276    if config.length_ma_range.len() != 3 {
1277        return Err(JsValue::from_str(
1278            "Invalid config: length_ma_range must have exactly 3 elements [start, end, step]",
1279        ));
1280    }
1281    if config.length_signal_range.len() != 3 {
1282        return Err(JsValue::from_str(
1283            "Invalid config: length_signal_range must have exactly 3 elements [start, end, step]",
1284        ));
1285    }
1286    let sweep = ImpulseMacdBatchRange {
1287        length_ma: (
1288            config.length_ma_range[0],
1289            config.length_ma_range[1],
1290            config.length_ma_range[2],
1291        ),
1292        length_signal: (
1293            config.length_signal_range[0],
1294            config.length_signal_range[1],
1295            config.length_signal_range[2],
1296        ),
1297    };
1298    let batch = impulse_macd_batch_slice(high, low, close, &sweep)
1299        .map_err(|e| JsValue::from_str(&e.to_string()))?;
1300    serde_wasm_bindgen::to_value(&ImpulseMacdBatchJsOutput {
1301        impulse_macd: batch.impulse_macd,
1302        impulse_histo: batch.impulse_histo,
1303        signal: batch.signal,
1304        rows: batch.rows,
1305        cols: batch.cols,
1306        combos: batch.combos,
1307    })
1308    .map_err(|e| JsValue::from_str(&format!("Serialization error: {e}")))
1309}
1310
1311#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1312#[wasm_bindgen]
1313pub fn impulse_macd_alloc(len: usize) -> *mut f64 {
1314    let mut vec = Vec::<f64>::with_capacity(len * 3);
1315    let ptr = vec.as_mut_ptr();
1316    std::mem::forget(vec);
1317    ptr
1318}
1319
1320#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1321#[wasm_bindgen]
1322pub fn impulse_macd_free(ptr: *mut f64, len: usize) {
1323    unsafe {
1324        let _ = Vec::from_raw_parts(ptr, len * 3, len * 3);
1325    }
1326}
1327
1328#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1329#[wasm_bindgen]
1330pub fn impulse_macd_into(
1331    high_ptr: *const f64,
1332    low_ptr: *const f64,
1333    close_ptr: *const f64,
1334    out_ptr: *mut f64,
1335    len: usize,
1336    length_ma: usize,
1337    length_signal: usize,
1338) -> Result<(), JsValue> {
1339    if high_ptr.is_null() || low_ptr.is_null() || close_ptr.is_null() || out_ptr.is_null() {
1340        return Err(JsValue::from_str(
1341            "null pointer passed to impulse_macd_into",
1342        ));
1343    }
1344    unsafe {
1345        let high = std::slice::from_raw_parts(high_ptr, len);
1346        let low = std::slice::from_raw_parts(low_ptr, len);
1347        let close = std::slice::from_raw_parts(close_ptr, len);
1348        let out = std::slice::from_raw_parts_mut(out_ptr, len * 3);
1349        let (out_md, rest) = out.split_at_mut(len);
1350        let (out_hist, out_signal) = rest.split_at_mut(len);
1351        let input = ImpulseMacdInput::from_slices(
1352            high,
1353            low,
1354            close,
1355            ImpulseMacdParams {
1356                length_ma: Some(length_ma),
1357                length_signal: Some(length_signal),
1358            },
1359        );
1360        impulse_macd_into_slice(out_md, out_hist, out_signal, &input, Kernel::Auto)
1361            .map_err(|e| JsValue::from_str(&e.to_string()))
1362    }
1363}
1364
1365#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1366#[wasm_bindgen(js_name = "impulse_macd_into_host")]
1367pub fn impulse_macd_into_host(
1368    high: &[f64],
1369    low: &[f64],
1370    close: &[f64],
1371    out_ptr: *mut f64,
1372    length_ma: usize,
1373    length_signal: usize,
1374) -> Result<(), JsValue> {
1375    if out_ptr.is_null() {
1376        return Err(JsValue::from_str(
1377            "null pointer passed to impulse_macd_into_host",
1378        ));
1379    }
1380    unsafe {
1381        let out = std::slice::from_raw_parts_mut(out_ptr, close.len() * 3);
1382        let (out_md, rest) = out.split_at_mut(close.len());
1383        let (out_hist, out_signal) = rest.split_at_mut(close.len());
1384        let input = ImpulseMacdInput::from_slices(
1385            high,
1386            low,
1387            close,
1388            ImpulseMacdParams {
1389                length_ma: Some(length_ma),
1390                length_signal: Some(length_signal),
1391            },
1392        );
1393        impulse_macd_into_slice(out_md, out_hist, out_signal, &input, Kernel::Auto)
1394            .map_err(|e| JsValue::from_str(&e.to_string()))
1395    }
1396}
1397
1398#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1399#[wasm_bindgen]
1400pub fn impulse_macd_batch_into(
1401    high_ptr: *const f64,
1402    low_ptr: *const f64,
1403    close_ptr: *const f64,
1404    out_ptr: *mut f64,
1405    len: usize,
1406    length_ma_start: usize,
1407    length_ma_end: usize,
1408    length_ma_step: usize,
1409    length_signal_start: usize,
1410    length_signal_end: usize,
1411    length_signal_step: usize,
1412) -> Result<usize, JsValue> {
1413    if high_ptr.is_null() || low_ptr.is_null() || close_ptr.is_null() || out_ptr.is_null() {
1414        return Err(JsValue::from_str(
1415            "null pointer passed to impulse_macd_batch_into",
1416        ));
1417    }
1418    unsafe {
1419        let high = std::slice::from_raw_parts(high_ptr, len);
1420        let low = std::slice::from_raw_parts(low_ptr, len);
1421        let close = std::slice::from_raw_parts(close_ptr, len);
1422        let sweep = ImpulseMacdBatchRange {
1423            length_ma: (length_ma_start, length_ma_end, length_ma_step),
1424            length_signal: (length_signal_start, length_signal_end, length_signal_step),
1425        };
1426        let combos =
1427            expand_grid_impulse_macd(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
1428        let rows = combos.len();
1429        let out = std::slice::from_raw_parts_mut(out_ptr, rows * len * 3);
1430        let (out_md, rest) = out.split_at_mut(rows * len);
1431        let (out_hist, out_signal) = rest.split_at_mut(rows * len);
1432        impulse_macd_batch_inner_into(
1433            high,
1434            low,
1435            close,
1436            &sweep,
1437            Kernel::Scalar,
1438            false,
1439            out_md,
1440            out_hist,
1441            out_signal,
1442        )
1443        .map_err(|e| JsValue::from_str(&e.to_string()))?;
1444        Ok(rows)
1445    }
1446}
1447
1448#[cfg(test)]
1449mod tests {
1450    use super::*;
1451    use crate::indicators::dispatch::{
1452        compute_cpu_batch, IndicatorBatchRequest, IndicatorDataRef, IndicatorParamSet, ParamKV,
1453        ParamValue,
1454    };
1455
1456    fn sample_ohlc(len: usize) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
1457        let mut high = Vec::with_capacity(len);
1458        let mut low = Vec::with_capacity(len);
1459        let mut close = Vec::with_capacity(len);
1460        for i in 0..len {
1461            let base = 100.0 + i as f64 * 0.06 + (i as f64 * 0.17).sin() * 2.4;
1462            let cl = base + (i as f64 * 0.11).cos() * 0.9;
1463            let hi = cl + 1.3 + (i as f64 * 0.07).sin().abs();
1464            let lo = cl - 1.1 - (i as f64 * 0.05).cos().abs();
1465            high.push(hi);
1466            low.push(lo);
1467            close.push(cl);
1468        }
1469        (high, low, close)
1470    }
1471
1472    fn assert_close_nan(actual: &[f64], expected: &[f64]) {
1473        assert_eq!(actual.len(), expected.len());
1474        for i in 0..actual.len() {
1475            let a = actual[i];
1476            let e = expected[i];
1477            if a.is_nan() || e.is_nan() {
1478                assert!(
1479                    a.is_nan() && e.is_nan(),
1480                    "nan mismatch at {i}: got {a}, expected {e}"
1481                );
1482            } else {
1483                assert!(
1484                    (a - e).abs() <= 1e-10,
1485                    "mismatch at {i}: got {a}, expected {e}"
1486                );
1487            }
1488        }
1489    }
1490
1491    fn naive_expected(
1492        high: &[f64],
1493        low: &[f64],
1494        close: &[f64],
1495        length_ma: usize,
1496        length_signal: usize,
1497    ) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
1498        let mut md = vec![f64::NAN; close.len()];
1499        let mut hist = vec![f64::NAN; close.len()];
1500        let mut signal = vec![f64::NAN; close.len()];
1501
1502        let mut hi_smma = SmmaState::new(length_ma);
1503        let mut lo_smma = SmmaState::new(length_ma);
1504        let mut ema1 = EmaState::new(length_ma);
1505        let mut ema2 = EmaState::new(length_ma);
1506        let mut signal_sma = SmaState::new(length_signal);
1507
1508        for i in 0..close.len() {
1509            if !valid_bar(high[i], low[i], close[i]) {
1510                hi_smma.reset();
1511                lo_smma.reset();
1512                ema1.reset();
1513                ema2.reset();
1514                signal_sma.reset();
1515                continue;
1516            }
1517            let src = (high[i] + low[i] + close[i]) / 3.0;
1518            let hi = hi_smma.update(high[i]);
1519            let lo = lo_smma.update(low[i]);
1520            let e1 = ema1.update(src);
1521            let e2 = ema2.update(e1);
1522            let mi = e1 + (e1 - e2);
1523            let now_md = match (hi, lo) {
1524                (Some(hi), Some(lo)) if mi > hi => mi - hi,
1525                (Some(_), Some(lo)) if mi < lo => mi - lo,
1526                _ => 0.0,
1527            };
1528            md[i] = now_md;
1529            if let Some(sig) = signal_sma.update(now_md) {
1530                signal[i] = sig;
1531                hist[i] = now_md - sig;
1532            }
1533        }
1534
1535        (md, hist, signal)
1536    }
1537
1538    #[test]
1539    fn impulse_macd_matches_naive() {
1540        let (high, low, close) = sample_ohlc(256);
1541        let input = ImpulseMacdInput::from_slices(
1542            &high,
1543            &low,
1544            &close,
1545            ImpulseMacdParams {
1546                length_ma: Some(34),
1547                length_signal: Some(9),
1548            },
1549        );
1550        let out = impulse_macd(&input).expect("indicator");
1551        let (expected_md, expected_hist, expected_signal) =
1552            naive_expected(&high, &low, &close, 34, 9);
1553        assert_close_nan(&out.impulse_macd, &expected_md);
1554        assert_close_nan(&out.impulse_histo, &expected_hist);
1555        assert_close_nan(&out.signal, &expected_signal);
1556    }
1557
1558    #[test]
1559    fn impulse_macd_into_matches_api() {
1560        let (high, low, close) = sample_ohlc(192);
1561        let input = ImpulseMacdInput::from_slices(
1562            &high,
1563            &low,
1564            &close,
1565            ImpulseMacdParams {
1566                length_ma: Some(21),
1567                length_signal: Some(7),
1568            },
1569        );
1570        let out = impulse_macd(&input).expect("baseline");
1571        let mut md = vec![0.0; close.len()];
1572        let mut hist = vec![0.0; close.len()];
1573        let mut signal = vec![0.0; close.len()];
1574        impulse_macd_into(&input, &mut md, &mut hist, &mut signal).expect("into");
1575        assert_close_nan(&md, &out.impulse_macd);
1576        assert_close_nan(&hist, &out.impulse_histo);
1577        assert_close_nan(&signal, &out.signal);
1578    }
1579
1580    #[test]
1581    fn impulse_macd_stream_matches_batch() {
1582        let (high, low, close) = sample_ohlc(192);
1583        let input = ImpulseMacdInput::from_slices(
1584            &high,
1585            &low,
1586            &close,
1587            ImpulseMacdParams {
1588                length_ma: Some(34),
1589                length_signal: Some(9),
1590            },
1591        );
1592        let batch = impulse_macd(&input).expect("batch");
1593        let mut stream = ImpulseMacdStream::try_new(ImpulseMacdParams {
1594            length_ma: Some(34),
1595            length_signal: Some(9),
1596        })
1597        .expect("stream");
1598        let mut md = Vec::with_capacity(close.len());
1599        let mut hist = Vec::with_capacity(close.len());
1600        let mut signal = Vec::with_capacity(close.len());
1601        for i in 0..close.len() {
1602            match stream.update_reset_on_nan(high[i], low[i], close[i]) {
1603                Some((a, b, c)) => {
1604                    md.push(a);
1605                    hist.push(b);
1606                    signal.push(c);
1607                }
1608                None => {
1609                    md.push(f64::NAN);
1610                    hist.push(f64::NAN);
1611                    signal.push(f64::NAN);
1612                }
1613            }
1614        }
1615        assert_close_nan(&md, &batch.impulse_macd);
1616        assert_close_nan(&hist, &batch.impulse_histo);
1617        assert_close_nan(&signal, &batch.signal);
1618    }
1619
1620    #[test]
1621    fn impulse_macd_batch_single_param_matches_single() {
1622        let (high, low, close) = sample_ohlc(160);
1623        let sweep = ImpulseMacdBatchRange {
1624            length_ma: (34, 34, 0),
1625            length_signal: (9, 9, 0),
1626        };
1627        let batch =
1628            impulse_macd_batch_with_kernel(&high, &low, &close, &sweep, Kernel::ScalarBatch)
1629                .expect("batch");
1630        let input = ImpulseMacdInput::from_slices(
1631            &high,
1632            &low,
1633            &close,
1634            ImpulseMacdParams {
1635                length_ma: Some(34),
1636                length_signal: Some(9),
1637            },
1638        );
1639        let out = impulse_macd(&input).expect("single");
1640        assert_eq!(batch.rows, 1);
1641        assert_eq!(batch.cols, close.len());
1642        assert_close_nan(&batch.impulse_macd[..close.len()], &out.impulse_macd);
1643        assert_close_nan(&batch.impulse_histo[..close.len()], &out.impulse_histo);
1644        assert_close_nan(&batch.signal[..close.len()], &out.signal);
1645    }
1646
1647    #[test]
1648    fn impulse_macd_rejects_invalid_length_signal() {
1649        let (high, low, close) = sample_ohlc(32);
1650        let input = ImpulseMacdInput::from_slices(
1651            &high,
1652            &low,
1653            &close,
1654            ImpulseMacdParams {
1655                length_ma: Some(20),
1656                length_signal: Some(0),
1657            },
1658        );
1659        let err = impulse_macd(&input).expect_err("invalid");
1660        assert!(matches!(err, ImpulseMacdError::InvalidLengthSignal { .. }));
1661    }
1662
1663    #[test]
1664    fn impulse_macd_dispatch_matches_direct() {
1665        let (high, low, close) = sample_ohlc(180);
1666        let params = [
1667            ParamKV {
1668                key: "length_ma",
1669                value: ParamValue::Int(34),
1670            },
1671            ParamKV {
1672                key: "length_signal",
1673                value: ParamValue::Int(9),
1674            },
1675        ];
1676        let combos = [IndicatorParamSet { params: &params }];
1677        let out = compute_cpu_batch(IndicatorBatchRequest {
1678            indicator_id: "impulse_macd",
1679            output_id: Some("impulse_macd"),
1680            data: IndicatorDataRef::Ohlc {
1681                open: &close,
1682                high: &high,
1683                low: &low,
1684                close: &close,
1685            },
1686            combos: &combos,
1687            kernel: Kernel::ScalarBatch,
1688        })
1689        .expect("dispatch");
1690        let direct = impulse_macd(&ImpulseMacdInput::from_slices(
1691            &high,
1692            &low,
1693            &close,
1694            ImpulseMacdParams {
1695                length_ma: Some(34),
1696                length_signal: Some(9),
1697            },
1698        ))
1699        .expect("direct");
1700        assert_eq!(out.rows, 1);
1701        assert_eq!(out.cols, close.len());
1702        assert_close_nan(
1703            out.values_f64.as_ref().expect("values"),
1704            &direct.impulse_macd,
1705        );
1706    }
1707}