Skip to main content

vector_ta/indicators/
vwmacd.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::indicators::moving_averages::ma::{ma, ma_with_kernel, MaData};
16use crate::utilities::data_loader::{source_type, Candles};
17use crate::utilities::enums::Kernel;
18use crate::utilities::helpers::{
19    alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
20    make_uninit_matrix,
21};
22#[cfg(feature = "python")]
23use crate::utilities::kernel_validation::validate_kernel;
24use aligned_vec::{AVec, CACHELINE_ALIGN};
25#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
26use core::arch::x86_64::*;
27#[cfg(not(target_arch = "wasm32"))]
28use rayon::prelude::*;
29use std::convert::AsRef;
30use std::error::Error;
31use std::mem::MaybeUninit;
32use thiserror::Error;
33
34#[derive(Debug, Clone)]
35pub enum VwmacdData<'a> {
36    Candles {
37        candles: &'a Candles,
38        close_source: &'a str,
39        volume_source: &'a str,
40    },
41    Slices {
42        close: &'a [f64],
43        volume: &'a [f64],
44    },
45}
46
47#[derive(Debug, Clone)]
48pub struct VwmacdOutput {
49    pub macd: Vec<f64>,
50    pub signal: Vec<f64>,
51    pub hist: Vec<f64>,
52}
53
54#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
55#[wasm_bindgen]
56#[derive(Serialize, Deserialize)]
57pub struct VwmacdJsOutput {
58    #[wasm_bindgen(getter_with_clone)]
59    pub macd: Vec<f64>,
60    #[wasm_bindgen(getter_with_clone)]
61    pub signal: Vec<f64>,
62    #[wasm_bindgen(getter_with_clone)]
63    pub hist: Vec<f64>,
64}
65
66#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
67#[derive(Serialize, Deserialize)]
68pub struct VwmacdBatchConfig {
69    pub fast_range: (usize, usize, usize),
70    pub slow_range: (usize, usize, usize),
71    pub signal_range: (usize, usize, usize),
72    pub fast_ma_type: Option<String>,
73    pub slow_ma_type: Option<String>,
74    pub signal_ma_type: Option<String>,
75}
76
77#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
78#[derive(Serialize, Deserialize)]
79pub struct VwmacdBatchJsOutput {
80    pub values: Vec<f64>,
81    pub combos: Vec<VwmacdParams>,
82    pub rows: usize,
83    pub cols: usize,
84}
85
86#[derive(Debug, Clone)]
87#[cfg_attr(
88    all(target_arch = "wasm32", feature = "wasm"),
89    derive(Serialize, Deserialize)
90)]
91pub struct VwmacdParams {
92    pub fast_period: Option<usize>,
93    pub slow_period: Option<usize>,
94    pub signal_period: Option<usize>,
95    pub fast_ma_type: Option<String>,
96    pub slow_ma_type: Option<String>,
97    pub signal_ma_type: Option<String>,
98}
99
100impl Default for VwmacdParams {
101    fn default() -> Self {
102        Self {
103            fast_period: Some(12),
104            slow_period: Some(26),
105            signal_period: Some(9),
106            fast_ma_type: Some("sma".to_string()),
107            slow_ma_type: Some("sma".to_string()),
108            signal_ma_type: Some("ema".to_string()),
109        }
110    }
111}
112
113#[derive(Debug, Clone)]
114pub struct VwmacdInput<'a> {
115    pub data: VwmacdData<'a>,
116    pub params: VwmacdParams,
117}
118
119impl<'a> VwmacdInput<'a> {
120    #[inline]
121    pub fn from_candles(
122        candles: &'a Candles,
123        close_source: &'a str,
124        volume_source: &'a str,
125        params: VwmacdParams,
126    ) -> Self {
127        Self {
128            data: VwmacdData::Candles {
129                candles,
130                close_source,
131                volume_source,
132            },
133            params,
134        }
135    }
136    #[inline]
137    pub fn from_slices(close: &'a [f64], volume: &'a [f64], params: VwmacdParams) -> Self {
138        Self {
139            data: VwmacdData::Slices { close, volume },
140            params,
141        }
142    }
143    #[inline]
144    pub fn with_default_candles(candles: &'a Candles) -> Self {
145        Self::from_candles(candles, "close", "volume", VwmacdParams::default())
146    }
147    #[inline]
148    pub fn get_fast(&self) -> usize {
149        self.params.fast_period.unwrap_or(12)
150    }
151    #[inline]
152    pub fn get_slow(&self) -> usize {
153        self.params.slow_period.unwrap_or(26)
154    }
155    #[inline]
156    pub fn get_signal(&self) -> usize {
157        self.params.signal_period.unwrap_or(9)
158    }
159    #[inline]
160    pub fn get_fast_ma_type(&self) -> &str {
161        self.params.fast_ma_type.as_deref().unwrap_or("sma")
162    }
163    #[inline]
164    pub fn get_slow_ma_type(&self) -> &str {
165        self.params.slow_ma_type.as_deref().unwrap_or("sma")
166    }
167    #[inline]
168    pub fn get_signal_ma_type(&self) -> &str {
169        self.params.signal_ma_type.as_deref().unwrap_or("ema")
170    }
171}
172
173#[derive(Clone, Debug)]
174pub struct VwmacdBuilder {
175    fast: Option<usize>,
176    slow: Option<usize>,
177    signal: Option<usize>,
178    fast_ma_type: Option<String>,
179    slow_ma_type: Option<String>,
180    signal_ma_type: Option<String>,
181    kernel: Kernel,
182}
183
184impl Default for VwmacdBuilder {
185    fn default() -> Self {
186        Self {
187            fast: None,
188            slow: None,
189            signal: None,
190            fast_ma_type: None,
191            slow_ma_type: None,
192            signal_ma_type: None,
193            kernel: Kernel::Auto,
194        }
195    }
196}
197
198impl VwmacdBuilder {
199    #[inline(always)]
200    pub fn new() -> Self {
201        Self::default()
202    }
203    #[inline(always)]
204    pub fn fast(mut self, n: usize) -> Self {
205        self.fast = Some(n);
206        self
207    }
208    #[inline(always)]
209    pub fn slow(mut self, n: usize) -> Self {
210        self.slow = Some(n);
211        self
212    }
213    #[inline(always)]
214    pub fn signal(mut self, n: usize) -> Self {
215        self.signal = Some(n);
216        self
217    }
218    #[inline(always)]
219    pub fn fast_ma_type(mut self, ma_type: String) -> Self {
220        self.fast_ma_type = Some(ma_type);
221        self
222    }
223    #[inline(always)]
224    pub fn slow_ma_type(mut self, ma_type: String) -> Self {
225        self.slow_ma_type = Some(ma_type);
226        self
227    }
228    #[inline(always)]
229    pub fn signal_ma_type(mut self, ma_type: String) -> Self {
230        self.signal_ma_type = Some(ma_type);
231        self
232    }
233    #[inline(always)]
234    pub fn kernel(mut self, k: Kernel) -> Self {
235        self.kernel = k;
236        self
237    }
238    #[inline(always)]
239    pub fn apply(self, c: &Candles) -> Result<VwmacdOutput, VwmacdError> {
240        let p = VwmacdParams {
241            fast_period: self.fast,
242            slow_period: self.slow,
243            signal_period: self.signal,
244            fast_ma_type: self.fast_ma_type,
245            slow_ma_type: self.slow_ma_type,
246            signal_ma_type: self.signal_ma_type,
247        };
248        let i = VwmacdInput::from_candles(c, "close", "volume", p);
249        vwmacd_with_kernel(&i, self.kernel)
250    }
251    #[inline(always)]
252    pub fn apply_slices(self, close: &[f64], volume: &[f64]) -> Result<VwmacdOutput, VwmacdError> {
253        let p = VwmacdParams {
254            fast_period: self.fast,
255            slow_period: self.slow,
256            signal_period: self.signal,
257            fast_ma_type: self.fast_ma_type,
258            slow_ma_type: self.slow_ma_type,
259            signal_ma_type: self.signal_ma_type,
260        };
261        let i = VwmacdInput::from_slices(close, volume, p);
262        vwmacd_with_kernel(&i, self.kernel)
263    }
264    #[inline(always)]
265    pub fn into_stream(self) -> Result<VwmacdStream, VwmacdError> {
266        let p = VwmacdParams {
267            fast_period: self.fast,
268            slow_period: self.slow,
269            signal_period: self.signal,
270            fast_ma_type: self.fast_ma_type,
271            slow_ma_type: self.slow_ma_type,
272            signal_ma_type: self.signal_ma_type,
273        };
274        VwmacdStream::try_new(p)
275    }
276}
277
278#[derive(Debug, Error)]
279pub enum VwmacdError {
280    #[error("vwmacd: Input data slice is empty.")]
281    EmptyInputData,
282    #[error("vwmacd: All values are NaN.")]
283    AllValuesNaN,
284    #[error(
285        "vwmacd: Invalid period: fast={fast}, slow={slow}, signal={signal}, data_len={data_len}"
286    )]
287    InvalidPeriod {
288        fast: usize,
289        slow: usize,
290        signal: usize,
291        data_len: usize,
292    },
293    #[error("vwmacd: Not enough valid data: needed={needed}, valid={valid}")]
294    NotEnoughValidData { needed: usize, valid: usize },
295    #[error("vwmacd: Output length mismatch: expected={expected}, got={got}")]
296    OutputLengthMismatch { expected: usize, got: usize },
297    #[error("vwmacd: Invalid range: start={start}, end={end}, step={step}")]
298    InvalidRange {
299        start: String,
300        end: String,
301        step: String,
302    },
303    #[error("vwmacd: Invalid kernel for batch: {0:?}")]
304    InvalidKernelForBatch(Kernel),
305    #[error("vwmacd: MA calculation error: {0}")]
306    MaError(String),
307}
308
309#[inline(always)]
310fn first_valid_pair(close: &[f64], volume: &[f64]) -> Option<usize> {
311    close
312        .iter()
313        .zip(volume)
314        .position(|(c, v)| !c.is_nan() && !v.is_nan())
315}
316
317#[inline]
318pub fn vwmacd(input: &VwmacdInput) -> Result<VwmacdOutput, VwmacdError> {
319    vwmacd_with_kernel(input, Kernel::Auto)
320}
321
322pub fn vwmacd_with_kernel(
323    input: &VwmacdInput,
324    kernel: Kernel,
325) -> Result<VwmacdOutput, VwmacdError> {
326    let (
327        close,
328        volume,
329        fast,
330        slow,
331        signal_period,
332        fmt,
333        smt,
334        sigmt,
335        first,
336        macd_warmup_abs,
337        total_warmup_abs,
338        chosen,
339    ) = vwmacd_prepare(input, kernel)?;
340
341    let mut macd = alloc_with_nan_prefix(close.len(), macd_warmup_abs);
342    let mut signal = alloc_with_nan_prefix(close.len(), total_warmup_abs);
343    let mut hist = alloc_with_nan_prefix(close.len(), total_warmup_abs);
344
345    vwmacd_compute_into(
346        close,
347        volume,
348        fast,
349        slow,
350        signal_period,
351        fmt,
352        smt,
353        sigmt,
354        first,
355        macd_warmup_abs,
356        total_warmup_abs,
357        chosen,
358        &mut macd,
359        &mut signal,
360        &mut hist,
361    )?;
362
363    Ok(VwmacdOutput { macd, signal, hist })
364}
365
366pub fn vwmacd_into_slice(
367    dst_macd: &mut [f64],
368    dst_signal: &mut [f64],
369    dst_hist: &mut [f64],
370    input: &VwmacdInput,
371    kern: Kernel,
372) -> Result<(), VwmacdError> {
373    let (
374        close,
375        volume,
376        fast,
377        slow,
378        signal_period,
379        fmt,
380        smt,
381        sigmt,
382        first,
383        macd_warmup_abs,
384        total_warmup_abs,
385        chosen,
386    ) = vwmacd_prepare(input, kern)?;
387    let len = close.len();
388    if dst_macd.len() != len || dst_signal.len() != len || dst_hist.len() != len {
389        if dst_macd.len() != len {
390            return Err(VwmacdError::OutputLengthMismatch {
391                expected: len,
392                got: dst_macd.len(),
393            });
394        }
395        if dst_signal.len() != len {
396            return Err(VwmacdError::OutputLengthMismatch {
397                expected: len,
398                got: dst_signal.len(),
399            });
400        }
401        return Err(VwmacdError::OutputLengthMismatch {
402            expected: len,
403            got: dst_hist.len(),
404        });
405    }
406
407    vwmacd_compute_into(
408        close,
409        volume,
410        fast,
411        slow,
412        signal_period,
413        fmt,
414        smt,
415        sigmt,
416        first,
417        macd_warmup_abs,
418        total_warmup_abs,
419        chosen,
420        dst_macd,
421        dst_signal,
422        dst_hist,
423    )
424}
425
426#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
427pub fn vwmacd_into(
428    input: &VwmacdInput,
429    macd_out: &mut [f64],
430    signal_out: &mut [f64],
431    hist_out: &mut [f64],
432) -> Result<(), VwmacdError> {
433    let (
434        close,
435        volume,
436        fast,
437        slow,
438        signal,
439        fast_ma_type,
440        slow_ma_type,
441        signal_ma_type,
442        first,
443        macd_warmup_abs,
444        total_warmup_abs,
445        chosen,
446    ) = vwmacd_prepare(input, Kernel::Auto)?;
447
448    let len = close.len();
449    if macd_out.len() != len || signal_out.len() != len || hist_out.len() != len {
450        if macd_out.len() != len {
451            return Err(VwmacdError::OutputLengthMismatch {
452                expected: len,
453                got: macd_out.len(),
454            });
455        }
456        if signal_out.len() != len {
457            return Err(VwmacdError::OutputLengthMismatch {
458                expected: len,
459                got: signal_out.len(),
460            });
461        }
462        return Err(VwmacdError::OutputLengthMismatch {
463            expected: len,
464            got: hist_out.len(),
465        });
466    }
467
468    let qnan = f64::from_bits(0x7ff8_0000_0000_0000);
469    for i in 0..macd_warmup_abs.min(len) {
470        macd_out[i] = qnan;
471    }
472    for i in 0..total_warmup_abs.min(len) {
473        signal_out[i] = qnan;
474        hist_out[i] = qnan;
475    }
476
477    vwmacd_compute_into(
478        close,
479        volume,
480        fast,
481        slow,
482        signal,
483        fast_ma_type,
484        slow_ma_type,
485        signal_ma_type,
486        first,
487        macd_warmup_abs,
488        total_warmup_abs,
489        chosen,
490        macd_out,
491        signal_out,
492        hist_out,
493    )
494}
495
496#[inline]
497pub unsafe fn vwmacd_scalar(
498    close: &[f64],
499    volume: &[f64],
500    fast: usize,
501    slow: usize,
502    signal: usize,
503    fast_ma_type: &str,
504    slow_ma_type: &str,
505    signal_ma_type: &str,
506) -> Result<VwmacdOutput, VwmacdError> {
507    let len = close.len();
508    let mut close_x_volume = alloc_with_nan_prefix(len, 0);
509    for i in 0..len {
510        if !close[i].is_nan() && !volume[i].is_nan() {
511            close_x_volume[i] = close[i] * volume[i];
512        }
513    }
514
515    let slow_ma_cv = ma(slow_ma_type, MaData::Slice(&close_x_volume), slow)
516        .map_err(|e| VwmacdError::MaError(e.to_string()))?;
517    let slow_ma_v = ma(slow_ma_type, MaData::Slice(&volume), slow)
518        .map_err(|e| VwmacdError::MaError(e.to_string()))?;
519
520    let mut vwma_slow = alloc_with_nan_prefix(len, slow - 1);
521    for i in 0..len {
522        let denom = slow_ma_v[i];
523        if !denom.is_nan() && denom != 0.0 {
524            vwma_slow[i] = slow_ma_cv[i] / denom;
525        }
526    }
527
528    let fast_ma_cv = ma(fast_ma_type, MaData::Slice(&close_x_volume), fast)
529        .map_err(|e| VwmacdError::MaError(e.to_string()))?;
530    let fast_ma_v = ma(fast_ma_type, MaData::Slice(&volume), fast)
531        .map_err(|e| VwmacdError::MaError(e.to_string()))?;
532
533    let mut vwma_fast = alloc_with_nan_prefix(len, fast - 1);
534    for i in 0..len {
535        let denom = fast_ma_v[i];
536        if !denom.is_nan() && denom != 0.0 {
537            vwma_fast[i] = fast_ma_cv[i] / denom;
538        }
539    }
540
541    let mut macd = alloc_with_nan_prefix(len, slow - 1);
542    for i in 0..len {
543        if !vwma_fast[i].is_nan() && !vwma_slow[i].is_nan() {
544            macd[i] = vwma_fast[i] - vwma_slow[i];
545        }
546    }
547
548    let mut signal_vec = ma(signal_ma_type, MaData::Slice(&macd), signal)
549        .map_err(|e| VwmacdError::MaError(e.to_string()))?;
550
551    let total_warmup = slow + signal - 2;
552    for i in 0..total_warmup {
553        signal_vec[i] = f64::NAN;
554    }
555
556    let mut hist = alloc_with_nan_prefix(len, total_warmup);
557    for i in 0..len {
558        if !macd[i].is_nan() && !signal_vec[i].is_nan() {
559            hist[i] = macd[i] - signal_vec[i];
560        }
561    }
562    Ok(VwmacdOutput {
563        macd,
564        signal: signal_vec,
565        hist,
566    })
567}
568
569pub unsafe fn vwmacd_scalar_classic(
570    close: &[f64],
571    volume: &[f64],
572    fast: usize,
573    slow: usize,
574    signal: usize,
575    fast_ma_type: &str,
576    slow_ma_type: &str,
577    signal_ma_type: &str,
578    first_valid_idx: usize,
579    macd_warmup_abs: usize,
580    total_warmup_abs: usize,
581    dst_macd: &mut [f64],
582    dst_signal: &mut [f64],
583    dst_hist: &mut [f64],
584) -> Result<(), VwmacdError> {
585    let len = close.len();
586
587    for i in 0..macd_warmup_abs.min(len) {
588        dst_macd[i] = f64::NAN;
589    }
590
591    if first_valid_idx < len {
592        let mut f_cv = 0.0f64;
593        let mut f_v = 0.0f64;
594        let mut s_cv = 0.0f64;
595        let mut s_v = 0.0f64;
596
597        let mut i = first_valid_idx;
598        while i < len {
599            let v_i = volume[i];
600            let cv_i = close[i] * v_i;
601
602            f_cv += cv_i;
603            f_v += v_i;
604            s_cv += cv_i;
605            s_v += v_i;
606
607            let n_since_first = i - first_valid_idx + 1;
608            if n_since_first > fast {
609                let j = i - fast;
610                let v_o = volume[j];
611                let cv_o = close[j] * v_o;
612                f_cv -= cv_o;
613                f_v -= v_o;
614            }
615            if n_since_first > slow {
616                let j = i - slow;
617                let v_o = volume[j];
618                let cv_o = close[j] * v_o;
619                s_cv -= cv_o;
620                s_v -= v_o;
621            }
622
623            if i >= macd_warmup_abs {
624                if f_v != 0.0 && s_v != 0.0 {
625                    let fast_vwma = f_cv / f_v;
626                    let slow_vwma = s_cv / s_v;
627                    dst_macd[i] = fast_vwma - slow_vwma;
628                } else {
629                    dst_macd[i] = f64::NAN;
630                }
631            }
632            i += 1;
633        }
634    }
635
636    if macd_warmup_abs < len {
637        let alpha = 2.0 / (signal as f64 + 1.0);
638        let beta = 1.0 - alpha;
639
640        let start = macd_warmup_abs;
641        let warmup_end = (start + signal).min(len);
642        if start < len {
643            let mut mean = dst_macd[start];
644            dst_signal[start] = mean;
645            let mut count = 1usize;
646            for i in (start + 1)..warmup_end {
647                let x = dst_macd[i];
648                count += 1;
649                mean = ((count as f64 - 1.0) * mean + x) / (count as f64);
650                dst_signal[i] = mean;
651            }
652
653            let mut prev = mean;
654            for i in warmup_end..len {
655                let x = dst_macd[i];
656                prev = beta.mul_add(prev, alpha * x);
657                dst_signal[i] = prev;
658            }
659        }
660    }
661
662    for i in 0..total_warmup_abs.min(len) {
663        dst_signal[i] = f64::NAN;
664    }
665
666    for i in 0..total_warmup_abs.min(len) {
667        dst_hist[i] = f64::NAN;
668    }
669    for i in total_warmup_abs..len {
670        if !dst_macd[i].is_nan() && !dst_signal[i].is_nan() {
671            dst_hist[i] = dst_macd[i] - dst_signal[i];
672        } else {
673            dst_hist[i] = f64::NAN;
674        }
675    }
676
677    Ok(())
678}
679
680#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
681#[inline]
682pub unsafe fn vwmacd_avx2(
683    close: &[f64],
684    volume: &[f64],
685    fast: usize,
686    slow: usize,
687    signal: usize,
688    fast_ma_type: &str,
689    slow_ma_type: &str,
690    signal_ma_type: &str,
691) -> Result<VwmacdOutput, VwmacdError> {
692    vwmacd_scalar(
693        close,
694        volume,
695        fast,
696        slow,
697        signal,
698        fast_ma_type,
699        slow_ma_type,
700        signal_ma_type,
701    )
702}
703
704#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
705#[inline]
706pub unsafe fn vwmacd_avx512(
707    close: &[f64],
708    volume: &[f64],
709    fast: usize,
710    slow: usize,
711    signal: usize,
712    fast_ma_type: &str,
713    slow_ma_type: &str,
714    signal_ma_type: &str,
715) -> Result<VwmacdOutput, VwmacdError> {
716    if slow <= 32 {
717        vwmacd_avx512_short(
718            close,
719            volume,
720            fast,
721            slow,
722            signal,
723            fast_ma_type,
724            slow_ma_type,
725            signal_ma_type,
726        )
727    } else {
728        vwmacd_avx512_long(
729            close,
730            volume,
731            fast,
732            slow,
733            signal,
734            fast_ma_type,
735            slow_ma_type,
736            signal_ma_type,
737        )
738    }
739}
740
741#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
742#[inline]
743pub unsafe fn vwmacd_avx512_short(
744    close: &[f64],
745    volume: &[f64],
746    fast: usize,
747    slow: usize,
748    signal: usize,
749    fast_ma_type: &str,
750    slow_ma_type: &str,
751    signal_ma_type: &str,
752) -> Result<VwmacdOutput, VwmacdError> {
753    vwmacd_scalar(
754        close,
755        volume,
756        fast,
757        slow,
758        signal,
759        fast_ma_type,
760        slow_ma_type,
761        signal_ma_type,
762    )
763}
764
765#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
766#[inline]
767pub unsafe fn vwmacd_avx512_long(
768    close: &[f64],
769    volume: &[f64],
770    fast: usize,
771    slow: usize,
772    signal: usize,
773    fast_ma_type: &str,
774    slow_ma_type: &str,
775    signal_ma_type: &str,
776) -> Result<VwmacdOutput, VwmacdError> {
777    vwmacd_scalar(
778        close,
779        volume,
780        fast,
781        slow,
782        signal,
783        fast_ma_type,
784        slow_ma_type,
785        signal_ma_type,
786    )
787}
788
789#[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
790#[inline]
791pub unsafe fn vwmacd_simd128(
792    close: &[f64],
793    volume: &[f64],
794    fast: usize,
795    slow: usize,
796    signal: usize,
797    fast_ma_type: &str,
798    slow_ma_type: &str,
799    signal_ma_type: &str,
800) -> Result<VwmacdOutput, VwmacdError> {
801    vwmacd_scalar(
802        close,
803        volume,
804        fast,
805        slow,
806        signal,
807        fast_ma_type,
808        slow_ma_type,
809        signal_ma_type,
810    )
811}
812
813#[inline]
814pub unsafe fn vwmacd_scalar_macd_into(
815    close: &[f64],
816    volume: &[f64],
817    fast: usize,
818    slow: usize,
819    signal: usize,
820    fast_ma_type: &str,
821    slow_ma_type: &str,
822    signal_ma_type: &str,
823    out: &mut [f64],
824) -> Result<(), VwmacdError> {
825    let len = close.len();
826
827    if fast_ma_type.eq_ignore_ascii_case("sma") && slow_ma_type.eq_ignore_ascii_case("sma") {
828        if len == 0 {
829            return Ok(());
830        }
831        let first = match first_valid_pair(close, volume) {
832            Some(ix) => ix,
833            None => return Ok(()),
834        };
835        let macd_warmup_abs = first + fast.max(slow) - 1;
836        for i in 0..macd_warmup_abs.min(len) {
837            out[i] = f64::NAN;
838        }
839
840        let mut f_cv = 0.0f64;
841        let mut f_v = 0.0f64;
842        let mut s_cv = 0.0f64;
843        let mut s_v = 0.0f64;
844        let mut i = first;
845        while i < len {
846            let v_i = volume[i];
847            let cv_i = close[i] * v_i;
848            f_cv += cv_i;
849            f_v += v_i;
850            s_cv += cv_i;
851            s_v += v_i;
852
853            let n_since_first = i - first + 1;
854            if n_since_first > fast {
855                let j = i - fast;
856                let v_o = volume[j];
857                let cv_o = close[j] * v_o;
858                f_cv -= cv_o;
859                f_v -= v_o;
860            }
861            if n_since_first > slow {
862                let j = i - slow;
863                let v_o = volume[j];
864                let cv_o = close[j] * v_o;
865                s_cv -= cv_o;
866                s_v -= v_o;
867            }
868
869            if i >= macd_warmup_abs {
870                if f_v != 0.0 && s_v != 0.0 {
871                    out[i] = (f_cv / f_v) - (s_cv / s_v);
872                } else {
873                    out[i] = f64::NAN;
874                }
875            }
876            i += 1;
877        }
878
879        return Ok(());
880    }
881
882    let mut close_x_volume = alloc_with_nan_prefix(len, 0);
883    for i in 0..len {
884        if !close[i].is_nan() && !volume[i].is_nan() {
885            close_x_volume[i] = close[i] * volume[i];
886        }
887    }
888
889    let slow_ma_cv = ma_with_kernel(
890        slow_ma_type,
891        MaData::Slice(&close_x_volume),
892        slow,
893        Kernel::Scalar,
894    )
895    .map_err(|e| VwmacdError::MaError(e.to_string()))?;
896    let slow_ma_v = ma_with_kernel(slow_ma_type, MaData::Slice(&volume), slow, Kernel::Scalar)
897        .map_err(|e| VwmacdError::MaError(e.to_string()))?;
898    let fast_ma_cv = ma_with_kernel(
899        fast_ma_type,
900        MaData::Slice(&close_x_volume),
901        fast,
902        Kernel::Scalar,
903    )
904    .map_err(|e| VwmacdError::MaError(e.to_string()))?;
905    let fast_ma_v = ma_with_kernel(fast_ma_type, MaData::Slice(&volume), fast, Kernel::Scalar)
906        .map_err(|e| VwmacdError::MaError(e.to_string()))?;
907
908    let macd_warmup = slow.max(fast);
909    for i in 0..macd_warmup.min(len) {
910        out[i] = f64::NAN;
911    }
912    for i in macd_warmup..len {
913        let sd = slow_ma_v[i];
914        let fd = fast_ma_v[i];
915        if sd != 0.0 && !sd.is_nan() && fd != 0.0 && !fd.is_nan() {
916            out[i] = (fast_ma_cv[i] / fd) - (slow_ma_cv[i] / sd);
917        } else {
918            out[i] = f64::NAN;
919        }
920    }
921    Ok(())
922}
923
924#[inline(always)]
925pub unsafe fn vwmacd_row_scalar(
926    close: &[f64],
927    volume: &[f64],
928    fast: usize,
929    slow: usize,
930    signal: usize,
931    fast_ma_type: &str,
932    slow_ma_type: &str,
933    signal_ma_type: &str,
934    out: &mut [f64],
935) {
936    let _ = vwmacd_scalar_macd_into(
937        close,
938        volume,
939        fast,
940        slow,
941        signal,
942        fast_ma_type,
943        slow_ma_type,
944        signal_ma_type,
945        out,
946    );
947}
948
949#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
950#[inline(always)]
951pub unsafe fn vwmacd_row_avx2(
952    close: &[f64],
953    volume: &[f64],
954    fast: usize,
955    slow: usize,
956    signal: usize,
957    fast_ma_type: &str,
958    slow_ma_type: &str,
959    signal_ma_type: &str,
960    out: &mut [f64],
961) {
962    vwmacd_row_scalar(
963        close,
964        volume,
965        fast,
966        slow,
967        signal,
968        fast_ma_type,
969        slow_ma_type,
970        signal_ma_type,
971        out,
972    );
973}
974
975#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
976#[inline(always)]
977pub unsafe fn vwmacd_row_avx512(
978    close: &[f64],
979    volume: &[f64],
980    fast: usize,
981    slow: usize,
982    signal: usize,
983    fast_ma_type: &str,
984    slow_ma_type: &str,
985    signal_ma_type: &str,
986    out: &mut [f64],
987) {
988    if slow <= 32 {
989        vwmacd_row_avx512_short(
990            close,
991            volume,
992            fast,
993            slow,
994            signal,
995            fast_ma_type,
996            slow_ma_type,
997            signal_ma_type,
998            out,
999        );
1000    } else {
1001        vwmacd_row_avx512_long(
1002            close,
1003            volume,
1004            fast,
1005            slow,
1006            signal,
1007            fast_ma_type,
1008            slow_ma_type,
1009            signal_ma_type,
1010            out,
1011        );
1012    }
1013}
1014
1015#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1016#[inline(always)]
1017pub unsafe fn vwmacd_row_avx512_short(
1018    close: &[f64],
1019    volume: &[f64],
1020    fast: usize,
1021    slow: usize,
1022    signal: usize,
1023    fast_ma_type: &str,
1024    slow_ma_type: &str,
1025    signal_ma_type: &str,
1026    out: &mut [f64],
1027) {
1028    vwmacd_row_scalar(
1029        close,
1030        volume,
1031        fast,
1032        slow,
1033        signal,
1034        fast_ma_type,
1035        slow_ma_type,
1036        signal_ma_type,
1037        out,
1038    );
1039}
1040
1041#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1042#[inline(always)]
1043pub unsafe fn vwmacd_row_avx512_long(
1044    close: &[f64],
1045    volume: &[f64],
1046    fast: usize,
1047    slow: usize,
1048    signal: usize,
1049    fast_ma_type: &str,
1050    slow_ma_type: &str,
1051    signal_ma_type: &str,
1052    out: &mut [f64],
1053) {
1054    vwmacd_row_scalar(
1055        close,
1056        volume,
1057        fast,
1058        slow,
1059        signal,
1060        fast_ma_type,
1061        slow_ma_type,
1062        signal_ma_type,
1063        out,
1064    );
1065}
1066
1067#[inline(always)]
1068pub unsafe fn vwmacd_streaming_scalar(
1069    cv_buffer: &[f64],
1070    v_buffer: &[f64],
1071    fast: usize,
1072    slow: usize,
1073    signal: usize,
1074    fast_ma_type: &str,
1075    slow_ma_type: &str,
1076    signal_ma_type: &str,
1077    buffer_size: usize,
1078    head: usize,
1079    count: usize,
1080    fast_cv_sum: f64,
1081    fast_v_sum: f64,
1082    slow_cv_sum: f64,
1083    slow_v_sum: f64,
1084    macd_buffer: &[f64],
1085    signal_ema_state: Option<f64>,
1086) -> (f64, f64, f64) {
1087    if !(fast_ma_type.eq_ignore_ascii_case("sma")
1088        && slow_ma_type.eq_ignore_ascii_case("sma")
1089        && signal_ma_type.eq_ignore_ascii_case("ema"))
1090    {
1091        return (f64::NAN, f64::NAN, f64::NAN);
1092    }
1093
1094    let fast_ready = count >= fast;
1095    let slow_ready = count >= slow;
1096
1097    let mut macd = f64::NAN;
1098
1099    let vwma_fast = if fast_ready && fast_v_sum != 0.0 {
1100        fast_cv_sum / fast_v_sum
1101    } else {
1102        f64::NAN
1103    };
1104    let vwma_slow = if slow_ready && slow_v_sum != 0.0 {
1105        slow_cv_sum / slow_v_sum
1106    } else {
1107        f64::NAN
1108    };
1109
1110    if vwma_fast.is_finite() && vwma_slow.is_finite() {
1111        macd = vwma_fast - vwma_slow;
1112    }
1113
1114    let mut signal_val = f64::NAN;
1115    let have_signal_window = count >= (slow + signal - 1);
1116
1117    if have_signal_window && macd.is_finite() {
1118        let alpha = 2.0 / (signal as f64 + 1.0);
1119        let beta = 1.0 - alpha;
1120
1121        signal_val = match signal_ema_state {
1122            Some(prev) => beta.mul_add(prev, alpha * macd),
1123            None => {
1124                let macd_idx = (count - 1) % signal;
1125                let mut sum = 0.0;
1126                let mut valid = 0usize;
1127                for i in 0..signal {
1128                    let val = if i == macd_idx { macd } else { macd_buffer[i] };
1129                    if val.is_finite() {
1130                        sum += val;
1131                        valid += 1;
1132                    }
1133                }
1134                if valid == signal {
1135                    sum / signal as f64
1136                } else {
1137                    f64::NAN
1138                }
1139            }
1140        };
1141    }
1142
1143    let hist = if macd.is_finite() && signal_val.is_finite() {
1144        macd - signal_val
1145    } else {
1146        f64::NAN
1147    };
1148    (macd, signal_val, hist)
1149}
1150
1151#[derive(Debug, Clone)]
1152pub struct VwmacdStream {
1153    fast_period: usize,
1154    slow_period: usize,
1155    signal_period: usize,
1156    fast_ma_type: String,
1157    slow_ma_type: String,
1158    signal_ma_type: String,
1159
1160    close_volume_buffer: Vec<f64>,
1161    volume_buffer: Vec<f64>,
1162
1163    close_buffer: Vec<f64>,
1164
1165    macd_buffer: Vec<f64>,
1166
1167    fast_cv_work: Vec<f64>,
1168    fast_v_work: Vec<f64>,
1169    slow_cv_work: Vec<f64>,
1170    slow_v_work: Vec<f64>,
1171    signal_work: Vec<f64>,
1172
1173    fast_cv_sum: f64,
1174    fast_v_sum: f64,
1175    slow_cv_sum: f64,
1176    slow_v_sum: f64,
1177
1178    signal_ema_state: Option<f64>,
1179
1180    head: usize,
1181
1182    count: usize,
1183
1184    fast_filled: bool,
1185    slow_filled: bool,
1186    signal_filled: bool,
1187}
1188
1189impl VwmacdStream {
1190    pub fn try_new(params: VwmacdParams) -> Result<Self, VwmacdError> {
1191        let fast = params.fast_period.unwrap_or(12);
1192        let slow = params.slow_period.unwrap_or(26);
1193        let signal = params.signal_period.unwrap_or(9);
1194        let fast_ma_type = params.fast_ma_type.unwrap_or_else(|| "sma".to_string());
1195        let slow_ma_type = params.slow_ma_type.unwrap_or_else(|| "sma".to_string());
1196        let signal_ma_type = params.signal_ma_type.unwrap_or_else(|| "ema".to_string());
1197
1198        if fast == 0 || slow == 0 || signal == 0 {
1199            return Err(VwmacdError::InvalidPeriod {
1200                fast,
1201                slow,
1202                signal,
1203                data_len: 0,
1204            });
1205        }
1206
1207        let buffer_size = (slow.max(signal) + 10).max(40);
1208
1209        Ok(Self {
1210            fast_period: fast,
1211            slow_period: slow,
1212            signal_period: signal,
1213            fast_ma_type,
1214            slow_ma_type,
1215            signal_ma_type,
1216            close_volume_buffer: vec![0.0; buffer_size],
1217            volume_buffer: vec![0.0; buffer_size],
1218            close_buffer: vec![0.0; buffer_size],
1219            fast_cv_sum: 0.0,
1220            fast_v_sum: 0.0,
1221            slow_cv_sum: 0.0,
1222            slow_v_sum: 0.0,
1223            macd_buffer: vec![f64::NAN; signal],
1224
1225            fast_cv_work: vec![0.0; fast],
1226            fast_v_work: vec![0.0; fast],
1227            slow_cv_work: vec![0.0; slow],
1228            slow_v_work: vec![0.0; slow],
1229            signal_work: vec![0.0; signal],
1230            signal_ema_state: None,
1231            head: 0,
1232            count: 0,
1233            fast_filled: false,
1234            slow_filled: false,
1235            signal_filled: false,
1236        })
1237    }
1238
1239    pub fn update(&mut self, close: f64, volume: f64) -> Option<(f64, f64, f64)> {
1240        let cv = close * volume;
1241        let buf_len = self.close_volume_buffer.len();
1242        let idx = self.count % buf_len;
1243        self.close_volume_buffer[idx] = cv;
1244        self.volume_buffer[idx] = volume;
1245        self.close_buffer[idx] = close;
1246
1247        let default_ma = self.fast_ma_type.eq_ignore_ascii_case("sma")
1248            && self.slow_ma_type.eq_ignore_ascii_case("sma")
1249            && self.signal_ma_type.eq_ignore_ascii_case("ema");
1250
1251        let mut vwma_fast = f64::NAN;
1252        let mut vwma_slow = f64::NAN;
1253
1254        if default_ma {
1255            self.fast_cv_sum += cv;
1256            self.fast_v_sum += volume;
1257            self.slow_cv_sum += cv;
1258            self.slow_v_sum += volume;
1259            let new_count = self.count + 1;
1260
1261            if new_count > self.fast_period {
1262                let prev_idx = (self.count + buf_len - self.fast_period) % buf_len;
1263                self.fast_cv_sum -= self.close_volume_buffer[prev_idx];
1264                self.fast_v_sum -= self.volume_buffer[prev_idx];
1265            }
1266            if new_count > self.slow_period {
1267                let prev_idx = (self.count + buf_len - self.slow_period) % buf_len;
1268                self.slow_cv_sum -= self.close_volume_buffer[prev_idx];
1269                self.slow_v_sum -= self.volume_buffer[prev_idx];
1270            }
1271
1272            let (macd, signal, hist) = unsafe {
1273                vwmacd_streaming_scalar(
1274                    &self.close_volume_buffer,
1275                    &self.volume_buffer,
1276                    self.fast_period,
1277                    self.slow_period,
1278                    self.signal_period,
1279                    &self.fast_ma_type,
1280                    &self.slow_ma_type,
1281                    &self.signal_ma_type,
1282                    buf_len,
1283                    idx,
1284                    new_count,
1285                    self.fast_cv_sum,
1286                    self.fast_v_sum,
1287                    self.slow_cv_sum,
1288                    self.slow_v_sum,
1289                    &self.macd_buffer,
1290                    self.signal_ema_state,
1291                )
1292            };
1293
1294            let macd_idx = (new_count - 1) % self.signal_period;
1295            self.macd_buffer[macd_idx] = macd;
1296
1297            self.count = new_count;
1298
1299            if self.count >= self.slow_period + self.signal_period - 1 {
1300                if signal.is_finite() {
1301                    self.signal_ema_state = Some(signal);
1302                    self.signal_filled = true;
1303                }
1304            }
1305
1306            if macd.is_finite() {
1307                return Some((macd, signal, hist));
1308            } else {
1309                return None;
1310            }
1311        } else {
1312            self.count += 1;
1313
1314            if self.count >= self.fast_period {
1315                let start = if self.count <= buf_len {
1316                    self.count.saturating_sub(self.fast_period)
1317                } else {
1318                    ((idx + 1 + buf_len - self.fast_period) % buf_len)
1319                };
1320                for i in 0..self.fast_period {
1321                    let b = if self.count <= buf_len {
1322                        start + i
1323                    } else {
1324                        (start + i) % buf_len
1325                    };
1326                    self.fast_cv_work[i] = self.close_volume_buffer[b];
1327                    self.fast_v_work[i] = self.volume_buffer[b];
1328                }
1329                if let (Ok(cv_ma), Ok(v_ma)) = (
1330                    ma(
1331                        &self.fast_ma_type,
1332                        MaData::Slice(&self.fast_cv_work),
1333                        self.fast_period,
1334                    ),
1335                    ma(
1336                        &self.fast_ma_type,
1337                        MaData::Slice(&self.fast_v_work),
1338                        self.fast_period,
1339                    ),
1340                ) {
1341                    if let (Some(&cv_val), Some(&v_val)) = (cv_ma.last(), v_ma.last()) {
1342                        if v_val != 0.0 && !v_val.is_nan() {
1343                            vwma_fast = cv_val / v_val;
1344                        }
1345                    }
1346                }
1347            }
1348
1349            if self.count >= self.slow_period {
1350                let start = if self.count <= buf_len {
1351                    self.count.saturating_sub(self.slow_period)
1352                } else {
1353                    ((idx + 1 + buf_len - self.slow_period) % buf_len)
1354                };
1355                for i in 0..self.slow_period {
1356                    let b = if self.count <= buf_len {
1357                        start + i
1358                    } else {
1359                        (start + i) % buf_len
1360                    };
1361                    self.slow_cv_work[i] = self.close_volume_buffer[b];
1362                    self.slow_v_work[i] = self.volume_buffer[b];
1363                }
1364                if let (Ok(cv_ma), Ok(v_ma)) = (
1365                    ma(
1366                        &self.slow_ma_type,
1367                        MaData::Slice(&self.slow_cv_work),
1368                        self.slow_period,
1369                    ),
1370                    ma(
1371                        &self.slow_ma_type,
1372                        MaData::Slice(&self.slow_v_work),
1373                        self.slow_period,
1374                    ),
1375                ) {
1376                    if let (Some(&cv_val), Some(&v_val)) = (cv_ma.last(), v_ma.last()) {
1377                        if v_val != 0.0 && !v_val.is_nan() {
1378                            vwma_slow = cv_val / v_val;
1379                        }
1380                    }
1381                }
1382            }
1383        }
1384
1385        if default_ma {
1386            self.count += 1;
1387        }
1388
1389        let macd = if !vwma_fast.is_nan() && !vwma_slow.is_nan() {
1390            vwma_fast - vwma_slow
1391        } else {
1392            f64::NAN
1393        };
1394
1395        let macd_idx = (self.count - 1) % self.signal_period;
1396        self.macd_buffer[macd_idx] = macd;
1397
1398        let signal = if self.count >= self.slow_period + self.signal_period - 1
1399            && self.signal_ma_type.eq_ignore_ascii_case("ema")
1400        {
1401            if !self.signal_filled {
1402                let macd_idx = (self.count - 1) % self.signal_period;
1403                let oldest = (macd_idx + 1) % self.signal_period;
1404                let mut sum = 0.0;
1405                for i in 0..self.signal_period {
1406                    let src = (oldest + i) % self.signal_period;
1407                    sum += self.macd_buffer[src];
1408                }
1409                let mean = sum / self.signal_period as f64;
1410                self.signal_ema_state = Some(mean);
1411                self.signal_filled = true;
1412                mean
1413            } else {
1414                let alpha = 2.0 / (self.signal_period as f64 + 1.0);
1415                let beta = 1.0 - alpha;
1416                let prev = self.signal_ema_state.unwrap();
1417                let updated = beta.mul_add(prev, alpha * macd);
1418                self.signal_ema_state = Some(updated);
1419                updated
1420            }
1421        } else if self.count >= self.slow_period + self.signal_period - 1 {
1422            let macd_idx = (self.count - 1) % self.signal_period;
1423            let oldest = (macd_idx + 1) % self.signal_period;
1424            for i in 0..self.signal_period {
1425                let src = (oldest + i) % self.signal_period;
1426                self.signal_work[i] = self.macd_buffer[src];
1427            }
1428            if let Ok(signal_ma) = ma(
1429                &self.signal_ma_type,
1430                MaData::Slice(&self.signal_work),
1431                self.signal_period,
1432            ) {
1433                signal_ma.last().copied().unwrap_or(f64::NAN)
1434            } else {
1435                f64::NAN
1436            }
1437        } else {
1438            f64::NAN
1439        };
1440
1441        let hist = if !macd.is_nan() && !signal.is_nan() {
1442            macd - signal
1443        } else {
1444            f64::NAN
1445        };
1446
1447        if !macd.is_nan() {
1448            Some((macd, signal, hist))
1449        } else {
1450            None
1451        }
1452    }
1453}
1454
1455fn vwmacd_prepare<'a>(
1456    input: &'a VwmacdInput,
1457    kernel: Kernel,
1458) -> Result<
1459    (
1460        &'a [f64],
1461        &'a [f64],
1462        usize,
1463        usize,
1464        usize,
1465        &'a str,
1466        &'a str,
1467        &'a str,
1468        usize,
1469        usize,
1470        usize,
1471        Kernel,
1472    ),
1473    VwmacdError,
1474> {
1475    let (close, volume) = match &input.data {
1476        VwmacdData::Candles {
1477            candles,
1478            close_source,
1479            volume_source,
1480        } => (
1481            source_type(candles, close_source),
1482            source_type(candles, volume_source),
1483        ),
1484        VwmacdData::Slices { close, volume } => (*close, *volume),
1485    };
1486
1487    let len = close.len();
1488    if len == 0 {
1489        return Err(VwmacdError::EmptyInputData);
1490    }
1491    if volume.len() != len {
1492        return Err(VwmacdError::OutputLengthMismatch {
1493            expected: len,
1494            got: volume.len(),
1495        });
1496    }
1497
1498    if !close.iter().any(|x| !x.is_nan()) || !volume.iter().any(|x| !x.is_nan()) {
1499        return Err(VwmacdError::AllValuesNaN);
1500    }
1501
1502    let fast = input.get_fast();
1503    let slow = input.get_slow();
1504    let signal = input.get_signal();
1505
1506    if fast == 0 || slow == 0 || signal == 0 || fast > len || slow > len || signal > len {
1507        return Err(VwmacdError::InvalidPeriod {
1508            fast,
1509            slow,
1510            signal,
1511            data_len: len,
1512        });
1513    }
1514
1515    let first = first_valid_pair(close, volume).ok_or(VwmacdError::AllValuesNaN)?;
1516
1517    if len - first < slow {
1518        return Err(VwmacdError::NotEnoughValidData {
1519            needed: slow,
1520            valid: len - first,
1521        });
1522    }
1523
1524    let macd_warmup_abs = first + fast.max(slow) - 1;
1525    let total_warmup_abs = macd_warmup_abs + signal - 1;
1526
1527    let chosen = match kernel {
1528        Kernel::Auto => detect_best_kernel(),
1529        k => k,
1530    };
1531
1532    let chosen = if input.get_fast_ma_type().eq_ignore_ascii_case("sma")
1533        && input.get_slow_ma_type().eq_ignore_ascii_case("sma")
1534        && input.get_signal_ma_type().eq_ignore_ascii_case("ema")
1535    {
1536        Kernel::Scalar
1537    } else {
1538        chosen
1539    };
1540
1541    Ok((
1542        close,
1543        volume,
1544        fast,
1545        slow,
1546        signal,
1547        input.get_fast_ma_type(),
1548        input.get_slow_ma_type(),
1549        input.get_signal_ma_type(),
1550        first,
1551        macd_warmup_abs,
1552        total_warmup_abs,
1553        chosen,
1554    ))
1555}
1556
1557#[inline(always)]
1558fn vwmacd_compute_into(
1559    close: &[f64],
1560    volume: &[f64],
1561    fast: usize,
1562    slow: usize,
1563    signal: usize,
1564    fast_ma_type: &str,
1565    slow_ma_type: &str,
1566    signal_ma_type: &str,
1567    first: usize,
1568    macd_warmup_abs: usize,
1569    total_warmup_abs: usize,
1570    kernel: Kernel,
1571    macd_out: &mut [f64],
1572    signal_out: &mut [f64],
1573    hist_out: &mut [f64],
1574) -> Result<(), VwmacdError> {
1575    let len = close.len();
1576
1577    if kernel == Kernel::Scalar
1578        && fast_ma_type.eq_ignore_ascii_case("sma")
1579        && slow_ma_type.eq_ignore_ascii_case("sma")
1580        && signal_ma_type.eq_ignore_ascii_case("ema")
1581    {
1582        unsafe {
1583            return vwmacd_scalar_classic(
1584                close,
1585                volume,
1586                fast,
1587                slow,
1588                signal,
1589                fast_ma_type,
1590                slow_ma_type,
1591                signal_ma_type,
1592                first,
1593                macd_warmup_abs,
1594                total_warmup_abs,
1595                macd_out,
1596                signal_out,
1597                hist_out,
1598            );
1599        }
1600    }
1601
1602    #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1603    if (kernel == Kernel::Avx2 || kernel == Kernel::Avx512)
1604        && fast_ma_type.eq_ignore_ascii_case("sma")
1605        && slow_ma_type.eq_ignore_ascii_case("sma")
1606        && signal_ma_type.eq_ignore_ascii_case("ema")
1607    {
1608        unsafe {
1609            if kernel == Kernel::Avx512 {
1610                return vwmacd_classic_into_avx512(
1611                    close,
1612                    volume,
1613                    fast,
1614                    slow,
1615                    signal,
1616                    first,
1617                    macd_warmup_abs,
1618                    total_warmup_abs,
1619                    macd_out,
1620                    signal_out,
1621                    hist_out,
1622                );
1623            } else {
1624                return vwmacd_classic_into_avx2(
1625                    close,
1626                    volume,
1627                    fast,
1628                    slow,
1629                    signal,
1630                    first,
1631                    macd_warmup_abs,
1632                    total_warmup_abs,
1633                    macd_out,
1634                    signal_out,
1635                    hist_out,
1636                );
1637            }
1638        }
1639    }
1640
1641    let mut cv = alloc_with_nan_prefix(len, first);
1642    for i in first..len {
1643        let c = close[i];
1644        let v = volume[i];
1645        if !c.is_nan() && !v.is_nan() {
1646            cv[i] = c * v;
1647        }
1648    }
1649
1650    let mut slow_cv = alloc_with_nan_prefix(len, first + slow - 1);
1651    let mut slow_v = alloc_with_nan_prefix(len, first + slow - 1);
1652    let mut fast_cv = alloc_with_nan_prefix(len, first + fast - 1);
1653    let mut fast_v = alloc_with_nan_prefix(len, first + fast - 1);
1654
1655    let slow_cv_result = ma_with_kernel(slow_ma_type, MaData::Slice(&cv), slow, kernel)
1656        .map_err(|e| VwmacdError::MaError(e.to_string()))?;
1657    let slow_v_result = ma_with_kernel(slow_ma_type, MaData::Slice(&volume), slow, kernel)
1658        .map_err(|e| VwmacdError::MaError(e.to_string()))?;
1659
1660    slow_cv.copy_from_slice(&slow_cv_result);
1661    slow_v.copy_from_slice(&slow_v_result);
1662
1663    let fast_cv_result = ma_with_kernel(fast_ma_type, MaData::Slice(&cv), fast, kernel)
1664        .map_err(|e| VwmacdError::MaError(e.to_string()))?;
1665    let fast_v_result = ma_with_kernel(fast_ma_type, MaData::Slice(&volume), fast, kernel)
1666        .map_err(|e| VwmacdError::MaError(e.to_string()))?;
1667
1668    fast_cv.copy_from_slice(&fast_cv_result);
1669    fast_v.copy_from_slice(&fast_v_result);
1670
1671    for i in 0..macd_warmup_abs {
1672        macd_out[i] = f64::NAN;
1673    }
1674    for i in macd_warmup_abs..len {
1675        let sd = slow_v[i];
1676        let fd = fast_v[i];
1677        if sd != 0.0 && !sd.is_nan() && fd != 0.0 && !fd.is_nan() {
1678            macd_out[i] = (fast_cv[i] / fd) - (slow_cv[i] / sd);
1679        } else {
1680            macd_out[i] = f64::NAN;
1681        }
1682    }
1683
1684    let signal_result = ma_with_kernel(signal_ma_type, MaData::Slice(&macd_out), signal, kernel)
1685        .map_err(|e| VwmacdError::MaError(e.to_string()))?;
1686
1687    signal_out.copy_from_slice(&signal_result);
1688
1689    for i in 0..total_warmup_abs {
1690        signal_out[i] = f64::NAN;
1691    }
1692
1693    for i in 0..total_warmup_abs {
1694        hist_out[i] = f64::NAN;
1695    }
1696    for i in total_warmup_abs..len {
1697        let m = macd_out[i];
1698        let s = signal_out[i];
1699        hist_out[i] = if !m.is_nan() && !s.is_nan() {
1700            m - s
1701        } else {
1702            f64::NAN
1703        };
1704    }
1705
1706    Ok(())
1707}
1708
1709#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1710#[inline]
1711unsafe fn vwmacd_classic_into_avx2(
1712    close: &[f64],
1713    volume: &[f64],
1714    fast: usize,
1715    slow: usize,
1716    signal: usize,
1717    first: usize,
1718    macd_warmup_abs: usize,
1719    total_warmup_abs: usize,
1720    macd_out: &mut [f64],
1721    signal_out: &mut [f64],
1722    hist_out: &mut [f64],
1723) -> Result<(), VwmacdError> {
1724    let len = close.len();
1725    for i in 0..macd_warmup_abs.min(len) {
1726        macd_out[i] = f64::NAN;
1727    }
1728
1729    let mut cv = Vec::<f64>::with_capacity(len);
1730    cv.set_len(len);
1731    {
1732        let ptr_c = close.as_ptr();
1733        let ptr_v = volume.as_ptr();
1734        let ptr_o = cv.as_mut_ptr();
1735        let mut i = first;
1736        let lanes = 4usize;
1737        let vec_end = first + ((len - first) / lanes) * lanes;
1738        while i + lanes <= vec_end {
1739            let c = _mm256_loadu_pd(ptr_c.add(i));
1740            let v = _mm256_loadu_pd(ptr_v.add(i));
1741            let prod = _mm256_mul_pd(c, v);
1742            _mm256_storeu_pd(ptr_o.add(i), prod);
1743            i += lanes;
1744        }
1745        while i < len {
1746            *ptr_o.add(i) = *ptr_c.add(i) * *ptr_v.add(i);
1747            i += 1;
1748        }
1749    }
1750
1751    let mut f_cv = 0.0f64;
1752    let mut f_v = 0.0f64;
1753    let mut s_cv = 0.0f64;
1754    let mut s_v = 0.0f64;
1755    let mut i = first;
1756    while i < len {
1757        let v_i = volume[i];
1758        let cv_i = cv[i];
1759        f_cv += cv_i;
1760        f_v += v_i;
1761        s_cv += cv_i;
1762        s_v += v_i;
1763
1764        let n_since_first = i - first + 1;
1765        if n_since_first > fast {
1766            let j = i - fast;
1767            let v_o = volume[j];
1768            let cv_o = cv[j];
1769            f_cv -= cv_o;
1770            f_v -= v_o;
1771        }
1772        if n_since_first > slow {
1773            let j = i - slow;
1774            let v_o = volume[j];
1775            let cv_o = cv[j];
1776            s_cv -= cv_o;
1777            s_v -= v_o;
1778        }
1779
1780        if i >= macd_warmup_abs {
1781            if f_v != 0.0 && s_v != 0.0 {
1782                macd_out[i] = (f_cv / f_v) - (s_cv / s_v);
1783            } else {
1784                macd_out[i] = f64::NAN;
1785            }
1786        }
1787        i += 1;
1788    }
1789
1790    if macd_warmup_abs < len {
1791        let alpha = 2.0f64 / (signal as f64 + 1.0);
1792        let beta = 1.0f64 - alpha;
1793        let start = macd_warmup_abs;
1794        let warmup_end = (start + signal).min(len);
1795        if start < len {
1796            let mut mean = macd_out[start];
1797            signal_out[start] = mean;
1798            let mut count = 1usize;
1799            let mut k = start + 1;
1800            while k < warmup_end {
1801                let x = macd_out[k];
1802                count += 1;
1803                mean = ((count as f64 - 1.0) * mean + x) / (count as f64);
1804                signal_out[k] = mean;
1805                k += 1;
1806            }
1807            let mut prev = mean;
1808            let mut t = warmup_end;
1809            while t < len {
1810                let x = macd_out[t];
1811                prev = beta.mul_add(prev, alpha * x);
1812                signal_out[t] = prev;
1813                t += 1;
1814            }
1815        }
1816    }
1817
1818    for i in 0..total_warmup_abs.min(len) {
1819        signal_out[i] = f64::NAN;
1820        hist_out[i] = f64::NAN;
1821    }
1822    for i in total_warmup_abs..len {
1823        let m = macd_out[i];
1824        let s = signal_out[i];
1825        hist_out[i] = if !m.is_nan() && !s.is_nan() {
1826            m - s
1827        } else {
1828            f64::NAN
1829        };
1830    }
1831
1832    Ok(())
1833}
1834
1835#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1836#[inline]
1837unsafe fn vwmacd_classic_into_avx512(
1838    close: &[f64],
1839    volume: &[f64],
1840    fast: usize,
1841    slow: usize,
1842    signal: usize,
1843    first: usize,
1844    macd_warmup_abs: usize,
1845    total_warmup_abs: usize,
1846    macd_out: &mut [f64],
1847    signal_out: &mut [f64],
1848    hist_out: &mut [f64],
1849) -> Result<(), VwmacdError> {
1850    let len = close.len();
1851    for i in 0..macd_warmup_abs.min(len) {
1852        macd_out[i] = f64::NAN;
1853    }
1854
1855    let mut cv = Vec::<f64>::with_capacity(len);
1856    cv.set_len(len);
1857    {
1858        let ptr_c = close.as_ptr();
1859        let ptr_v = volume.as_ptr();
1860        let ptr_o = cv.as_mut_ptr();
1861        let lanes = 8usize;
1862        let mut i = first;
1863        let vec_end = first + ((len - first) / lanes) * lanes;
1864        while i + lanes <= vec_end {
1865            let c = _mm512_loadu_pd(ptr_c.add(i));
1866            let v = _mm512_loadu_pd(ptr_v.add(i));
1867            let prod = _mm512_mul_pd(c, v);
1868            _mm512_storeu_pd(ptr_o.add(i), prod);
1869            i += lanes;
1870        }
1871        while i < len {
1872            *ptr_o.add(i) = *ptr_c.add(i) * *ptr_v.add(i);
1873            i += 1;
1874        }
1875    }
1876
1877    let mut f_cv = 0.0f64;
1878    let mut f_v = 0.0f64;
1879    let mut s_cv = 0.0f64;
1880    let mut s_v = 0.0f64;
1881    let mut i = first;
1882    while i < len {
1883        let v_i = volume[i];
1884        let cv_i = cv[i];
1885        f_cv += cv_i;
1886        f_v += v_i;
1887        s_cv += cv_i;
1888        s_v += v_i;
1889
1890        let n_since_first = i - first + 1;
1891        if n_since_first > fast {
1892            let j = i - fast;
1893            let v_o = volume[j];
1894            let cv_o = cv[j];
1895            f_cv -= cv_o;
1896            f_v -= v_o;
1897        }
1898        if n_since_first > slow {
1899            let j = i - slow;
1900            let v_o = volume[j];
1901            let cv_o = cv[j];
1902            s_cv -= cv_o;
1903            s_v -= v_o;
1904        }
1905
1906        if i >= macd_warmup_abs {
1907            if f_v != 0.0 && s_v != 0.0 {
1908                macd_out[i] = (f_cv / f_v) - (s_cv / s_v);
1909            } else {
1910                macd_out[i] = f64::NAN;
1911            }
1912        }
1913        i += 1;
1914    }
1915
1916    if macd_warmup_abs < len {
1917        let alpha = 2.0f64 / (signal as f64 + 1.0);
1918        let beta = 1.0f64 - alpha;
1919        let start = macd_warmup_abs;
1920        let warmup_end = (start + signal).min(len);
1921        if start < len {
1922            let mut mean = macd_out[start];
1923            signal_out[start] = mean;
1924            let mut count = 1usize;
1925            let mut k = start + 1;
1926            while k < warmup_end {
1927                let x = macd_out[k];
1928                count += 1;
1929                mean = ((count as f64 - 1.0) * mean + x) / (count as f64);
1930                signal_out[k] = mean;
1931                k += 1;
1932            }
1933            let mut prev = mean;
1934            let mut t = warmup_end;
1935            while t < len {
1936                let x = macd_out[t];
1937                prev = beta.mul_add(prev, alpha * x);
1938                signal_out[t] = prev;
1939                t += 1;
1940            }
1941        }
1942    }
1943
1944    for i in 0..total_warmup_abs.min(len) {
1945        signal_out[i] = f64::NAN;
1946        hist_out[i] = f64::NAN;
1947    }
1948    for i in total_warmup_abs..len {
1949        let m = macd_out[i];
1950        let s = signal_out[i];
1951        hist_out[i] = if !m.is_nan() && !s.is_nan() {
1952            m - s
1953        } else {
1954            f64::NAN
1955        };
1956    }
1957
1958    Ok(())
1959}
1960
1961#[derive(Clone, Debug)]
1962pub struct VwmacdBatchRange {
1963    pub fast: (usize, usize, usize),
1964    pub slow: (usize, usize, usize),
1965    pub signal: (usize, usize, usize),
1966    pub fast_ma_type: String,
1967    pub slow_ma_type: String,
1968    pub signal_ma_type: String,
1969}
1970
1971impl Default for VwmacdBatchRange {
1972    fn default() -> Self {
1973        Self {
1974            fast: (12, 12, 0),
1975            slow: (26, 275, 1),
1976            signal: (9, 9, 0),
1977            fast_ma_type: "sma".to_string(),
1978            slow_ma_type: "sma".to_string(),
1979            signal_ma_type: "ema".to_string(),
1980        }
1981    }
1982}
1983
1984#[derive(Clone, Debug, Default)]
1985pub struct VwmacdBatchBuilder {
1986    range: VwmacdBatchRange,
1987    kernel: Kernel,
1988}
1989
1990impl VwmacdBatchBuilder {
1991    pub fn new() -> Self {
1992        Self::default()
1993    }
1994    pub fn kernel(mut self, k: Kernel) -> Self {
1995        self.kernel = k;
1996        self
1997    }
1998    #[inline]
1999    pub fn fast_range(mut self, start: usize, end: usize, step: usize) -> Self {
2000        self.range.fast = (start, end, step);
2001        self
2002    }
2003    #[inline]
2004    pub fn slow_range(mut self, start: usize, end: usize, step: usize) -> Self {
2005        self.range.slow = (start, end, step);
2006        self
2007    }
2008    #[inline]
2009    pub fn signal_range(mut self, start: usize, end: usize, step: usize) -> Self {
2010        self.range.signal = (start, end, step);
2011        self
2012    }
2013    #[inline]
2014    pub fn fast_ma_type(mut self, ma_type: String) -> Self {
2015        self.range.fast_ma_type = ma_type;
2016        self
2017    }
2018    #[inline]
2019    pub fn slow_ma_type(mut self, ma_type: String) -> Self {
2020        self.range.slow_ma_type = ma_type;
2021        self
2022    }
2023    #[inline]
2024    pub fn signal_ma_type(mut self, ma_type: String) -> Self {
2025        self.range.signal_ma_type = ma_type;
2026        self
2027    }
2028    #[inline]
2029    pub fn apply_slices(
2030        self,
2031        close: &[f64],
2032        volume: &[f64],
2033    ) -> Result<VwmacdBatchOutput, VwmacdError> {
2034        vwmacd_batch_with_kernel(close, volume, &self.range, self.kernel)
2035    }
2036}
2037
2038#[inline(always)]
2039fn expand_grid(r: &VwmacdBatchRange) -> Result<Vec<VwmacdParams>, VwmacdError> {
2040    fn axis_usize((start, end, step): (usize, usize, usize)) -> Result<Vec<usize>, VwmacdError> {
2041        if step == 0 || start == end {
2042            return Ok(vec![start]);
2043        }
2044        if start < end {
2045            let st = step.max(1);
2046            let mut v = Vec::new();
2047            let mut cur = start;
2048            while cur <= end {
2049                v.push(cur);
2050                let next = cur.saturating_add(st);
2051                if next == cur {
2052                    break;
2053                }
2054                cur = next;
2055            }
2056            if v.is_empty() {
2057                return Err(VwmacdError::InvalidRange {
2058                    start: start.to_string(),
2059                    end: end.to_string(),
2060                    step: step.to_string(),
2061                });
2062            }
2063            return Ok(v);
2064        }
2065        let mut v = Vec::new();
2066        let mut x = start as isize;
2067        let end_i = end as isize;
2068        let st = (step as isize).max(1);
2069        while x >= end_i {
2070            v.push(x as usize);
2071            x -= st;
2072        }
2073        if v.is_empty() {
2074            return Err(VwmacdError::InvalidRange {
2075                start: start.to_string(),
2076                end: end.to_string(),
2077                step: step.to_string(),
2078            });
2079        }
2080        Ok(v)
2081    }
2082
2083    let fasts = axis_usize(r.fast)?;
2084    let slows = axis_usize(r.slow)?;
2085    let signals = axis_usize(r.signal)?;
2086
2087    let cap = fasts
2088        .len()
2089        .checked_mul(slows.len())
2090        .and_then(|x| x.checked_mul(signals.len()))
2091        .ok_or_else(|| VwmacdError::InvalidRange {
2092            start: "cap".into(),
2093            end: "overflow".into(),
2094            step: "mul".into(),
2095        })?;
2096
2097    let mut out = Vec::with_capacity(cap);
2098    for &f in &fasts {
2099        for &s in &slows {
2100            for &g in &signals {
2101                out.push(VwmacdParams {
2102                    fast_period: Some(f),
2103                    slow_period: Some(s),
2104                    signal_period: Some(g),
2105                    fast_ma_type: Some(r.fast_ma_type.clone()),
2106                    slow_ma_type: Some(r.slow_ma_type.clone()),
2107                    signal_ma_type: Some(r.signal_ma_type.clone()),
2108                });
2109            }
2110        }
2111    }
2112    Ok(out)
2113}
2114
2115#[derive(Clone, Debug)]
2116pub struct VwmacdBatchOutput {
2117    pub macd: Vec<f64>,
2118    pub signal: Vec<f64>,
2119    pub hist: Vec<f64>,
2120    pub params: Vec<VwmacdParams>,
2121    pub rows: usize,
2122    pub cols: usize,
2123}
2124
2125impl VwmacdBatchOutput {
2126    pub fn values_for(&self, p: &VwmacdParams) -> Option<(&[f64], &[f64], &[f64])> {
2127        let row = self.params.iter().position(|c| {
2128            c.fast_period == p.fast_period
2129                && c.slow_period == p.slow_period
2130                && c.signal_period == p.signal_period
2131                && c.fast_ma_type.as_deref() == p.fast_ma_type.as_deref()
2132                && c.slow_ma_type.as_deref() == p.slow_ma_type.as_deref()
2133                && c.signal_ma_type.as_deref() == p.signal_ma_type.as_deref()
2134        })?;
2135        let start = row * self.cols;
2136        Some((
2137            &self.macd[start..start + self.cols],
2138            &self.signal[start..start + self.cols],
2139            &self.hist[start..start + self.cols],
2140        ))
2141    }
2142}
2143
2144pub fn vwmacd_batch_with_kernel(
2145    close: &[f64],
2146    volume: &[f64],
2147    sweep: &VwmacdBatchRange,
2148    k: Kernel,
2149) -> Result<VwmacdBatchOutput, VwmacdError> {
2150    let kernel = match k {
2151        Kernel::Auto => detect_best_batch_kernel(),
2152        other if other.is_batch() => other,
2153        other => {
2154            return Err(VwmacdError::InvalidKernelForBatch(other));
2155        }
2156    };
2157    let simd = match kernel {
2158        Kernel::Avx512Batch => Kernel::Avx512,
2159        Kernel::Avx2Batch => Kernel::Avx2,
2160        Kernel::ScalarBatch => Kernel::Scalar,
2161
2162        Kernel::Scalar => Kernel::Scalar,
2163        Kernel::Avx2 => Kernel::Avx2,
2164        Kernel::Avx512 => Kernel::Avx512,
2165        _ => Kernel::Scalar,
2166    };
2167    vwmacd_batch_par_slice(close, volume, sweep, simd)
2168}
2169
2170#[inline(always)]
2171pub fn vwmacd_batch_slice(
2172    close: &[f64],
2173    volume: &[f64],
2174    sweep: &VwmacdBatchRange,
2175    kern: Kernel,
2176) -> Result<VwmacdBatchOutput, VwmacdError> {
2177    vwmacd_batch_inner(close, volume, sweep, kern, false)
2178}
2179
2180#[inline(always)]
2181pub fn vwmacd_batch_par_slice(
2182    close: &[f64],
2183    volume: &[f64],
2184    sweep: &VwmacdBatchRange,
2185    kern: Kernel,
2186) -> Result<VwmacdBatchOutput, VwmacdError> {
2187    vwmacd_batch_inner(close, volume, sweep, kern, true)
2188}
2189
2190#[inline(always)]
2191fn vwmacd_batch_inner(
2192    close: &[f64],
2193    volume: &[f64],
2194    sweep: &VwmacdBatchRange,
2195    kern: Kernel,
2196    parallel: bool,
2197) -> Result<VwmacdBatchOutput, VwmacdError> {
2198    let params = expand_grid(sweep)?;
2199    let len = close.len();
2200    if len == 0 {
2201        return Err(VwmacdError::EmptyInputData);
2202    }
2203    if volume.len() != len {
2204        return Err(VwmacdError::OutputLengthMismatch {
2205            expected: len,
2206            got: volume.len(),
2207        });
2208    }
2209    let rows = params.len();
2210    let cols = len;
2211    rows.checked_mul(cols)
2212        .ok_or_else(|| VwmacdError::InvalidRange {
2213            start: "rows".into(),
2214            end: "cols".into(),
2215            step: "mul".into(),
2216        })?;
2217
2218    let first = first_valid_pair(close, volume).ok_or(VwmacdError::AllValuesNaN)?;
2219
2220    let warmups: Vec<usize> = params
2221        .iter()
2222        .map(|p| {
2223            let f = p.fast_period.unwrap_or(12);
2224            let s = p.slow_period.unwrap_or(26);
2225            let g = p.signal_period.unwrap_or(9);
2226            first + f.max(s) - 1 + g - 1
2227        })
2228        .collect();
2229
2230    let mut macd_mu = make_uninit_matrix(rows, cols);
2231    let mut signal_mu = make_uninit_matrix(rows, cols);
2232    let mut hist_mu = make_uninit_matrix(rows, cols);
2233
2234    unsafe {
2235        init_matrix_prefixes(
2236            &mut macd_mu,
2237            cols,
2238            &params
2239                .iter()
2240                .map(|p| {
2241                    let f = p.fast_period.unwrap_or(12);
2242                    let s = p.slow_period.unwrap_or(26);
2243                    first + f.max(s) - 1
2244                })
2245                .collect::<Vec<_>>(),
2246        );
2247        init_matrix_prefixes(&mut signal_mu, cols, &warmups);
2248        init_matrix_prefixes(&mut hist_mu, cols, &warmups);
2249    }
2250
2251    let actual = match kern {
2252        Kernel::Auto => detect_best_batch_kernel(),
2253        k => k,
2254    };
2255    let simd = match actual {
2256        Kernel::Avx512Batch => Kernel::Avx512,
2257        Kernel::Avx2Batch => Kernel::Avx2,
2258        Kernel::ScalarBatch => Kernel::Scalar,
2259        k => k,
2260    };
2261
2262    let do_row = |row: usize,
2263                  macd_row_mu: &mut [MaybeUninit<f64>],
2264                  signal_row_mu: &mut [MaybeUninit<f64>],
2265                  hist_row_mu: &mut [MaybeUninit<f64>]| {
2266        let p = &params[row];
2267        let f = p.fast_period.unwrap();
2268        let s = p.slow_period.unwrap();
2269        let g = p.signal_period.unwrap();
2270        let fmt = p.fast_ma_type.as_deref().unwrap_or("sma");
2271        let smt = p.slow_ma_type.as_deref().unwrap_or("sma");
2272        let sigt = p.signal_ma_type.as_deref().unwrap_or("ema");
2273
2274        let macd_row = unsafe {
2275            std::slice::from_raw_parts_mut(macd_row_mu.as_mut_ptr() as *mut f64, macd_row_mu.len())
2276        };
2277        let signal_row = unsafe {
2278            std::slice::from_raw_parts_mut(
2279                signal_row_mu.as_mut_ptr() as *mut f64,
2280                signal_row_mu.len(),
2281            )
2282        };
2283        let hist_row = unsafe {
2284            std::slice::from_raw_parts_mut(hist_row_mu.as_mut_ptr() as *mut f64, hist_row_mu.len())
2285        };
2286
2287        let macd_warmup_abs = first + f.max(s) - 1;
2288        let total_warmup_abs = macd_warmup_abs + g - 1;
2289
2290        vwmacd_compute_into(
2291            close,
2292            volume,
2293            f,
2294            s,
2295            g,
2296            fmt,
2297            smt,
2298            sigt,
2299            first,
2300            macd_warmup_abs,
2301            total_warmup_abs,
2302            simd,
2303            macd_row,
2304            signal_row,
2305            hist_row,
2306        )
2307        .unwrap();
2308    };
2309
2310    if parallel {
2311        #[cfg(not(target_arch = "wasm32"))]
2312        {
2313            macd_mu
2314                .par_chunks_mut(cols)
2315                .zip(signal_mu.par_chunks_mut(cols))
2316                .zip(hist_mu.par_chunks_mut(cols))
2317                .enumerate()
2318                .for_each(|(row, ((m, s), h))| do_row(row, m, s, h));
2319        }
2320        #[cfg(target_arch = "wasm32")]
2321        {
2322            for (row, ((m, s), h)) in macd_mu
2323                .chunks_mut(cols)
2324                .zip(signal_mu.chunks_mut(cols))
2325                .zip(hist_mu.chunks_mut(cols))
2326                .enumerate()
2327            {
2328                do_row(row, m, s, h);
2329            }
2330        }
2331    } else {
2332        for (row, ((m, s), h)) in macd_mu
2333            .chunks_mut(cols)
2334            .zip(signal_mu.chunks_mut(cols))
2335            .zip(hist_mu.chunks_mut(cols))
2336            .enumerate()
2337        {
2338            do_row(row, m, s, h);
2339        }
2340    }
2341
2342    let mut mdrop = core::mem::ManuallyDrop::new(macd_mu);
2343    let macd = unsafe {
2344        Vec::from_raw_parts(
2345            mdrop.as_mut_ptr() as *mut f64,
2346            mdrop.len(),
2347            mdrop.capacity(),
2348        )
2349    };
2350    let mut sdrop = core::mem::ManuallyDrop::new(signal_mu);
2351    let signal = unsafe {
2352        Vec::from_raw_parts(
2353            sdrop.as_mut_ptr() as *mut f64,
2354            sdrop.len(),
2355            sdrop.capacity(),
2356        )
2357    };
2358    let mut hdrop = core::mem::ManuallyDrop::new(hist_mu);
2359    let hist = unsafe {
2360        Vec::from_raw_parts(
2361            hdrop.as_mut_ptr() as *mut f64,
2362            hdrop.len(),
2363            hdrop.capacity(),
2364        )
2365    };
2366
2367    Ok(VwmacdBatchOutput {
2368        macd,
2369        signal,
2370        hist,
2371        params,
2372        rows,
2373        cols,
2374    })
2375}
2376
2377#[inline(always)]
2378fn vwmacd_batch_inner_into(
2379    close: &[f64],
2380    volume: &[f64],
2381    sweep: &VwmacdBatchRange,
2382    kern: Kernel,
2383    parallel: bool,
2384    macd_out: &mut [f64],
2385    signal_out: &mut [f64],
2386    hist_out: &mut [f64],
2387) -> Result<Vec<VwmacdParams>, VwmacdError> {
2388    let combos = expand_grid(sweep)?;
2389    let rows = combos.len();
2390    let cols = close.len();
2391
2392    if cols == 0 {
2393        return Err(VwmacdError::EmptyInputData);
2394    }
2395    if volume.len() != cols {
2396        return Err(VwmacdError::OutputLengthMismatch {
2397            expected: cols,
2398            got: volume.len(),
2399        });
2400    }
2401
2402    let expected = rows
2403        .checked_mul(cols)
2404        .ok_or_else(|| VwmacdError::InvalidRange {
2405            start: "rows".into(),
2406            end: "cols".into(),
2407            step: "mul".into(),
2408        })?;
2409    if macd_out.len() != expected || signal_out.len() != expected || hist_out.len() != expected {
2410        let got = macd_out.len().min(signal_out.len()).min(hist_out.len());
2411        return Err(VwmacdError::OutputLengthMismatch { expected, got });
2412    }
2413
2414    let first = first_valid_pair(close, volume).ok_or(VwmacdError::AllValuesNaN)?;
2415
2416    let macd_mu = unsafe {
2417        std::slice::from_raw_parts_mut(
2418            macd_out.as_mut_ptr() as *mut MaybeUninit<f64>,
2419            macd_out.len(),
2420        )
2421    };
2422    let signal_mu = unsafe {
2423        std::slice::from_raw_parts_mut(
2424            signal_out.as_mut_ptr() as *mut MaybeUninit<f64>,
2425            signal_out.len(),
2426        )
2427    };
2428    let hist_mu = unsafe {
2429        std::slice::from_raw_parts_mut(
2430            hist_out.as_mut_ptr() as *mut MaybeUninit<f64>,
2431            hist_out.len(),
2432        )
2433    };
2434
2435    let macd_warmups: Vec<usize> = combos
2436        .iter()
2437        .map(|p| {
2438            let f = p.fast_period.unwrap_or(12);
2439            let s = p.slow_period.unwrap_or(26);
2440            first + f.max(s) - 1
2441        })
2442        .collect();
2443    let total_warmups: Vec<usize> = combos
2444        .iter()
2445        .map(|p| {
2446            let f = p.fast_period.unwrap_or(12);
2447            let s = p.slow_period.unwrap_or(26);
2448            let g = p.signal_period.unwrap_or(9);
2449            first + f.max(s) - 1 + g - 1
2450        })
2451        .collect();
2452
2453    unsafe {
2454        init_matrix_prefixes(macd_mu, cols, &macd_warmups);
2455        init_matrix_prefixes(signal_mu, cols, &total_warmups);
2456        init_matrix_prefixes(hist_mu, cols, &total_warmups);
2457    }
2458
2459    let actual = match kern {
2460        Kernel::Auto => detect_best_batch_kernel(),
2461        k => k,
2462    };
2463    let simd = match actual {
2464        Kernel::Avx512Batch => Kernel::Avx512,
2465        Kernel::Avx2Batch => Kernel::Avx2,
2466        Kernel::ScalarBatch => Kernel::Scalar,
2467        k => k,
2468    };
2469
2470    let do_row = |row: usize,
2471                  m: &mut [MaybeUninit<f64>],
2472                  s: &mut [MaybeUninit<f64>],
2473                  h: &mut [MaybeUninit<f64>]| {
2474        let p = &combos[row];
2475        let f = p.fast_period.unwrap();
2476        let sl = p.slow_period.unwrap();
2477        let g = p.signal_period.unwrap();
2478        let fmt = p.fast_ma_type.as_deref().unwrap_or("sma");
2479        let smt = p.slow_ma_type.as_deref().unwrap_or("sma");
2480        let sigt = p.signal_ma_type.as_deref().unwrap_or("ema");
2481
2482        let macd_row = unsafe { std::slice::from_raw_parts_mut(m.as_mut_ptr() as *mut f64, cols) };
2483        let signal_row =
2484            unsafe { std::slice::from_raw_parts_mut(s.as_mut_ptr() as *mut f64, cols) };
2485        let hist_row = unsafe { std::slice::from_raw_parts_mut(h.as_mut_ptr() as *mut f64, cols) };
2486
2487        let macd_warmup_abs = macd_warmups[row];
2488        let total_warmup_abs = total_warmups[row];
2489
2490        vwmacd_compute_into(
2491            close,
2492            volume,
2493            f,
2494            sl,
2495            g,
2496            fmt,
2497            smt,
2498            sigt,
2499            first,
2500            macd_warmup_abs,
2501            total_warmup_abs,
2502            simd,
2503            macd_row,
2504            signal_row,
2505            hist_row,
2506        )
2507        .unwrap();
2508    };
2509
2510    if parallel {
2511        #[cfg(not(target_arch = "wasm32"))]
2512        {
2513            macd_mu
2514                .par_chunks_mut(cols)
2515                .zip(signal_mu.par_chunks_mut(cols))
2516                .zip(hist_mu.par_chunks_mut(cols))
2517                .enumerate()
2518                .for_each(|(row, ((m, s), h))| do_row(row, m, s, h));
2519        }
2520        #[cfg(target_arch = "wasm32")]
2521        {
2522            for (row, ((m, s), h)) in macd_mu
2523                .chunks_mut(cols)
2524                .zip(signal_mu.chunks_mut(cols))
2525                .zip(hist_mu.chunks_mut(cols))
2526                .enumerate()
2527            {
2528                do_row(row, m, s, h);
2529            }
2530        }
2531    } else {
2532        for (row, ((m, s), h)) in macd_mu
2533            .chunks_mut(cols)
2534            .zip(signal_mu.chunks_mut(cols))
2535            .zip(hist_mu.chunks_mut(cols))
2536            .enumerate()
2537        {
2538            do_row(row, m, s, h);
2539        }
2540    }
2541
2542    Ok(combos)
2543}
2544
2545#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2546#[wasm_bindgen(js_name = vwmacd_unified)]
2547pub fn vwmacd_unified_js(
2548    close: &[f64],
2549    volume: &[f64],
2550    fast_period: usize,
2551    slow_period: usize,
2552    signal_period: usize,
2553    fast_ma_type: &str,
2554    slow_ma_type: &str,
2555    signal_ma_type: &str,
2556) -> Result<JsValue, JsValue> {
2557    let params = VwmacdParams {
2558        fast_period: Some(fast_period),
2559        slow_period: Some(slow_period),
2560        signal_period: Some(signal_period),
2561        fast_ma_type: Some(fast_ma_type.to_string()),
2562        slow_ma_type: Some(slow_ma_type.to_string()),
2563        signal_ma_type: Some(signal_ma_type.to_string()),
2564    };
2565    let input = VwmacdInput::from_slices(close, volume, params);
2566    let (c, v, f, s, g, fmt, smt, sigt, first, macd_warmup_abs, total_warmup_abs, k) =
2567        vwmacd_prepare(&input, Kernel::Auto).map_err(|e| JsValue::from_str(&e.to_string()))?;
2568
2569    let mut macd = alloc_with_nan_prefix(close.len(), macd_warmup_abs);
2570    let mut signal = alloc_with_nan_prefix(close.len(), total_warmup_abs);
2571    let mut hist = alloc_with_nan_prefix(close.len(), total_warmup_abs);
2572
2573    vwmacd_compute_into(
2574        c,
2575        v,
2576        f,
2577        s,
2578        g,
2579        fmt,
2580        smt,
2581        sigt,
2582        first,
2583        macd_warmup_abs,
2584        total_warmup_abs,
2585        k,
2586        &mut macd,
2587        &mut signal,
2588        &mut hist,
2589    )
2590    .map_err(|e| JsValue::from_str(&e.to_string()))?;
2591
2592    let out = VwmacdJsOutput { macd, signal, hist };
2593    serde_wasm_bindgen::to_value(&out)
2594        .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
2595}
2596
2597#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2598#[wasm_bindgen]
2599pub fn vwmacd_js(
2600    close: &[f64],
2601    volume: &[f64],
2602    fast_period: usize,
2603    slow_period: usize,
2604    signal_period: usize,
2605    fast_ma_type: &str,
2606    slow_ma_type: &str,
2607    signal_ma_type: &str,
2608) -> Result<Vec<f64>, JsValue> {
2609    if close.len() != volume.len() {
2610        return Err(JsValue::from_str(
2611            "Close and volume arrays must have the same length",
2612        ));
2613    }
2614
2615    let params = VwmacdParams {
2616        fast_period: Some(fast_period),
2617        slow_period: Some(slow_period),
2618        signal_period: Some(signal_period),
2619        fast_ma_type: Some(fast_ma_type.to_string()),
2620        slow_ma_type: Some(slow_ma_type.to_string()),
2621        signal_ma_type: Some(signal_ma_type.to_string()),
2622    };
2623    let input = VwmacdInput::from_slices(close, volume, params);
2624
2625    let (
2626        close_data,
2627        volume_data,
2628        fast,
2629        slow,
2630        signal,
2631        fast_ma_type,
2632        slow_ma_type,
2633        signal_ma_type,
2634        first,
2635        macd_warmup,
2636        total_warmup,
2637        kernel_enum,
2638    ) = vwmacd_prepare(&input, Kernel::Auto).map_err(|e| JsValue::from_str(&e.to_string()))?;
2639
2640    let mut macd = alloc_with_nan_prefix(close.len(), macd_warmup);
2641    let mut signal_vec = alloc_with_nan_prefix(close.len(), total_warmup);
2642    let mut hist = alloc_with_nan_prefix(close.len(), total_warmup);
2643
2644    vwmacd_compute_into(
2645        close_data,
2646        volume_data,
2647        fast,
2648        slow,
2649        signal,
2650        fast_ma_type,
2651        slow_ma_type,
2652        signal_ma_type,
2653        first,
2654        macd_warmup,
2655        total_warmup,
2656        kernel_enum,
2657        &mut macd,
2658        &mut signal_vec,
2659        &mut hist,
2660    )
2661    .map_err(|e| JsValue::from_str(&e.to_string()))?;
2662
2663    let mut result = Vec::with_capacity(close.len() * 3);
2664    result.extend_from_slice(&macd);
2665    result.extend_from_slice(&signal_vec);
2666    result.extend_from_slice(&hist);
2667
2668    Ok(result)
2669}
2670
2671#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2672#[wasm_bindgen]
2673pub fn vwmacd_alloc(len: usize) -> *mut f64 {
2674    let mut vec = Vec::<f64>::with_capacity(len);
2675    let ptr = vec.as_mut_ptr();
2676    std::mem::forget(vec);
2677    ptr
2678}
2679
2680#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2681#[wasm_bindgen]
2682pub fn vwmacd_free(ptr: *mut f64, len: usize) {
2683    if !ptr.is_null() {
2684        unsafe {
2685            let _ = Vec::from_raw_parts(ptr, len, len);
2686        }
2687    }
2688}
2689
2690#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2691#[wasm_bindgen]
2692pub fn vwmacd_into(
2693    close_ptr: *const f64,
2694    volume_ptr: *const f64,
2695    macd_ptr: *mut f64,
2696    signal_ptr: *mut f64,
2697    hist_ptr: *mut f64,
2698    len: usize,
2699    fast_period: usize,
2700    slow_period: usize,
2701    signal_period: usize,
2702    fast_ma_type: &str,
2703    slow_ma_type: &str,
2704    signal_ma_type: &str,
2705) -> Result<(), JsValue> {
2706    if close_ptr.is_null()
2707        || volume_ptr.is_null()
2708        || macd_ptr.is_null()
2709        || signal_ptr.is_null()
2710        || hist_ptr.is_null()
2711    {
2712        return Err(JsValue::from_str("Null pointer provided"));
2713    }
2714
2715    unsafe {
2716        let close = std::slice::from_raw_parts(close_ptr, len);
2717        let volume = std::slice::from_raw_parts(volume_ptr, len);
2718        let macd = std::slice::from_raw_parts_mut(macd_ptr, len);
2719        let signal = std::slice::from_raw_parts_mut(signal_ptr, len);
2720        let hist = std::slice::from_raw_parts_mut(hist_ptr, len);
2721
2722        let params = VwmacdParams {
2723            fast_period: Some(fast_period),
2724            slow_period: Some(slow_period),
2725            signal_period: Some(signal_period),
2726            fast_ma_type: Some(fast_ma_type.to_string()),
2727            slow_ma_type: Some(slow_ma_type.to_string()),
2728            signal_ma_type: Some(signal_ma_type.to_string()),
2729        };
2730        let input = VwmacdInput::from_slices(close, volume, params);
2731
2732        let (c, v, f, s, g, fmt, smt, sigt, first, macd_warmup_abs, total_warmup_abs, k) =
2733            vwmacd_prepare(&input, Kernel::Auto).map_err(|e| JsValue::from_str(&e.to_string()))?;
2734
2735        vwmacd_compute_into(
2736            c,
2737            v,
2738            f,
2739            s,
2740            g,
2741            fmt,
2742            smt,
2743            sigt,
2744            first,
2745            macd_warmup_abs,
2746            total_warmup_abs,
2747            k,
2748            macd,
2749            signal,
2750            hist,
2751        )
2752        .map_err(|e| JsValue::from_str(&e.to_string()))
2753    }
2754}
2755
2756#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2757#[wasm_bindgen(js_name = vwmacd_batch)]
2758pub fn vwmacd_batch_unified_js(
2759    close: &[f64],
2760    volume: &[f64],
2761    config: JsValue,
2762) -> Result<JsValue, JsValue> {
2763    let cfg: VwmacdBatchConfig = serde_wasm_bindgen::from_value(config)
2764        .map_err(|e| JsValue::from_str(&format!("Invalid config: {}", e)))?;
2765
2766    let sweep = VwmacdBatchRange {
2767        fast: cfg.fast_range,
2768        slow: cfg.slow_range,
2769        signal: cfg.signal_range,
2770        fast_ma_type: cfg.fast_ma_type.unwrap_or_else(|| "sma".into()),
2771        slow_ma_type: cfg.slow_ma_type.unwrap_or_else(|| "sma".into()),
2772        signal_ma_type: cfg.signal_ma_type.unwrap_or_else(|| "ema".into()),
2773    };
2774
2775    let out = vwmacd_batch_inner(close, volume, &sweep, detect_best_kernel(), false)
2776        .map_err(|e| JsValue::from_str(&e.to_string()))?;
2777
2778    let mut values = Vec::with_capacity(out.macd.len() + out.signal.len() + out.hist.len());
2779    values.extend_from_slice(&out.macd);
2780    values.extend_from_slice(&out.signal);
2781    values.extend_from_slice(&out.hist);
2782
2783    let js = VwmacdBatchJsOutput {
2784        values,
2785        combos: out.params,
2786        rows: out.rows,
2787        cols: out.cols,
2788    };
2789    serde_wasm_bindgen::to_value(&js)
2790        .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
2791}
2792
2793#[cfg(feature = "python")]
2794#[pyfunction(name = "vwmacd")]
2795#[pyo3(signature=(close, volume, fast, slow, signal, fast_ma_type="sma", slow_ma_type="sma", signal_ma_type="ema", kernel=None))]
2796pub fn vwmacd_py<'py>(
2797    py: Python<'py>,
2798    close: PyReadonlyArray1<'py, f64>,
2799    volume: PyReadonlyArray1<'py, f64>,
2800    fast: usize,
2801    slow: usize,
2802    signal: usize,
2803    fast_ma_type: &str,
2804    slow_ma_type: &str,
2805    signal_ma_type: &str,
2806    kernel: Option<&str>,
2807) -> PyResult<(
2808    Bound<'py, PyArray1<f64>>,
2809    Bound<'py, PyArray1<f64>>,
2810    Bound<'py, PyArray1<f64>>,
2811)> {
2812    let close = close.as_slice()?;
2813    let volume = volume.as_slice()?;
2814    let params = VwmacdParams {
2815        fast_period: Some(fast),
2816        slow_period: Some(slow),
2817        signal_period: Some(signal),
2818        fast_ma_type: Some(fast_ma_type.to_string()),
2819        slow_ma_type: Some(slow_ma_type.to_string()),
2820        signal_ma_type: Some(signal_ma_type.to_string()),
2821    };
2822    let input = VwmacdInput::from_slices(close, volume, params);
2823    let kern = validate_kernel(kernel, false)?;
2824
2825    let macd_arr = unsafe { PyArray1::<f64>::new(py, [close.len()], false) };
2826    let signal_arr = unsafe { PyArray1::<f64>::new(py, [close.len()], false) };
2827    let hist_arr = unsafe { PyArray1::<f64>::new(py, [close.len()], false) };
2828
2829    let macd_slice = unsafe { macd_arr.as_slice_mut()? };
2830    let signal_slice = unsafe { signal_arr.as_slice_mut()? };
2831    let hist_slice = unsafe { hist_arr.as_slice_mut()? };
2832
2833    py.allow_threads(|| vwmacd_into_slice(macd_slice, signal_slice, hist_slice, &input, kern))
2834        .map_err(|e| PyValueError::new_err(e.to_string()))?;
2835
2836    Ok((macd_arr, signal_arr, hist_arr))
2837}
2838
2839#[cfg(feature = "python")]
2840#[pyclass(name = "VwmacdStream")]
2841pub struct VwmacdStreamPy {
2842    stream: VwmacdStream,
2843}
2844
2845#[cfg(feature = "python")]
2846#[pymethods]
2847impl VwmacdStreamPy {
2848    #[new]
2849    #[pyo3(signature = (fast_period=None, slow_period=None, signal_period=None, fast_ma_type=None, slow_ma_type=None, signal_ma_type=None))]
2850    fn new(
2851        fast_period: Option<usize>,
2852        slow_period: Option<usize>,
2853        signal_period: Option<usize>,
2854        fast_ma_type: Option<&str>,
2855        slow_ma_type: Option<&str>,
2856        signal_ma_type: Option<&str>,
2857    ) -> PyResult<Self> {
2858        let params = VwmacdParams {
2859            fast_period,
2860            slow_period,
2861            signal_period,
2862            fast_ma_type: fast_ma_type.map(|s| s.to_string()),
2863            slow_ma_type: slow_ma_type.map(|s| s.to_string()),
2864            signal_ma_type: signal_ma_type.map(|s| s.to_string()),
2865        };
2866
2867        let stream =
2868            VwmacdStream::try_new(params).map_err(|e| PyValueError::new_err(e.to_string()))?;
2869
2870        Ok(VwmacdStreamPy { stream })
2871    }
2872
2873    fn update(&mut self, close: f64, volume: f64) -> (Option<f64>, Option<f64>, Option<f64>) {
2874        match self.stream.update(close, volume) {
2875            Some((macd, signal, hist)) => (Some(macd), Some(signal), Some(hist)),
2876            None => (None, None, None),
2877        }
2878    }
2879}
2880
2881#[cfg(feature = "python")]
2882#[pyfunction(name = "vwmacd_batch")]
2883#[pyo3(signature=(close, volume, fast_range, slow_range, signal_range, fast_ma_type="sma", slow_ma_type="sma", signal_ma_type="ema", kernel=None))]
2884pub fn vwmacd_batch_py<'py>(
2885    py: Python<'py>,
2886    close: PyReadonlyArray1<'py, f64>,
2887    volume: PyReadonlyArray1<'py, f64>,
2888    fast_range: (usize, usize, usize),
2889    slow_range: (usize, usize, usize),
2890    signal_range: (usize, usize, usize),
2891    fast_ma_type: &str,
2892    slow_ma_type: &str,
2893    signal_ma_type: &str,
2894    kernel: Option<&str>,
2895) -> PyResult<Bound<'py, PyDict>> {
2896    let close = close.as_slice()?;
2897    let volume = volume.as_slice()?;
2898
2899    let sweep = VwmacdBatchRange {
2900        fast: fast_range,
2901        slow: slow_range,
2902        signal: signal_range,
2903        fast_ma_type: fast_ma_type.to_string(),
2904        slow_ma_type: slow_ma_type.to_string(),
2905        signal_ma_type: signal_ma_type.to_string(),
2906    };
2907    let combos = expand_grid(&sweep).map_err(|e| PyValueError::new_err(e.to_string()))?;
2908    let rows = combos.len();
2909    let cols = close.len();
2910    let total = rows
2911        .checked_mul(cols)
2912        .ok_or_else(|| PyValueError::new_err("vwmacd_batch: rows*cols overflow".to_string()))?;
2913
2914    let macd_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
2915    let signal_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
2916    let hist_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
2917
2918    let macd_slice = unsafe { macd_arr.as_slice_mut()? };
2919    let signal_slice = unsafe { signal_arr.as_slice_mut()? };
2920    let hist_slice = unsafe { hist_arr.as_slice_mut()? };
2921
2922    let kern = validate_kernel(kernel, true)?;
2923    py.allow_threads(|| {
2924        let simd = match kern {
2925            Kernel::Auto => detect_best_batch_kernel(),
2926            k => k,
2927        };
2928        vwmacd_batch_inner_into(
2929            close,
2930            volume,
2931            &sweep,
2932            simd,
2933            true,
2934            macd_slice,
2935            signal_slice,
2936            hist_slice,
2937        )
2938    })
2939    .map_err(|e| PyValueError::new_err(e.to_string()))?;
2940
2941    let d = PyDict::new(py);
2942    d.set_item("macd", macd_arr.reshape((rows, cols))?)?;
2943    d.set_item("signal", signal_arr.reshape((rows, cols))?)?;
2944    d.set_item("hist", hist_arr.reshape((rows, cols))?)?;
2945    d.set_item(
2946        "fast_periods",
2947        combos
2948            .iter()
2949            .map(|p| p.fast_period.unwrap() as u64)
2950            .collect::<Vec<_>>()
2951            .into_pyarray(py),
2952    )?;
2953    d.set_item(
2954        "slow_periods",
2955        combos
2956            .iter()
2957            .map(|p| p.slow_period.unwrap() as u64)
2958            .collect::<Vec<_>>()
2959            .into_pyarray(py),
2960    )?;
2961    d.set_item(
2962        "signal_periods",
2963        combos
2964            .iter()
2965            .map(|p| p.signal_period.unwrap() as u64)
2966            .collect::<Vec<_>>()
2967            .into_pyarray(py),
2968    )?;
2969    d.set_item(
2970        "fast_ma_types",
2971        combos
2972            .iter()
2973            .map(|p| p.fast_ma_type.as_deref().unwrap_or("sma"))
2974            .collect::<Vec<_>>(),
2975    )?;
2976    d.set_item(
2977        "slow_ma_types",
2978        combos
2979            .iter()
2980            .map(|p| p.slow_ma_type.as_deref().unwrap_or("sma"))
2981            .collect::<Vec<_>>(),
2982    )?;
2983    d.set_item(
2984        "signal_ma_types",
2985        combos
2986            .iter()
2987            .map(|p| p.signal_ma_type.as_deref().unwrap_or("ema"))
2988            .collect::<Vec<_>>(),
2989    )?;
2990    Ok(d)
2991}
2992
2993#[cfg(all(feature = "python", feature = "cuda"))]
2994use crate::cuda::{cuda_available, CudaVwmacd};
2995#[cfg(all(feature = "python", feature = "cuda"))]
2996use crate::utilities::dlpack_cuda::make_device_array_py;
2997
2998#[cfg(all(feature = "python", feature = "cuda"))]
2999#[pyfunction(name = "vwmacd_cuda_batch_dev")]
3000#[pyo3(signature = (close_f32, volume_f32, fast_range, slow_range, signal_range, device_id=0))]
3001pub fn vwmacd_cuda_batch_dev_py<'py>(
3002    py: Python<'py>,
3003    close_f32: numpy::PyReadonlyArray1<'py, f32>,
3004    volume_f32: numpy::PyReadonlyArray1<'py, f32>,
3005    fast_range: (usize, usize, usize),
3006    slow_range: (usize, usize, usize),
3007    signal_range: (usize, usize, usize),
3008    device_id: usize,
3009) -> PyResult<Bound<'py, pyo3::types::PyDict>> {
3010    use numpy::IntoPyArray;
3011    if !cuda_available() {
3012        return Err(PyValueError::new_err("CUDA not available"));
3013    }
3014    let prices = close_f32.as_slice()?;
3015    let volumes = volume_f32.as_slice()?;
3016    let sweep = VwmacdBatchRange {
3017        fast: fast_range,
3018        slow: slow_range,
3019        signal: signal_range,
3020        fast_ma_type: "sma".to_string(),
3021        slow_ma_type: "sma".to_string(),
3022        signal_ma_type: "ema".to_string(),
3023    };
3024
3025    let ((macd_buf, signal_buf, hist_buf), combos) = py.allow_threads(|| {
3026        let cuda = CudaVwmacd::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
3027        cuda.vwmacd_batch_dev(prices, volumes, &sweep)
3028            .map(|(triplet, combos)| ((triplet.macd, triplet.signal, triplet.hist), combos))
3029            .map_err(|e| PyValueError::new_err(e.to_string()))
3030    })?;
3031
3032    let dict = pyo3::types::PyDict::new(py);
3033    let macd_dev = make_device_array_py(device_id, macd_buf)?;
3034    dict.set_item("macd", Py::new(py, macd_dev)?)?;
3035    let signal_dev = make_device_array_py(device_id, signal_buf)?;
3036    dict.set_item("signal", Py::new(py, signal_dev)?)?;
3037    let hist_dev = make_device_array_py(device_id, hist_buf)?;
3038    dict.set_item("hist", Py::new(py, hist_dev)?)?;
3039    dict.set_item("rows", combos.len())?;
3040    dict.set_item("cols", prices.len())?;
3041    dict.set_item(
3042        "fasts",
3043        combos
3044            .iter()
3045            .map(|c| c.fast_period.unwrap())
3046            .collect::<Vec<_>>()
3047            .into_pyarray(py),
3048    )?;
3049    dict.set_item(
3050        "slows",
3051        combos
3052            .iter()
3053            .map(|c| c.slow_period.unwrap())
3054            .collect::<Vec<_>>()
3055            .into_pyarray(py),
3056    )?;
3057    dict.set_item(
3058        "signals",
3059        combos
3060            .iter()
3061            .map(|c| c.signal_period.unwrap())
3062            .collect::<Vec<_>>()
3063            .into_pyarray(py),
3064    )?;
3065    Ok(dict)
3066}
3067
3068#[cfg(all(feature = "python", feature = "cuda"))]
3069#[pyfunction(name = "vwmacd_cuda_many_series_one_param_dev")]
3070#[pyo3(signature = (prices_tm_f32, volumes_tm_f32, fast, slow, signal, device_id=0))]
3071pub fn vwmacd_cuda_many_series_one_param_dev_py<'py>(
3072    py: Python<'py>,
3073    prices_tm_f32: numpy::PyReadonlyArray2<'py, f32>,
3074    volumes_tm_f32: numpy::PyReadonlyArray2<'py, f32>,
3075    fast: usize,
3076    slow: usize,
3077    signal: usize,
3078    device_id: usize,
3079) -> PyResult<Bound<'py, pyo3::types::PyDict>> {
3080    use numpy::PyUntypedArrayMethods;
3081    if !cuda_available() {
3082        return Err(PyValueError::new_err("CUDA not available"));
3083    }
3084    let ps = prices_tm_f32.shape();
3085    let vs = volumes_tm_f32.shape();
3086    if ps.len() != 2 || vs.len() != 2 || ps != vs {
3087        return Err(PyValueError::new_err(
3088            "expected two 2D arrays with same shape",
3089        ));
3090    }
3091    let rows = ps[0];
3092    let cols = ps[1];
3093    let p = prices_tm_f32.as_slice()?;
3094    let v = volumes_tm_f32.as_slice()?;
3095    let params = VwmacdParams {
3096        fast_period: Some(fast),
3097        slow_period: Some(slow),
3098        signal_period: Some(signal),
3099        fast_ma_type: Some("sma".into()),
3100        slow_ma_type: Some("sma".into()),
3101        signal_ma_type: Some("ema".into()),
3102    };
3103
3104    let (macd_buf, signal_buf, hist_buf) = py.allow_threads(|| {
3105        let cuda = CudaVwmacd::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
3106        cuda.vwmacd_many_series_one_param_time_major_dev(p, v, cols, rows, &params)
3107            .map(|triplet| (triplet.macd, triplet.signal, triplet.hist))
3108            .map_err(|e| PyValueError::new_err(e.to_string()))
3109    })?;
3110
3111    let dict = pyo3::types::PyDict::new(py);
3112    let macd_dev = make_device_array_py(device_id, macd_buf)?;
3113    dict.set_item("macd", Py::new(py, macd_dev)?)?;
3114    let signal_dev = make_device_array_py(device_id, signal_buf)?;
3115    dict.set_item("signal", Py::new(py, signal_dev)?)?;
3116    let hist_dev = make_device_array_py(device_id, hist_buf)?;
3117    dict.set_item("hist", Py::new(py, hist_dev)?)?;
3118    dict.set_item("rows", rows)?;
3119    dict.set_item("cols", cols)?;
3120    dict.set_item("fast", fast)?;
3121    dict.set_item("slow", slow)?;
3122    dict.set_item("signal_len", signal)?;
3123    Ok(dict)
3124}
3125
3126#[cfg(test)]
3127mod tests {
3128    use super::*;
3129    use crate::skip_if_unsupported;
3130    use crate::utilities::data_loader::read_candles_from_csv;
3131
3132    fn check_vwmacd_partial_params(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
3133        skip_if_unsupported!(kernel, test_name);
3134        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
3135        let candles = read_candles_from_csv(file_path)?;
3136        let default_params = VwmacdParams {
3137            fast_period: None,
3138            slow_period: None,
3139            signal_period: None,
3140            fast_ma_type: None,
3141            slow_ma_type: None,
3142            signal_ma_type: None,
3143        };
3144        let input = VwmacdInput::from_candles(&candles, "close", "volume", default_params);
3145        let output = vwmacd_with_kernel(&input, kernel)?;
3146        assert_eq!(output.macd.len(), candles.close.len());
3147        Ok(())
3148    }
3149
3150    fn check_vwmacd_accuracy(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
3151        skip_if_unsupported!(kernel, test_name);
3152        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
3153        let candles = read_candles_from_csv(file_path)?;
3154        let input = VwmacdInput::with_default_candles(&candles);
3155        let result = vwmacd_with_kernel(&input, kernel)?;
3156
3157        let expected_macd = [
3158            -394.95161155,
3159            -508.29106210,
3160            -490.70190723,
3161            -388.94996199,
3162            -341.13720646,
3163        ];
3164
3165        let expected_signal = [
3166            -539.48861567,
3167            -533.24910496,
3168            -524.73966541,
3169            -497.58172247,
3170            -466.29282108,
3171        ];
3172
3173        let expected_histogram = [
3174            144.53700412,
3175            24.95804286,
3176            34.03775818,
3177            108.63176274,
3178            125.15561462,
3179        ];
3180
3181        let last_five_macd = &result.macd[result.macd.len().saturating_sub(5)..];
3182        for (i, &val) in last_five_macd.iter().enumerate() {
3183            assert!(
3184                (val - expected_macd[i]).abs() < 2e-4,
3185                "[{}] MACD mismatch at idx {}: got {}, expected {}",
3186                test_name,
3187                i,
3188                val,
3189                expected_macd[i]
3190            );
3191        }
3192
3193        let last_five_signal = &result.signal[result.signal.len().saturating_sub(5)..];
3194        for (i, &val) in last_five_signal.iter().enumerate() {
3195            assert!(
3196                (val - expected_signal[i]).abs() < 2e-4,
3197                "[{}] Signal mismatch at idx {}: got {}, expected {}",
3198                test_name,
3199                i,
3200                val,
3201                expected_signal[i]
3202            );
3203        }
3204
3205        let last_five_hist = &result.hist[result.hist.len().saturating_sub(5)..];
3206        for (i, &val) in last_five_hist.iter().enumerate() {
3207            assert!(
3208                (val - expected_histogram[i]).abs() < 2e-4,
3209                "[{}] Histogram mismatch at idx {}: got {}, expected {}",
3210                test_name,
3211                i,
3212                val,
3213                expected_histogram[i]
3214            );
3215        }
3216
3217        Ok(())
3218    }
3219    fn check_vwmacd_with_custom_ma_types(
3220        test_name: &str,
3221        kernel: Kernel,
3222    ) -> Result<(), Box<dyn Error>> {
3223        skip_if_unsupported!(kernel, test_name);
3224        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
3225        let candles = read_candles_from_csv(file_path)?;
3226
3227        let params = VwmacdParams {
3228            fast_period: Some(12),
3229            slow_period: Some(26),
3230            signal_period: Some(9),
3231            fast_ma_type: Some("ema".to_string()),
3232            slow_ma_type: Some("wma".to_string()),
3233            signal_ma_type: Some("sma".to_string()),
3234        };
3235        let input = VwmacdInput::from_candles(&candles, "close", "volume", params);
3236        let output = vwmacd_with_kernel(&input, kernel)?;
3237        assert_eq!(output.macd.len(), candles.close.len());
3238
3239        let default_input = VwmacdInput::with_default_candles(&candles);
3240        let default_output = vwmacd_with_kernel(&default_input, kernel)?;
3241
3242        let different_count = output
3243            .macd
3244            .iter()
3245            .zip(&default_output.macd)
3246            .skip(50)
3247            .filter(|(&a, &b)| !a.is_nan() && !b.is_nan() && (a - b).abs() > 1e-10)
3248            .count();
3249
3250        assert!(
3251            different_count > 0,
3252            "Custom MA types should produce different results"
3253        );
3254        Ok(())
3255    }
3256
3257    fn check_vwmacd_nan_data(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
3258        skip_if_unsupported!(kernel, test_name);
3259        let close = [f64::NAN, f64::NAN];
3260        let volume = [f64::NAN, f64::NAN];
3261        let params = VwmacdParams::default();
3262        let input = VwmacdInput::from_slices(&close, &volume, params);
3263        let result = vwmacd_with_kernel(&input, kernel);
3264        assert!(result.is_err());
3265        Ok(())
3266    }
3267
3268    fn check_vwmacd_zero_period(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
3269        skip_if_unsupported!(kernel, test_name);
3270        let close = [10.0, 20.0, 30.0];
3271        let volume = [1.0, 1.0, 1.0];
3272        let params = VwmacdParams {
3273            fast_period: Some(0),
3274            slow_period: Some(26),
3275            signal_period: Some(9),
3276            fast_ma_type: None,
3277            slow_ma_type: None,
3278            signal_ma_type: None,
3279        };
3280        let input = VwmacdInput::from_slices(&close, &volume, params);
3281        let result = vwmacd_with_kernel(&input, kernel);
3282        assert!(result.is_err());
3283        Ok(())
3284    }
3285
3286    fn check_vwmacd_period_exceeds(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
3287        skip_if_unsupported!(kernel, test_name);
3288        let close = [10.0, 20.0, 30.0];
3289        let volume = [100.0, 200.0, 300.0];
3290        let params = VwmacdParams {
3291            fast_period: Some(12),
3292            slow_period: Some(26),
3293            signal_period: Some(9),
3294            fast_ma_type: None,
3295            slow_ma_type: None,
3296            signal_ma_type: None,
3297        };
3298        let input = VwmacdInput::from_slices(&close, &volume, params);
3299        let result = vwmacd_with_kernel(&input, kernel);
3300        assert!(result.is_err());
3301        Ok(())
3302    }
3303
3304    macro_rules! generate_all_vwmacd_tests {
3305        ($($test_fn:ident),*) => {
3306            paste::paste! {
3307                $(
3308                    #[test]
3309                    fn [<$test_fn _scalar_f64>]() {
3310                        let _ = $test_fn(stringify!([<$test_fn _scalar_f64>]), Kernel::Scalar);
3311                    }
3312                )*
3313                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
3314                $(
3315                    #[test]
3316                    fn [<$test_fn _avx2_f64>]() {
3317                        let _ = $test_fn(stringify!([<$test_fn _avx2_f64>]), Kernel::Avx2);
3318                    }
3319                    #[test]
3320                    fn [<$test_fn _avx512_f64>]() {
3321                        let _ = $test_fn(stringify!([<$test_fn _avx512_f64>]), Kernel::Avx512);
3322                    }
3323                )*
3324                #[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
3325                $(
3326                    #[test]
3327                    fn [<$test_fn _simd128_f64>]() {
3328                        let _ = $test_fn(stringify!([<$test_fn _simd128_f64>]), Kernel::Scalar);
3329                    }
3330                )*
3331            }
3332        }
3333    }
3334    #[cfg(debug_assertions)]
3335    fn check_vwmacd_no_poison(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
3336        skip_if_unsupported!(kernel, test_name);
3337
3338        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
3339        let candles = read_candles_from_csv(file_path)?;
3340
3341        let test_params = vec![
3342            VwmacdParams::default(),
3343            VwmacdParams {
3344                fast_period: Some(2),
3345                slow_period: Some(3),
3346                signal_period: Some(2),
3347                fast_ma_type: Some("sma".to_string()),
3348                slow_ma_type: Some("sma".to_string()),
3349                signal_ma_type: Some("ema".to_string()),
3350            },
3351            VwmacdParams {
3352                fast_period: Some(5),
3353                slow_period: Some(10),
3354                signal_period: Some(3),
3355                fast_ma_type: Some("ema".to_string()),
3356                slow_ma_type: Some("ema".to_string()),
3357                signal_ma_type: Some("sma".to_string()),
3358            },
3359            VwmacdParams {
3360                fast_period: Some(10),
3361                slow_period: Some(20),
3362                signal_period: Some(5),
3363                fast_ma_type: Some("wma".to_string()),
3364                slow_ma_type: Some("sma".to_string()),
3365                signal_ma_type: Some("ema".to_string()),
3366            },
3367            VwmacdParams {
3368                fast_period: Some(12),
3369                slow_period: Some(26),
3370                signal_period: Some(9),
3371                fast_ma_type: Some("sma".to_string()),
3372                slow_ma_type: Some("sma".to_string()),
3373                signal_ma_type: Some("ema".to_string()),
3374            },
3375            VwmacdParams {
3376                fast_period: Some(20),
3377                slow_period: Some(40),
3378                signal_period: Some(10),
3379                fast_ma_type: Some("ema".to_string()),
3380                slow_ma_type: Some("wma".to_string()),
3381                signal_ma_type: Some("sma".to_string()),
3382            },
3383            VwmacdParams {
3384                fast_period: Some(50),
3385                slow_period: Some(100),
3386                signal_period: Some(20),
3387                fast_ma_type: Some("sma".to_string()),
3388                slow_ma_type: Some("ema".to_string()),
3389                signal_ma_type: Some("wma".to_string()),
3390            },
3391            VwmacdParams {
3392                fast_period: Some(25),
3393                slow_period: Some(26),
3394                signal_period: Some(9),
3395                fast_ma_type: Some("ema".to_string()),
3396                slow_ma_type: Some("ema".to_string()),
3397                signal_ma_type: Some("ema".to_string()),
3398            },
3399            VwmacdParams {
3400                fast_period: Some(8),
3401                slow_period: Some(21),
3402                signal_period: Some(5),
3403                fast_ma_type: Some("wma".to_string()),
3404                slow_ma_type: Some("wma".to_string()),
3405                signal_ma_type: Some("wma".to_string()),
3406            },
3407            VwmacdParams {
3408                fast_period: Some(15),
3409                slow_period: Some(30),
3410                signal_period: Some(15),
3411                fast_ma_type: Some("sma".to_string()),
3412                slow_ma_type: Some("wma".to_string()),
3413                signal_ma_type: Some("ema".to_string()),
3414            },
3415        ];
3416
3417        for (param_idx, params) in test_params.iter().enumerate() {
3418            let input = VwmacdInput::from_candles(&candles, "close", "volume", params.clone());
3419            let output = vwmacd_with_kernel(&input, kernel)?;
3420
3421            for (i, &val) in output.macd.iter().enumerate() {
3422                if val.is_nan() {
3423                    continue;
3424                }
3425
3426                let bits = val.to_bits();
3427
3428                if bits == 0x11111111_11111111 {
3429                    panic!(
3430						"[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) in MACD at index {} \
3431						 with params: fast={}, slow={}, signal={}, fast_ma={}, slow_ma={}, signal_ma={} (param set {})",
3432						test_name, val, bits, i,
3433						params.fast_period.unwrap_or(12),
3434						params.slow_period.unwrap_or(26),
3435						params.signal_period.unwrap_or(9),
3436						params.fast_ma_type.as_deref().unwrap_or("sma"),
3437						params.slow_ma_type.as_deref().unwrap_or("sma"),
3438						params.signal_ma_type.as_deref().unwrap_or("ema"),
3439						param_idx
3440					);
3441                }
3442
3443                if bits == 0x22222222_22222222 {
3444                    panic!(
3445						"[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) in MACD at index {} \
3446						 with params: fast={}, slow={}, signal={}, fast_ma={}, slow_ma={}, signal_ma={} (param set {})",
3447						test_name, val, bits, i,
3448						params.fast_period.unwrap_or(12),
3449						params.slow_period.unwrap_or(26),
3450						params.signal_period.unwrap_or(9),
3451						params.fast_ma_type.as_deref().unwrap_or("sma"),
3452						params.slow_ma_type.as_deref().unwrap_or("sma"),
3453						params.signal_ma_type.as_deref().unwrap_or("ema"),
3454						param_idx
3455					);
3456                }
3457
3458                if bits == 0x33333333_33333333 {
3459                    panic!(
3460						"[{}] Found make_uninit_matrix poison value {} (0x{:016X}) in MACD at index {} \
3461						 with params: fast={}, slow={}, signal={}, fast_ma={}, slow_ma={}, signal_ma={} (param set {})",
3462						test_name, val, bits, i,
3463						params.fast_period.unwrap_or(12),
3464						params.slow_period.unwrap_or(26),
3465						params.signal_period.unwrap_or(9),
3466						params.fast_ma_type.as_deref().unwrap_or("sma"),
3467						params.slow_ma_type.as_deref().unwrap_or("sma"),
3468						params.signal_ma_type.as_deref().unwrap_or("ema"),
3469						param_idx
3470					);
3471                }
3472            }
3473
3474            for (i, &val) in output.signal.iter().enumerate() {
3475                if val.is_nan() {
3476                    continue;
3477                }
3478
3479                let bits = val.to_bits();
3480
3481                if bits == 0x11111111_11111111 {
3482                    panic!(
3483						"[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) in Signal at index {} \
3484						 with params: fast={}, slow={}, signal={}, fast_ma={}, slow_ma={}, signal_ma={} (param set {})",
3485						test_name, val, bits, i,
3486						params.fast_period.unwrap_or(12),
3487						params.slow_period.unwrap_or(26),
3488						params.signal_period.unwrap_or(9),
3489						params.fast_ma_type.as_deref().unwrap_or("sma"),
3490						params.slow_ma_type.as_deref().unwrap_or("sma"),
3491						params.signal_ma_type.as_deref().unwrap_or("ema"),
3492						param_idx
3493					);
3494                }
3495
3496                if bits == 0x22222222_22222222 {
3497                    panic!(
3498						"[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) in Signal at index {} \
3499						 with params: fast={}, slow={}, signal={}, fast_ma={}, slow_ma={}, signal_ma={} (param set {})",
3500						test_name, val, bits, i,
3501						params.fast_period.unwrap_or(12),
3502						params.slow_period.unwrap_or(26),
3503						params.signal_period.unwrap_or(9),
3504						params.fast_ma_type.as_deref().unwrap_or("sma"),
3505						params.slow_ma_type.as_deref().unwrap_or("sma"),
3506						params.signal_ma_type.as_deref().unwrap_or("ema"),
3507						param_idx
3508					);
3509                }
3510
3511                if bits == 0x33333333_33333333 {
3512                    panic!(
3513						"[{}] Found make_uninit_matrix poison value {} (0x{:016X}) in Signal at index {} \
3514						 with params: fast={}, slow={}, signal={}, fast_ma={}, slow_ma={}, signal_ma={} (param set {})",
3515						test_name, val, bits, i,
3516						params.fast_period.unwrap_or(12),
3517						params.slow_period.unwrap_or(26),
3518						params.signal_period.unwrap_or(9),
3519						params.fast_ma_type.as_deref().unwrap_or("sma"),
3520						params.slow_ma_type.as_deref().unwrap_or("sma"),
3521						params.signal_ma_type.as_deref().unwrap_or("ema"),
3522						param_idx
3523					);
3524                }
3525            }
3526
3527            for (i, &val) in output.hist.iter().enumerate() {
3528                if val.is_nan() {
3529                    continue;
3530                }
3531
3532                let bits = val.to_bits();
3533
3534                if bits == 0x11111111_11111111 {
3535                    panic!(
3536						"[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) in Histogram at index {} \
3537						 with params: fast={}, slow={}, signal={}, fast_ma={}, slow_ma={}, signal_ma={} (param set {})",
3538						test_name, val, bits, i,
3539						params.fast_period.unwrap_or(12),
3540						params.slow_period.unwrap_or(26),
3541						params.signal_period.unwrap_or(9),
3542						params.fast_ma_type.as_deref().unwrap_or("sma"),
3543						params.slow_ma_type.as_deref().unwrap_or("sma"),
3544						params.signal_ma_type.as_deref().unwrap_or("ema"),
3545						param_idx
3546					);
3547                }
3548
3549                if bits == 0x22222222_22222222 {
3550                    panic!(
3551						"[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) in Histogram at index {} \
3552						 with params: fast={}, slow={}, signal={}, fast_ma={}, slow_ma={}, signal_ma={} (param set {})",
3553						test_name, val, bits, i,
3554						params.fast_period.unwrap_or(12),
3555						params.slow_period.unwrap_or(26),
3556						params.signal_period.unwrap_or(9),
3557						params.fast_ma_type.as_deref().unwrap_or("sma"),
3558						params.slow_ma_type.as_deref().unwrap_or("sma"),
3559						params.signal_ma_type.as_deref().unwrap_or("ema"),
3560						param_idx
3561					);
3562                }
3563
3564                if bits == 0x33333333_33333333 {
3565                    panic!(
3566						"[{}] Found make_uninit_matrix poison value {} (0x{:016X}) in Histogram at index {} \
3567						 with params: fast={}, slow={}, signal={}, fast_ma={}, slow_ma={}, signal_ma={} (param set {})",
3568						test_name, val, bits, i,
3569						params.fast_period.unwrap_or(12),
3570						params.slow_period.unwrap_or(26),
3571						params.signal_period.unwrap_or(9),
3572						params.fast_ma_type.as_deref().unwrap_or("sma"),
3573						params.slow_ma_type.as_deref().unwrap_or("sma"),
3574						params.signal_ma_type.as_deref().unwrap_or("ema"),
3575						param_idx
3576					);
3577                }
3578            }
3579        }
3580
3581        Ok(())
3582    }
3583
3584    #[cfg(not(debug_assertions))]
3585    fn check_vwmacd_no_poison(_test_name: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
3586        Ok(())
3587    }
3588
3589    #[cfg(feature = "proptest")]
3590    #[allow(clippy::float_cmp)]
3591    fn check_vwmacd_property(
3592        test_name: &str,
3593        kernel: Kernel,
3594    ) -> Result<(), Box<dyn std::error::Error>> {
3595        use proptest::prelude::*;
3596        skip_if_unsupported!(kernel, test_name);
3597
3598        let strat = (2usize..=20, 5usize..=50, 2usize..=20, 0..3usize).prop_flat_map(
3599            |(fast, slow, signal, ma_variant)| {
3600                let slow = slow.max(fast + 1);
3601                let data_len = slow * 2 + signal;
3602                (
3603                    prop::collection::vec(
3604                        (100.0f64..10000.0f64).prop_filter("finite", |x| x.is_finite()),
3605                        data_len..400,
3606                    ),
3607                    prop::collection::vec(
3608                        (0.001f64..1000000.0f64)
3609                            .prop_filter("finite positive", |x| x.is_finite() && *x > 0.0),
3610                        data_len..400,
3611                    ),
3612                    Just(fast),
3613                    Just(slow),
3614                    Just(signal),
3615                    Just(ma_variant),
3616                )
3617            },
3618        );
3619
3620        proptest::test_runner::TestRunner::default()
3621            .run(&strat, |(close, volume, fast, slow, signal, ma_variant)| {
3622                let len = close.len().min(volume.len());
3623                let close = &close[..len];
3624                let volume = &volume[..len];
3625
3626                let (fast_ma, slow_ma, signal_ma) = match ma_variant {
3627                    0 => ("sma", "sma", "ema"),
3628                    1 => ("ema", "ema", "sma"),
3629                    _ => ("wma", "sma", "ema"),
3630                };
3631
3632                let params = VwmacdParams {
3633                    fast_period: Some(fast),
3634                    slow_period: Some(slow),
3635                    signal_period: Some(signal),
3636                    fast_ma_type: Some(fast_ma.to_string()),
3637                    slow_ma_type: Some(slow_ma.to_string()),
3638                    signal_ma_type: Some(signal_ma.to_string()),
3639                };
3640                let input = VwmacdInput::from_slices(close, volume, params);
3641
3642                let VwmacdOutput {
3643                    macd,
3644                    signal: sig,
3645                    hist,
3646                } = vwmacd_with_kernel(&input, kernel).unwrap();
3647                let VwmacdOutput {
3648                    macd: ref_macd,
3649                    signal: ref_sig,
3650                    hist: ref_hist,
3651                } = vwmacd_with_kernel(&input, Kernel::Scalar).unwrap();
3652
3653                let params_fast = VwmacdParams {
3654                    fast_period: Some(fast),
3655                    slow_period: Some(fast),
3656                    signal_period: Some(2),
3657                    fast_ma_type: Some(fast_ma.to_string()),
3658                    slow_ma_type: Some(fast_ma.to_string()),
3659                    signal_ma_type: Some("sma".to_string()),
3660                };
3661                let input_fast = VwmacdInput::from_slices(close, volume, params_fast);
3662                let fast_vwma_result = vwmacd_with_kernel(&input_fast, Kernel::Scalar).unwrap();
3663
3664                let macd_warmup = slow - 1;
3665                let signal_warmup = macd_warmup + signal - 1;
3666                let hist_warmup = signal_warmup;
3667
3668                for i in 0..len {
3669                    let y_macd = macd[i];
3670                    let y_sig = sig[i];
3671                    let y_hist = hist[i];
3672                    let r_macd = ref_macd[i];
3673                    let r_sig = ref_sig[i];
3674                    let r_hist = ref_hist[i];
3675
3676                    if y_macd.is_nan() != r_macd.is_nan() {
3677                        prop_assert!(
3678                            false,
3679                            "MACD NaN mismatch at index {}: test={} ref={}",
3680                            i,
3681                            y_macd.is_nan(),
3682                            r_macd.is_nan()
3683                        );
3684                    }
3685                    if y_sig.is_nan() != r_sig.is_nan() {
3686                        prop_assert!(
3687                            false,
3688                            "Signal NaN mismatch at index {}: test={} ref={}",
3689                            i,
3690                            y_sig.is_nan(),
3691                            r_sig.is_nan()
3692                        );
3693                    }
3694                    if y_hist.is_nan() != r_hist.is_nan() {
3695                        prop_assert!(
3696                            false,
3697                            "Histogram NaN mismatch at index {}: test={} ref={}",
3698                            i,
3699                            y_hist.is_nan(),
3700                            r_hist.is_nan()
3701                        );
3702                    }
3703
3704                    if i >= hist_warmup {
3705                        prop_assert!(
3706                            y_macd.is_finite(),
3707                            "MACD not finite at index {}: {}",
3708                            i,
3709                            y_macd
3710                        );
3711                        prop_assert!(
3712                            y_sig.is_finite(),
3713                            "Signal not finite at index {}: {}",
3714                            i,
3715                            y_sig
3716                        );
3717                        prop_assert!(
3718                            y_hist.is_finite(),
3719                            "Histogram not finite at index {}: {}",
3720                            i,
3721                            y_hist
3722                        );
3723                    }
3724
3725                    if y_macd.is_finite() && y_sig.is_finite() {
3726                        let expected_hist = y_macd - y_sig;
3727                        prop_assert!(
3728                            (y_hist - expected_hist).abs() <= 1e-9,
3729                            "Histogram mismatch at {}: {} vs {} (macd={}, signal={})",
3730                            i,
3731                            y_hist,
3732                            expected_hist,
3733                            y_macd,
3734                            y_sig
3735                        );
3736                    }
3737
3738                    if !y_macd.is_finite() || !r_macd.is_finite() {
3739                        prop_assert!(
3740                            y_macd.to_bits() == r_macd.to_bits(),
3741                            "MACD finite/NaN mismatch at {}: {} vs {}",
3742                            i,
3743                            y_macd,
3744                            r_macd
3745                        );
3746                    } else {
3747                        let ulp_diff = y_macd.to_bits().abs_diff(r_macd.to_bits());
3748                        prop_assert!(
3749                            (y_macd - r_macd).abs() <= 1e-9 || ulp_diff <= 4,
3750                            "MACD mismatch at {}: {} vs {} (ULP={})",
3751                            i,
3752                            y_macd,
3753                            r_macd,
3754                            ulp_diff
3755                        );
3756                    }
3757
3758                    if !y_sig.is_finite() || !r_sig.is_finite() {
3759                        prop_assert!(
3760                            y_sig.to_bits() == r_sig.to_bits(),
3761                            "Signal finite/NaN mismatch at {}: {} vs {}",
3762                            i,
3763                            y_sig,
3764                            r_sig
3765                        );
3766                    } else {
3767                        let ulp_diff = y_sig.to_bits().abs_diff(r_sig.to_bits());
3768                        prop_assert!(
3769                            (y_sig - r_sig).abs() <= 1e-9 || ulp_diff <= 4,
3770                            "Signal mismatch at {}: {} vs {} (ULP={})",
3771                            i,
3772                            y_sig,
3773                            r_sig,
3774                            ulp_diff
3775                        );
3776                    }
3777
3778                    if !y_hist.is_finite() || !r_hist.is_finite() {
3779                        prop_assert!(
3780                            y_hist.to_bits() == r_hist.to_bits(),
3781                            "Histogram finite/NaN mismatch at {}: {} vs {}",
3782                            i,
3783                            y_hist,
3784                            r_hist
3785                        );
3786                    } else {
3787                        let ulp_diff = y_hist.to_bits().abs_diff(r_hist.to_bits());
3788                        prop_assert!(
3789                            (y_hist - r_hist).abs() <= 1e-9 || ulp_diff <= 4,
3790                            "Histogram mismatch at {}: {} vs {} (ULP={})",
3791                            i,
3792                            y_hist,
3793                            r_hist,
3794                            ulp_diff
3795                        );
3796                    }
3797
3798                    if close.windows(2).all(|w| (w[0] - w[1]).abs() < f64::EPSILON)
3799                        && volume
3800                            .windows(2)
3801                            .all(|w| (w[0] - w[1]).abs() < f64::EPSILON)
3802                        && y_macd.is_finite()
3803                    {
3804                        prop_assert!(
3805							y_macd.abs() <= 1e-9,
3806							"MACD should be ~0 with constant prices and volumes, got {} at index {}", y_macd, i
3807						);
3808                    }
3809
3810                    if volume[i] < 1.0 && y_macd.is_finite() {
3811                        prop_assert!(
3812                            y_macd.is_finite(),
3813                            "MACD should be finite even with small volume {} at index {}",
3814                            volume[i],
3815                            i
3816                        );
3817                    }
3818
3819                    if y_macd.is_finite() && i >= slow - 1 {
3820                        let all_prices_min = close.iter().cloned().fold(f64::INFINITY, f64::min);
3821                        let all_prices_max =
3822                            close.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
3823                        let total_range = all_prices_max - all_prices_min;
3824
3825                        prop_assert!(
3826                            y_macd.abs() <= total_range + 1e-6,
3827                            "MACD {} exceeds total price range {} at index {}",
3828                            y_macd.abs(),
3829                            total_range,
3830                            i
3831                        );
3832                    }
3833                }
3834
3835                if len > slow * 2 {
3836                    let mut extreme_volume = volume.to_vec();
3837
3838                    for i in (0..len).step_by(5) {
3839                        extreme_volume[i] *= 1000.0;
3840                    }
3841
3842                    let params_extreme = VwmacdParams {
3843                        fast_period: Some(fast),
3844                        slow_period: Some(slow),
3845                        signal_period: Some(signal),
3846                        fast_ma_type: Some(fast_ma.to_string()),
3847                        slow_ma_type: Some(slow_ma.to_string()),
3848                        signal_ma_type: Some(signal_ma.to_string()),
3849                    };
3850                    let input_extreme =
3851                        VwmacdInput::from_slices(close, &extreme_volume, params_extreme);
3852
3853                    let result = vwmacd_with_kernel(&input_extreme, kernel);
3854                    prop_assert!(result.is_ok(), "Should handle extreme volume ratios");
3855
3856                    if let Ok(extreme_output) = result {
3857                        for i in hist_warmup..len {
3858                            if extreme_output.macd[i].is_finite() {
3859                                prop_assert!(
3860                                    extreme_output.macd[i].is_finite(),
3861                                    "MACD should be finite with extreme volumes at index {}",
3862                                    i
3863                                );
3864                            }
3865                        }
3866                    }
3867                }
3868
3869                Ok(())
3870            })
3871            .unwrap();
3872
3873        Ok(())
3874    }
3875
3876    generate_all_vwmacd_tests!(
3877        check_vwmacd_partial_params,
3878        check_vwmacd_accuracy,
3879        check_vwmacd_with_custom_ma_types,
3880        check_vwmacd_nan_data,
3881        check_vwmacd_zero_period,
3882        check_vwmacd_period_exceeds,
3883        check_vwmacd_streaming,
3884        check_vwmacd_no_poison
3885    );
3886
3887    #[cfg(feature = "proptest")]
3888    generate_all_vwmacd_tests!(check_vwmacd_property);
3889
3890    fn check_vwmacd_streaming(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
3891        skip_if_unsupported!(kernel, test_name);
3892
3893        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
3894        let candles = read_candles_from_csv(file_path)?;
3895
3896        let fast_period = 12;
3897        let slow_period = 26;
3898        let signal_period = 9;
3899        let fast_ma_type = "sma";
3900        let slow_ma_type = "sma";
3901        let signal_ma_type = "ema";
3902
3903        let params = VwmacdParams {
3904            fast_period: Some(fast_period),
3905            slow_period: Some(slow_period),
3906            signal_period: Some(signal_period),
3907            fast_ma_type: Some(fast_ma_type.to_string()),
3908            slow_ma_type: Some(slow_ma_type.to_string()),
3909            signal_ma_type: Some(signal_ma_type.to_string()),
3910        };
3911        let input = VwmacdInput::from_slices(&candles.close, &candles.volume, params.clone());
3912        let batch_output = vwmacd_with_kernel(&input, kernel)?;
3913
3914        let mut stream = VwmacdStream::try_new(params)?;
3915
3916        let mut stream_macd = Vec::with_capacity(candles.close.len());
3917        let mut stream_signal = Vec::with_capacity(candles.close.len());
3918        let mut stream_hist = Vec::with_capacity(candles.close.len());
3919
3920        for i in 0..candles.close.len() {
3921            match stream.update(candles.close[i], candles.volume[i]) {
3922                Some((m, s, h)) => {
3923                    stream_macd.push(m);
3924                    stream_signal.push(s);
3925                    stream_hist.push(h);
3926                }
3927                None => {
3928                    stream_macd.push(f64::NAN);
3929                    stream_signal.push(f64::NAN);
3930                    stream_hist.push(f64::NAN);
3931                }
3932            }
3933        }
3934
3935        assert_eq!(batch_output.macd.len(), stream_macd.len());
3936        assert_eq!(batch_output.signal.len(), stream_signal.len());
3937        assert_eq!(batch_output.hist.len(), stream_hist.len());
3938
3939        let warmup = slow_period + 10;
3940        for i in warmup..stream_macd.len().min(warmup + 50) {
3941            let b = batch_output.macd[i];
3942            let s = stream_macd[i];
3943
3944            if !b.is_nan() && !s.is_nan() {
3945                let diff = (b - s).abs();
3946                let avg = (b.abs() + s.abs()) / 2.0;
3947                let relative_diff = if avg > 1e-10 { diff / avg } else { diff };
3948
3949                if relative_diff > 0.5 && diff > 10.0 {
3950                    eprintln!(
3951						"[{}] Warning: Large VWMACD streaming difference at idx {}: batch={}, stream={}, diff={}",
3952						test_name, i, b, s, diff
3953					);
3954                }
3955            }
3956        }
3957
3958        for i in warmup..stream_signal.len().min(warmup + 50) {
3959            let b = batch_output.signal[i];
3960            let s = stream_signal[i];
3961
3962            if !b.is_nan() && !s.is_nan() {
3963                let diff = (b - s).abs();
3964                let avg = (b.abs() + s.abs()) / 2.0;
3965                let relative_diff = if avg > 1e-10 { diff / avg } else { diff };
3966
3967                if relative_diff > 0.5 && diff > 10.0 {
3968                    eprintln!(
3969						"[{}] Warning: Large signal streaming difference at idx {}: batch={}, stream={}, diff={}",
3970						test_name, i, b, s, diff
3971					);
3972                }
3973            }
3974        }
3975
3976        let valid_macd_count = stream_macd
3977            .iter()
3978            .skip(warmup)
3979            .filter(|v| !v.is_nan())
3980            .count();
3981        let valid_signal_count = stream_signal
3982            .iter()
3983            .skip(warmup)
3984            .filter(|v| !v.is_nan())
3985            .count();
3986
3987        assert!(
3988            valid_macd_count > 0,
3989            "[{}] VWMACD streaming produced no valid MACD values after warmup",
3990            test_name
3991        );
3992        assert!(
3993            valid_signal_count > 0,
3994            "[{}] VWMACD streaming produced no valid signal values after warmup",
3995            test_name
3996        );
3997
3998        Ok(())
3999    }
4000
4001    fn check_batch_default_row(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
4002        skip_if_unsupported!(kernel, test);
4003
4004        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
4005        let c = read_candles_from_csv(file)?;
4006
4007        let close = &c.close;
4008        let volume = &c.volume;
4009
4010        let output = VwmacdBatchBuilder::new()
4011            .kernel(kernel)
4012            .apply_slices(close, volume)?;
4013
4014        let def = VwmacdParams::default();
4015        let (macd_row, signal_row, hist_row) =
4016            output.values_for(&def).expect("default row missing");
4017        assert_eq!(macd_row.len(), close.len());
4018
4019        let expected_macd = [
4020            -394.95161155,
4021            -508.29106210,
4022            -490.70190723,
4023            -388.94996199,
4024            -341.13720646,
4025        ];
4026        let start = macd_row.len() - 5;
4027        for (i, &v) in macd_row[start..].iter().enumerate() {
4028            assert!(
4029                (v - expected_macd[i]).abs() < 1e-3,
4030                "[{test}] default-row MACD mismatch at idx {i}: got {v}, expected {}",
4031                expected_macd[i]
4032            );
4033        }
4034
4035        let input = VwmacdInput::from_candles(&c, "close", "volume", def.clone());
4036        let result = vwmacd_with_kernel(&input, kernel)?;
4037
4038        let expected_signal = [
4039            -539.48861567,
4040            -533.24910496,
4041            -524.73966541,
4042            -497.58172247,
4043            -466.29282108,
4044        ];
4045        let signal_slice = &result.signal[result.signal.len() - 5..];
4046        for (i, &v) in signal_slice.iter().enumerate() {
4047            assert!(
4048                (v - expected_signal[i]).abs() < 1e-3,
4049                "[{test}] default-row Signal mismatch at idx {i}: got {v}, expected {}",
4050                expected_signal[i]
4051            );
4052        }
4053
4054        let expected_histogram = [
4055            144.53700412,
4056            24.95804286,
4057            34.03775818,
4058            108.63176274,
4059            125.15561462,
4060        ];
4061        let hist_slice = &result.hist[result.hist.len() - 5..];
4062        for (i, &v) in hist_slice.iter().enumerate() {
4063            assert!(
4064                (v - expected_histogram[i]).abs() < 1e-3,
4065                "[{test}] default-row Histogram mismatch at idx {i}: got {v}, expected {}",
4066                expected_histogram[i]
4067            );
4068        }
4069
4070        Ok(())
4071    }
4072
4073    fn check_batch_grid(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
4074        skip_if_unsupported!(kernel, test);
4075
4076        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
4077        let c = read_candles_from_csv(file)?;
4078
4079        let close = &c.close;
4080        let volume = &c.volume;
4081
4082        let output = VwmacdBatchBuilder::new()
4083            .kernel(kernel)
4084            .fast_range(10, 14, 2)
4085            .slow_range(20, 26, 3)
4086            .signal_range(5, 9, 2)
4087            .apply_slices(close, volume)?;
4088
4089        assert_eq!(output.cols, close.len());
4090        assert_eq!(output.rows, 3 * 3 * 3);
4091
4092        let params = VwmacdParams {
4093            fast_period: Some(12),
4094            slow_period: Some(23),
4095            signal_period: Some(7),
4096            fast_ma_type: Some("sma".to_string()),
4097            slow_ma_type: Some("sma".to_string()),
4098            signal_ma_type: Some("ema".to_string()),
4099        };
4100        let (macd_row, signal_row, hist_row) =
4101            output.values_for(&params).expect("row for params missing");
4102        assert_eq!(macd_row.len(), close.len());
4103        Ok(())
4104    }
4105
4106    #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
4107    #[test]
4108    fn test_vwmacd_into_matches_api() -> Result<(), Box<dyn Error>> {
4109        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
4110        let candles = read_candles_from_csv(file_path)?;
4111        let input = VwmacdInput::with_default_candles(&candles);
4112
4113        let base = vwmacd(&input)?;
4114
4115        let n = candles.close.len();
4116        let mut macd_out = vec![0.0; n];
4117        let mut signal_out = vec![0.0; n];
4118        let mut hist_out = vec![0.0; n];
4119        vwmacd_into(&input, &mut macd_out, &mut signal_out, &mut hist_out)?;
4120
4121        assert_eq!(base.macd.len(), n);
4122        assert_eq!(base.signal.len(), n);
4123        assert_eq!(base.hist.len(), n);
4124
4125        fn eq_or_both_nan(a: f64, b: f64) -> bool {
4126            (a.is_nan() && b.is_nan()) || (a == b)
4127        }
4128
4129        for i in 0..n {
4130            assert!(
4131                eq_or_both_nan(base.macd[i], macd_out[i]),
4132                "MACD mismatch at {}: base={}, into={}",
4133                i,
4134                base.macd[i],
4135                macd_out[i]
4136            );
4137            assert!(
4138                eq_or_both_nan(base.signal[i], signal_out[i]),
4139                "Signal mismatch at {}: base={}, into={}",
4140                i,
4141                base.signal[i],
4142                signal_out[i]
4143            );
4144            assert!(
4145                eq_or_both_nan(base.hist[i], hist_out[i]),
4146                "Hist mismatch at {}: base={}, into={}",
4147                i,
4148                base.hist[i],
4149                hist_out[i]
4150            );
4151        }
4152
4153        Ok(())
4154    }
4155
4156    fn check_batch_param_map(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
4157        skip_if_unsupported!(kernel, test);
4158
4159        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
4160        let c = read_candles_from_csv(file)?;
4161
4162        let close = &c.close;
4163        let volume = &c.volume;
4164
4165        let batch = VwmacdBatchBuilder::new()
4166            .kernel(kernel)
4167            .fast_range(12, 14, 1)
4168            .slow_range(26, 28, 1)
4169            .signal_range(9, 11, 1)
4170            .apply_slices(close, volume)?;
4171
4172        for (ix, param) in batch.params.iter().enumerate() {
4173            let by_index = &batch.macd[ix * batch.cols..(ix + 1) * batch.cols];
4174            let (by_api_macd, by_api_signal, by_api_hist) = batch.values_for(param).unwrap();
4175
4176            assert_eq!(by_index.len(), by_api_macd.len());
4177            for (i, (&x, &y)) in by_index.iter().zip(by_api_macd.iter()).enumerate() {
4178                if x.is_nan() && y.is_nan() {
4179                    continue;
4180                }
4181                assert!(
4182                    (x == y),
4183                    "[{}] param {:?}, mismatch at idx {}: got {}, expected {}",
4184                    test,
4185                    param,
4186                    i,
4187                    x,
4188                    y
4189                );
4190            }
4191        }
4192        Ok(())
4193    }
4194
4195    fn check_batch_custom_ma_types(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
4196        skip_if_unsupported!(kernel, test);
4197
4198        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
4199        let c = read_candles_from_csv(file)?;
4200
4201        let close = &c.close;
4202        let volume = &c.volume;
4203
4204        let output = VwmacdBatchBuilder::new()
4205            .kernel(kernel)
4206            .fast_ma_type("ema".to_string())
4207            .slow_ma_type("wma".to_string())
4208            .signal_ma_type("sma".to_string())
4209            .apply_slices(close, volume)?;
4210
4211        let params = VwmacdParams {
4212            fast_period: Some(12),
4213            slow_period: Some(26),
4214            signal_period: Some(9),
4215            fast_ma_type: Some("ema".to_string()),
4216            slow_ma_type: Some("wma".to_string()),
4217            signal_ma_type: Some("sma".to_string()),
4218        };
4219        let (macd_row, signal_row, hist_row) = output
4220            .values_for(&params)
4221            .expect("custom MA types row missing");
4222        assert_eq!(macd_row.len(), close.len());
4223        Ok(())
4224    }
4225
4226    macro_rules! gen_batch_tests {
4227        ($fn_name:ident) => {
4228            paste::paste! {
4229                #[test] fn [<$fn_name _scalar>]()      {
4230                    let _ = $fn_name(stringify!([<$fn_name _scalar>]), Kernel::ScalarBatch);
4231                }
4232                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
4233                #[test] fn [<$fn_name _avx2>]()        {
4234                    let _ = $fn_name(stringify!([<$fn_name _avx2>]), Kernel::Avx2Batch);
4235                }
4236                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
4237                #[test] fn [<$fn_name _avx512>]()      {
4238                    let _ = $fn_name(stringify!([<$fn_name _avx512>]), Kernel::Avx512Batch);
4239                }
4240                #[test] fn [<$fn_name _auto_detect>]() {
4241                    let _ = $fn_name(stringify!([<$fn_name _auto_detect>]), Kernel::Auto);
4242                }
4243            }
4244        };
4245    }
4246
4247    #[cfg(debug_assertions)]
4248    fn check_batch_no_poison(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
4249        skip_if_unsupported!(kernel, test);
4250
4251        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
4252        let c = read_candles_from_csv(file)?;
4253
4254        let close = &c.close;
4255        let volume = &c.volume;
4256
4257        let test_configs = vec![
4258            (2, 10, 2, 11, 20, 3, 2, 5, 1),
4259            (5, 15, 5, 16, 30, 5, 3, 9, 3),
4260            (10, 30, 10, 31, 60, 10, 5, 15, 5),
4261            (2, 5, 1, 6, 10, 1, 2, 4, 1),
4262            (12, 12, 0, 26, 26, 0, 9, 9, 0),
4263            (8, 16, 4, 20, 40, 10, 5, 10, 5),
4264        ];
4265
4266        for (
4267            cfg_idx,
4268            &(
4269                fast_start,
4270                fast_end,
4271                fast_step,
4272                slow_start,
4273                slow_end,
4274                slow_step,
4275                signal_start,
4276                signal_end,
4277                signal_step,
4278            ),
4279        ) in test_configs.iter().enumerate()
4280        {
4281            let mut builder = VwmacdBatchBuilder::new().kernel(kernel);
4282
4283            if fast_step > 0 {
4284                builder = builder.fast_range(fast_start, fast_end, fast_step);
4285            } else {
4286                builder = builder.fast_range(fast_start, fast_start, 1);
4287            }
4288
4289            if slow_step > 0 {
4290                builder = builder.slow_range(slow_start, slow_end, slow_step);
4291            } else {
4292                builder = builder.slow_range(slow_start, slow_start, 1);
4293            }
4294
4295            if signal_step > 0 {
4296                builder = builder.signal_range(signal_start, signal_end, signal_step);
4297            } else {
4298                builder = builder.signal_range(signal_start, signal_start, 1);
4299            }
4300
4301            let output = builder.apply_slices(close, volume)?;
4302
4303            for (idx, &val) in output.macd.iter().enumerate() {
4304                if val.is_nan() {
4305                    continue;
4306                }
4307
4308                let bits = val.to_bits();
4309                let row = idx / output.cols;
4310                let col = idx % output.cols;
4311                let combo = &output.params[row];
4312
4313                if bits == 0x11111111_11111111 {
4314                    panic!(
4315                        "[{}] Config {}: Found alloc_with_nan_prefix poison value {} (0x{:016X}) \
4316						 at row {} col {} (flat index {}) with params: fast={}, slow={}, signal={}, \
4317						 fast_ma={}, slow_ma={}, signal_ma={}",
4318                        test,
4319                        cfg_idx,
4320                        val,
4321                        bits,
4322                        row,
4323                        col,
4324                        idx,
4325                        combo.fast_period.unwrap_or(12),
4326                        combo.slow_period.unwrap_or(26),
4327                        combo.signal_period.unwrap_or(9),
4328                        combo.fast_ma_type.as_deref().unwrap_or("sma"),
4329                        combo.slow_ma_type.as_deref().unwrap_or("sma"),
4330                        combo.signal_ma_type.as_deref().unwrap_or("ema")
4331                    );
4332                }
4333
4334                if bits == 0x22222222_22222222 {
4335                    panic!(
4336                        "[{}] Config {}: Found init_matrix_prefixes poison value {} (0x{:016X}) \
4337						 at row {} col {} (flat index {}) with params: fast={}, slow={}, signal={}, \
4338						 fast_ma={}, slow_ma={}, signal_ma={}",
4339                        test,
4340                        cfg_idx,
4341                        val,
4342                        bits,
4343                        row,
4344                        col,
4345                        idx,
4346                        combo.fast_period.unwrap_or(12),
4347                        combo.slow_period.unwrap_or(26),
4348                        combo.signal_period.unwrap_or(9),
4349                        combo.fast_ma_type.as_deref().unwrap_or("sma"),
4350                        combo.slow_ma_type.as_deref().unwrap_or("sma"),
4351                        combo.signal_ma_type.as_deref().unwrap_or("ema")
4352                    );
4353                }
4354
4355                if bits == 0x33333333_33333333 {
4356                    panic!(
4357                        "[{}] Config {}: Found make_uninit_matrix poison value {} (0x{:016X}) \
4358						 at row {} col {} (flat index {}) with params: fast={}, slow={}, signal={}, \
4359						 fast_ma={}, slow_ma={}, signal_ma={}",
4360                        test,
4361                        cfg_idx,
4362                        val,
4363                        bits,
4364                        row,
4365                        col,
4366                        idx,
4367                        combo.fast_period.unwrap_or(12),
4368                        combo.slow_period.unwrap_or(26),
4369                        combo.signal_period.unwrap_or(9),
4370                        combo.fast_ma_type.as_deref().unwrap_or("sma"),
4371                        combo.slow_ma_type.as_deref().unwrap_or("sma"),
4372                        combo.signal_ma_type.as_deref().unwrap_or("ema")
4373                    );
4374                }
4375            }
4376        }
4377
4378        let ma_type_configs = vec![
4379            ("ema", "ema", "ema"),
4380            ("sma", "wma", "ema"),
4381            ("wma", "wma", "sma"),
4382        ];
4383
4384        for (cfg_idx, &(fast_ma, slow_ma, signal_ma)) in ma_type_configs.iter().enumerate() {
4385            let output = VwmacdBatchBuilder::new()
4386                .kernel(kernel)
4387                .fast_range(10, 15, 5)
4388                .slow_range(20, 30, 10)
4389                .signal_range(5, 10, 5)
4390                .fast_ma_type(fast_ma.to_string())
4391                .slow_ma_type(slow_ma.to_string())
4392                .signal_ma_type(signal_ma.to_string())
4393                .apply_slices(close, volume)?;
4394
4395            for (idx, &val) in output.macd.iter().enumerate() {
4396                if val.is_nan() {
4397                    continue;
4398                }
4399
4400                let bits = val.to_bits();
4401                let row = idx / output.cols;
4402                let col = idx % output.cols;
4403                let combo = &output.params[row];
4404
4405                if bits == 0x11111111_11111111
4406                    || bits == 0x22222222_22222222
4407                    || bits == 0x33333333_33333333
4408                {
4409                    let poison_type = if bits == 0x11111111_11111111 {
4410                        "alloc_with_nan_prefix"
4411                    } else if bits == 0x22222222_22222222 {
4412                        "init_matrix_prefixes"
4413                    } else {
4414                        "make_uninit_matrix"
4415                    };
4416
4417                    panic!(
4418                        "[{}] MA Type Config {}: Found {} poison value {} (0x{:016X}) \
4419						 at row {} col {} (flat index {}) with params: fast={}, slow={}, signal={}, \
4420						 fast_ma={}, slow_ma={}, signal_ma={}",
4421                        test,
4422                        cfg_idx,
4423                        poison_type,
4424                        val,
4425                        bits,
4426                        row,
4427                        col,
4428                        idx,
4429                        combo.fast_period.unwrap_or(12),
4430                        combo.slow_period.unwrap_or(26),
4431                        combo.signal_period.unwrap_or(9),
4432                        combo.fast_ma_type.as_deref().unwrap_or("sma"),
4433                        combo.slow_ma_type.as_deref().unwrap_or("sma"),
4434                        combo.signal_ma_type.as_deref().unwrap_or("ema")
4435                    );
4436                }
4437            }
4438        }
4439
4440        Ok(())
4441    }
4442
4443    #[cfg(not(debug_assertions))]
4444    fn check_batch_no_poison(_test: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
4445        Ok(())
4446    }
4447
4448    gen_batch_tests!(check_batch_default_row);
4449    gen_batch_tests!(check_batch_grid);
4450    gen_batch_tests!(check_batch_param_map);
4451    gen_batch_tests!(check_batch_custom_ma_types);
4452    gen_batch_tests!(check_batch_no_poison);
4453}