Skip to main content

vector_ta/indicators/
macz.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, PyList};
9
10#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
11use js_sys;
12#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
13use serde::{Deserialize, Serialize};
14#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
15use wasm_bindgen::prelude::*;
16
17use crate::utilities::data_loader::{source_type, Candles};
18use crate::utilities::enums::Kernel;
19use crate::utilities::helpers::{
20    alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
21    make_uninit_matrix,
22};
23#[cfg(feature = "python")]
24use crate::utilities::kernel_validation::validate_kernel;
25use aligned_vec::{AVec, CACHELINE_ALIGN};
26
27use crate::indicators::moving_averages::sma::{sma_with_kernel, SmaInput, SmaParams};
28use crate::indicators::stddev::{stddev_with_kernel, StdDevInput, StdDevParams};
29
30#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
31use core::arch::x86_64::*;
32
33#[cfg(not(target_arch = "wasm32"))]
34use rayon::prelude::*;
35
36use std::convert::AsRef;
37use std::error::Error;
38use std::mem::MaybeUninit;
39use thiserror::Error;
40
41impl<'a> AsRef<[f64]> for MaczInput<'a> {
42    #[inline(always)]
43    fn as_ref(&self) -> &[f64] {
44        match &self.data {
45            MaczData::Slice(sl) => sl,
46            MaczData::SliceWithVolume { data, .. } => data,
47            MaczData::Candles {
48                candles, source, ..
49            } => source_type(candles, source),
50        }
51    }
52}
53
54#[derive(Debug, Clone)]
55pub enum MaczData<'a> {
56    Candles {
57        candles: &'a Candles,
58        source: &'a str,
59        volume: &'a [f64],
60    },
61    Slice(&'a [f64]),
62    SliceWithVolume {
63        data: &'a [f64],
64        volume: &'a [f64],
65    },
66}
67
68#[derive(Debug, Clone)]
69pub struct MaczOutput {
70    pub values: Vec<f64>,
71}
72
73#[derive(Debug, Clone)]
74#[cfg_attr(
75    all(target_arch = "wasm32", feature = "wasm"),
76    derive(Serialize, Deserialize)
77)]
78pub struct MaczParams {
79    pub fast_length: Option<usize>,
80    pub slow_length: Option<usize>,
81    pub signal_length: Option<usize>,
82    pub lengthz: Option<usize>,
83    pub length_stdev: Option<usize>,
84    pub a: Option<f64>,
85    pub b: Option<f64>,
86    pub use_lag: Option<bool>,
87    pub gamma: Option<f64>,
88}
89
90impl Default for MaczParams {
91    fn default() -> Self {
92        Self {
93            fast_length: Some(12),
94            slow_length: Some(25),
95            signal_length: Some(9),
96            lengthz: Some(20),
97            length_stdev: Some(25),
98            a: Some(1.0),
99            b: Some(1.0),
100            use_lag: Some(false),
101            gamma: Some(0.02),
102        }
103    }
104}
105
106#[derive(Debug, Clone)]
107pub struct MaczInput<'a> {
108    pub data: MaczData<'a>,
109    pub params: MaczParams,
110}
111
112impl<'a> MaczInput<'a> {
113    #[inline]
114    pub fn from_candles(c: &'a Candles, s: &'a str, p: MaczParams) -> Self {
115        Self::from_candles_with_volume(c, s, &c.volume, p)
116    }
117
118    #[inline]
119    pub fn from_candles_with_volume(
120        c: &'a Candles,
121        s: &'a str,
122        volume: &'a [f64],
123        p: MaczParams,
124    ) -> Self {
125        Self {
126            data: MaczData::Candles {
127                candles: c,
128                source: s,
129                volume,
130            },
131            params: p,
132        }
133    }
134
135    #[inline]
136    pub fn from_slice(sl: &'a [f64], p: MaczParams) -> Self {
137        Self {
138            data: MaczData::Slice(sl),
139            params: p,
140        }
141    }
142
143    #[inline]
144    pub fn with_default_candles(c: &'a Candles) -> Self {
145        Self::from_candles(c, "close", MaczParams::default())
146    }
147
148    #[inline]
149    pub fn with_default_candles_auto_volume(c: &'a Candles) -> Self {
150        Self::with_default_candles(c)
151    }
152
153    #[inline]
154    pub fn from_slice_with_volume(sl: &'a [f64], vol: &'a [f64], p: MaczParams) -> Self {
155        Self {
156            data: MaczData::SliceWithVolume {
157                data: sl,
158                volume: vol,
159            },
160            params: p,
161        }
162    }
163
164    #[inline]
165    pub fn with_default_slice(sl: &'a [f64]) -> Self {
166        Self::from_slice(sl, MaczParams::default())
167    }
168
169    #[inline]
170    pub fn get_fast_length(&self) -> usize {
171        self.params.fast_length.unwrap_or(12)
172    }
173
174    #[inline]
175    pub fn get_slow_length(&self) -> usize {
176        self.params.slow_length.unwrap_or(25)
177    }
178
179    #[inline]
180    pub fn get_signal_length(&self) -> usize {
181        self.params.signal_length.unwrap_or(9)
182    }
183}
184
185#[derive(Debug, Clone)]
186pub struct MaczBuilder {
187    fast_length: Option<usize>,
188    slow_length: Option<usize>,
189    signal_length: Option<usize>,
190    lengthz: Option<usize>,
191    length_stdev: Option<usize>,
192    a: Option<f64>,
193    b: Option<f64>,
194    use_lag: Option<bool>,
195    gamma: Option<f64>,
196    kernel: Kernel,
197}
198
199impl Default for MaczBuilder {
200    fn default() -> Self {
201        Self {
202            fast_length: None,
203            slow_length: None,
204            signal_length: None,
205            lengthz: None,
206            length_stdev: None,
207            a: None,
208            b: None,
209            use_lag: None,
210            gamma: None,
211            kernel: Kernel::Auto,
212        }
213    }
214}
215
216impl MaczBuilder {
217    #[inline(always)]
218    pub fn new() -> Self {
219        Self::default()
220    }
221
222    #[inline(always)]
223    pub fn fast_length(mut self, n: usize) -> Self {
224        self.fast_length = Some(n);
225        self
226    }
227
228    #[inline(always)]
229    pub fn slow_length(mut self, n: usize) -> Self {
230        self.slow_length = Some(n);
231        self
232    }
233
234    #[inline(always)]
235    pub fn signal_length(mut self, n: usize) -> Self {
236        self.signal_length = Some(n);
237        self
238    }
239
240    #[inline(always)]
241    pub fn lengthz(mut self, n: usize) -> Self {
242        self.lengthz = Some(n);
243        self
244    }
245
246    #[inline(always)]
247    pub fn length_stdev(mut self, n: usize) -> Self {
248        self.length_stdev = Some(n);
249        self
250    }
251
252    #[inline(always)]
253    pub fn a(mut self, val: f64) -> Self {
254        self.a = Some(val);
255        self
256    }
257
258    #[inline(always)]
259    pub fn b(mut self, val: f64) -> Self {
260        self.b = Some(val);
261        self
262    }
263
264    #[inline(always)]
265    pub fn use_lag(mut self, val: bool) -> Self {
266        self.use_lag = Some(val);
267        self
268    }
269
270    #[inline(always)]
271    pub fn gamma(mut self, val: f64) -> Self {
272        self.gamma = Some(val);
273        self
274    }
275
276    #[inline(always)]
277    pub fn kernel(mut self, k: Kernel) -> Self {
278        self.kernel = k;
279        self
280    }
281
282    pub fn build_params(self) -> MaczParams {
283        MaczParams {
284            fast_length: self.fast_length.or(Some(12)),
285            slow_length: self.slow_length.or(Some(25)),
286            signal_length: self.signal_length.or(Some(9)),
287            lengthz: self.lengthz.or(Some(20)),
288            length_stdev: self.length_stdev.or(Some(25)),
289            a: self.a.or(Some(1.0)),
290            b: self.b.or(Some(1.0)),
291            use_lag: self.use_lag.or(Some(false)),
292            gamma: self.gamma.or(Some(0.02)),
293        }
294    }
295
296    pub fn apply_slice(self, data: &[f64]) -> Result<MaczOutput, MaczError> {
297        let kernel = if self.kernel == Kernel::Auto {
298            Kernel::Scalar
299        } else {
300            self.kernel
301        };
302        let params = self.build_params();
303        let input = MaczInput::from_slice(data, params);
304        macz_with_kernel(&input, kernel)
305    }
306
307    pub fn apply_candles(self, c: &Candles, src: &str) -> Result<MaczOutput, MaczError> {
308        let k = if self.kernel == Kernel::Auto {
309            Kernel::Scalar
310        } else {
311            self.kernel
312        };
313        macz_with_kernel(&MaczInput::from_candles(c, src, self.build_params()), k)
314    }
315
316    pub fn apply_candles_with_volume(
317        self,
318        c: &Candles,
319        src: &str,
320        volume: &[f64],
321    ) -> Result<MaczOutput, MaczError> {
322        let kernel = if self.kernel == Kernel::Auto {
323            Kernel::Scalar
324        } else {
325            self.kernel
326        };
327        let params = self.build_params();
328        let input = MaczInput::from_candles_with_volume(c, src, volume, params);
329        macz_with_kernel(&input, kernel)
330    }
331
332    #[inline(always)]
333    pub fn apply(self, c: &Candles) -> Result<MaczOutput, MaczError> {
334        let k = if self.kernel == Kernel::Auto {
335            Kernel::Scalar
336        } else {
337            self.kernel
338        };
339        let p = self.build_params();
340
341        let input = MaczInput::from_candles(c, "close", p);
342        macz_with_kernel(&input, k)
343    }
344
345    #[inline(always)]
346    pub fn into_stream(self) -> Result<MaczStream, MaczError> {
347        MaczStream::try_new(self.build_params())
348    }
349}
350
351#[derive(Debug, Error)]
352pub enum MaczError {
353    #[error("macz: Input data slice is empty.")]
354    EmptyInputData,
355
356    #[error("macz: All values are NaN.")]
357    AllValuesNaN,
358
359    #[error("macz: Invalid period: period = {period}, data length = {data_len}")]
360    InvalidPeriod { period: usize, data_len: usize },
361
362    #[error("macz: Not enough valid data: needed = {needed}, valid = {valid}")]
363    NotEnoughValidData { needed: usize, valid: usize },
364
365    #[error("macz: Invalid gamma: {gamma}")]
366    InvalidGamma { gamma: f64 },
367
368    #[error("macz: A out of range: {a} (must be between -2.0 and 2.0)")]
369    InvalidA { a: f64 },
370
371    #[error("macz: B out of range: {b} (must be between -2.0 and 2.0)")]
372    InvalidB { b: f64 },
373
374    #[error("macz: Volume data required for VWAP calculation")]
375    VolumeRequired,
376
377    #[error("macz: {msg}")]
378    InvalidParameter { msg: String },
379
380    #[error("macz: Output length mismatch: expected {expected}, got {got}")]
381    OutputLengthMismatch { expected: usize, got: usize },
382
383    #[error("macz: Invalid range: start={start}, end={end}, step={step}")]
384    InvalidRange {
385        start: String,
386        end: String,
387        step: String,
388    },
389
390    #[error("macz: Invalid kernel for batch: {0:?}")]
391    InvalidKernelForBatch(crate::utilities::enums::Kernel),
392}
393
394pub struct MaczWorkspace {
395    vwap: Vec<f64>,
396    zvwap: Vec<f64>,
397    fast_ma: Vec<f64>,
398    slow_ma: Vec<f64>,
399    macd: Vec<f64>,
400    stdev: Vec<f64>,
401    macz_t: Vec<f64>,
402    macz: Vec<f64>,
403    signal: Vec<f64>,
404}
405
406impl MaczWorkspace {
407    pub fn new(len: usize) -> Self {
408        Self {
409            vwap: Vec::with_capacity(len),
410            zvwap: Vec::with_capacity(len),
411            fast_ma: Vec::with_capacity(len),
412            slow_ma: Vec::with_capacity(len),
413            macd: Vec::with_capacity(len),
414            stdev: Vec::with_capacity(len),
415            macz_t: Vec::with_capacity(len),
416            macz: Vec::with_capacity(len),
417            signal: Vec::with_capacity(len),
418        }
419    }
420
421    pub fn resize(&mut self, len: usize) {
422        self.vwap.resize(len, f64::NAN);
423        self.zvwap.resize(len, f64::NAN);
424        self.fast_ma.resize(len, f64::NAN);
425        self.slow_ma.resize(len, f64::NAN);
426        self.macd.resize(len, f64::NAN);
427        self.stdev.resize(len, f64::NAN);
428        self.macz_t.resize(len, f64::NAN);
429        self.macz.resize(len, f64::NAN);
430        self.signal.resize(len, f64::NAN);
431    }
432}
433
434#[inline]
435fn calculate_vwap_into(
436    close: &[f64],
437    volume: Option<&[f64]>,
438    period: usize,
439    first: usize,
440    kernel: Kernel,
441    out: &mut [f64],
442) -> Result<(), MaczError> {
443    let len = close.len();
444    let start = first + period - 1;
445    if start > len {
446        return Err(MaczError::NotEnoughValidData {
447            needed: start - first,
448            valid: len - first,
449        });
450    }
451
452    if let Some(vol) = volume {
453        if vol.len() != len {
454            return Err(MaczError::InvalidParameter {
455                msg: "Close and volume arrays must have same length".into(),
456            });
457        }
458        for i in start..len {
459            let s = i + 1 - period;
460            let mut pv = 0.0;
461            let mut vs = 0.0;
462            let mut ok = true;
463            for j in s..=i {
464                let x = close[j];
465                let v = vol[j];
466                if x.is_nan() || v.is_nan() {
467                    ok = false;
468                    break;
469                }
470                pv += x * v;
471                vs += v;
472            }
473            out[i] = if ok && vs > 0.0 { pv / vs } else { f64::NAN };
474        }
475    } else {
476        let sma_input = SmaInput::from_slice(
477            close,
478            SmaParams {
479                period: Some(period),
480            },
481        );
482        let sma = sma_with_kernel(&sma_input, kernel).map_err(|e| MaczError::InvalidParameter {
483            msg: format!("VWAP=SMA error: {e}"),
484        })?;
485
486        out[start..].copy_from_slice(&sma.values[start..]);
487    }
488    Ok(())
489}
490
491#[inline]
492fn calculate_zvwap_into(
493    close: &[f64],
494    vwap: &[f64],
495    period: usize,
496    first: usize,
497    out: &mut [f64],
498) -> Result<(), MaczError> {
499    let len = close.len();
500    let start = first + period - 1;
501    if start > len {
502        return Err(MaczError::NotEnoughValidData {
503            needed: start - first,
504            valid: len - first,
505        });
506    }
507    for i in start..len {
508        let mean = vwap[i];
509        if mean.is_nan() {
510            out[i] = f64::NAN;
511            continue;
512        }
513        let s = i + 1 - period;
514
515        let mut sum = 0.0;
516        let mut ok = true;
517        for j in s..=i {
518            let x = close[j];
519            if x.is_nan() {
520                ok = false;
521                break;
522            }
523            let d = x - mean;
524            sum += d * d;
525        }
526        if !ok {
527            out[i] = f64::NAN;
528            continue;
529        }
530
531        let var = sum / period as f64;
532        let sd = var.sqrt();
533        out[i] = if sd > 0.0 {
534            (close[i] - mean) / sd
535        } else {
536            0.0
537        };
538    }
539    Ok(())
540}
541
542#[inline]
543fn stdev_source_population_into(
544    data: &[f64],
545    period: usize,
546    first: usize,
547    kernel: Kernel,
548    out: &mut [f64],
549) -> Result<(), MaczError> {
550    let len = data.len();
551    let start = first + period - 1;
552    if start > len {
553        return Err(MaczError::NotEnoughValidData {
554            needed: start - first,
555            valid: len - first,
556        });
557    }
558
559    let mean = sma_with_kernel(
560        &SmaInput::from_slice(
561            data,
562            SmaParams {
563                period: Some(period),
564            },
565        ),
566        kernel,
567    )
568    .map_err(|e| MaczError::InvalidParameter {
569        msg: format!("StdDev mean SMA error: {e}"),
570    })?
571    .values;
572
573    for i in start..len {
574        let m = mean[i];
575        if m.is_nan() {
576            out[i] = f64::NAN;
577            continue;
578        }
579        let s = i + 1 - period;
580
581        let mut sum = 0.0;
582        let mut ok = true;
583        for j in s..=i {
584            let x = data[j];
585            if x.is_nan() {
586                ok = false;
587                break;
588            }
589            let d = x - m;
590            sum += d * d;
591        }
592        if !ok {
593            out[i] = f64::NAN;
594            continue;
595        }
596        out[i] = (sum / period as f64).sqrt();
597    }
598    Ok(())
599}
600
601fn apply_laguerre(input: &[f64], gamma: f64, output: &mut [f64]) {
602    let len = input.len();
603    let mut l0 = 0.0;
604    let mut l1 = 0.0;
605    let mut l2 = 0.0;
606    let mut l3 = 0.0;
607
608    for i in 0..len {
609        if input[i].is_nan() {
610            output[i] = f64::NAN;
611        } else {
612            let s = input[i];
613            let new_l0 = (1.0 - gamma) * s + gamma * l0;
614            let new_l1 = -gamma * new_l0 + l0 + gamma * l1;
615            let new_l2 = -gamma * new_l1 + l1 + gamma * l2;
616            let new_l3 = -gamma * new_l2 + l2 + gamma * l3;
617
618            l0 = new_l0;
619            l1 = new_l1;
620            l2 = new_l2;
621            l3 = new_l3;
622
623            output[i] = (l0 + 2.0 * l1 + 2.0 * l2 + l3) / 6.0;
624        }
625    }
626}
627
628#[inline(always)]
629fn macz_warm_len(first: usize, slow: usize, lz: usize, lsd: usize, sig: usize) -> usize {
630    first + slow.max(lz).max(lsd) + sig - 2
631}
632
633#[inline(always)]
634fn macz_prepare<'a>(
635    input: &'a MaczInput,
636    kernel: Kernel,
637) -> Result<
638    (
639        &'a [f64],
640        Option<&'a [f64]>,
641        usize,
642        usize,
643        usize,
644        usize,
645        usize,
646        f64,
647        f64,
648        bool,
649        f64,
650        usize,
651        Kernel,
652    ),
653    MaczError,
654> {
655    let data = input.as_ref();
656    let len = data.len();
657    if len == 0 {
658        return Err(MaczError::EmptyInputData);
659    }
660    let first = data
661        .iter()
662        .position(|x| !x.is_nan())
663        .ok_or(MaczError::AllValuesNaN)?;
664
665    let fast = input.params.fast_length.unwrap_or(12);
666    let slow = input.params.slow_length.unwrap_or(25);
667    let sig = input.params.signal_length.unwrap_or(9);
668    let lz = input.params.lengthz.unwrap_or(20);
669    let lsd = input.params.length_stdev.unwrap_or(25);
670    let a = input.params.a.unwrap_or(1.0);
671    let b = input.params.b.unwrap_or(1.0);
672    let use_lag = input.params.use_lag.unwrap_or(false);
673    let gamma = input.params.gamma.unwrap_or(0.02);
674
675    if fast == 0 || slow == 0 || sig == 0 || lz == 0 || lsd == 0 {
676        return Err(MaczError::InvalidPeriod {
677            period: 0,
678            data_len: len,
679        });
680    }
681
682    let need = fast.max(slow).max(lz).max(lsd);
683    let valid = len - first;
684    if valid < need {
685        return Err(MaczError::NotEnoughValidData {
686            needed: need,
687            valid,
688        });
689    }
690
691    if !(-2.0..=2.0).contains(&a) {
692        return Err(MaczError::InvalidA { a });
693    }
694    if !(-2.0..=2.0).contains(&b) {
695        return Err(MaczError::InvalidB { b });
696    }
697    if !(0.0..1.0).contains(&gamma) {
698        return Err(MaczError::InvalidGamma { gamma });
699    }
700
701    let vol_opt = match &input.data {
702        MaczData::Candles { volume, .. } => Some(*volume),
703        MaczData::SliceWithVolume { volume, .. } => Some(*volume),
704        MaczData::Slice(_) => None,
705    };
706
707    let warm_hist = macz_warm_len(first, slow, lz, lsd, sig);
708
709    let chosen = match kernel {
710        Kernel::Auto => Kernel::Scalar,
711        k => k,
712    };
713    Ok((
714        data, vol_opt, fast, slow, sig, lz, lsd, a, b, use_lag, gamma, warm_hist, chosen,
715    ))
716}
717
718#[inline(always)]
719fn macz_compute_into_tail_only(
720    data: &[f64],
721    vol: Option<&[f64]>,
722    fast: usize,
723    slow: usize,
724    sig: usize,
725    lz: usize,
726    lsd: usize,
727    a: f64,
728    b: f64,
729    use_lag: bool,
730    gamma: f64,
731    warm_hist: usize,
732    kernel: Kernel,
733    out: &mut [f64],
734) -> Result<(), MaczError> {
735    let len = data.len();
736    let first = data
737        .iter()
738        .position(|x| !x.is_nan())
739        .ok_or(MaczError::AllValuesNaN)?;
740
741    if kernel == Kernel::Scalar {
742        unsafe {
743            return macz_scalar_classic(
744                data, vol, fast, slow, sig, lz, lsd, a, b, use_lag, gamma, first, warm_hist, out,
745            );
746        }
747    }
748
749    let warm_m = first + slow.max(lz).max(lsd) - 1;
750
751    let mut vwap = alloc_with_nan_prefix(len, first + lz - 1);
752    calculate_vwap_into(data, vol, lz, first, kernel, &mut vwap)?;
753
754    let mut zvwap = alloc_with_nan_prefix(len, first + lz - 1);
755    calculate_zvwap_into(data, &vwap, lz, first, &mut zvwap)?;
756
757    let fast_ma = sma_with_kernel(
758        &SmaInput::from_slice(data, SmaParams { period: Some(fast) }),
759        kernel,
760    )
761    .map_err(|e| MaczError::InvalidParameter {
762        msg: format!("Fast MA error: {e}"),
763    })?
764    .values;
765    let slow_ma = sma_with_kernel(
766        &SmaInput::from_slice(data, SmaParams { period: Some(slow) }),
767        kernel,
768    )
769    .map_err(|e| MaczError::InvalidParameter {
770        msg: format!("Slow MA error: {e}"),
771    })?
772    .values;
773
774    let mut macd = alloc_with_nan_prefix(len, first + slow - 1);
775    for i in (first + slow - 1)..len {
776        let f = fast_ma[i];
777        let s = slow_ma[i];
778        macd[i] = if f.is_nan() || s.is_nan() {
779            f64::NAN
780        } else {
781            f - s
782        };
783    }
784
785    let mut stdev = alloc_with_nan_prefix(len, first + lsd - 1);
786    stdev_source_population_into(data, lsd, first, kernel, &mut stdev)?;
787
788    let mut macz_t = alloc_with_nan_prefix(len, warm_m);
789    for i in warm_m..len {
790        let z = zvwap[i];
791        let m = macd[i];
792        let sd = stdev[i];
793        macz_t[i] = if z.is_nan() || m.is_nan() || sd.is_nan() || sd <= 0.0 {
794            f64::NAN
795        } else {
796            z * a + (m / sd) * b
797        };
798    }
799
800    let mut macz = alloc_with_nan_prefix(len, warm_m);
801    if use_lag {
802        apply_laguerre(&macz_t, gamma, &mut macz);
803    } else {
804        macz[warm_m..].copy_from_slice(&macz_t[warm_m..]);
805    }
806
807    let signal = sma_with_kernel(
808        &SmaInput::from_slice(&macz, SmaParams { period: Some(sig) }),
809        kernel,
810    )
811    .map_err(|e| MaczError::InvalidParameter {
812        msg: format!("Signal MA error: {e}"),
813    })?
814    .values;
815
816    for i in warm_hist..len {
817        let s = signal[i];
818        let m = macz[i];
819        out[i] = if s.is_nan() || m.is_nan() {
820            f64::NAN
821        } else {
822            m - s
823        };
824    }
825    Ok(())
826}
827
828pub fn macz_with_kernel(input: &MaczInput, kernel: Kernel) -> Result<MaczOutput, MaczError> {
829    let (data, vol, fast, slow, sig, lz, lsd, a, b, use_lag, gamma, warm_hist, chosen) =
830        macz_prepare(input, kernel)?;
831    let mut out = alloc_with_nan_prefix(data.len(), warm_hist);
832    macz_compute_into_tail_only(
833        data, vol, fast, slow, sig, lz, lsd, a, b, use_lag, gamma, warm_hist, chosen, &mut out,
834    )?;
835    Ok(MaczOutput { values: out })
836}
837
838pub fn macz(input: &MaczInput) -> Result<MaczOutput, MaczError> {
839    macz_with_kernel(input, Kernel::Auto)
840}
841
842#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
843pub fn macz_into(input: &MaczInput, out: &mut [f64]) -> Result<(), MaczError> {
844    macz_into_slice(out, input, Kernel::Auto)
845}
846
847pub fn macz_into_slice(dst: &mut [f64], input: &MaczInput, kern: Kernel) -> Result<(), MaczError> {
848    let (data, vol, fast, slow, sig, lz, lsd, a, b, use_lag, gamma, warm_hist, chosen) =
849        macz_prepare(input, kern)?;
850    if dst.len() != data.len() {
851        return Err(MaczError::OutputLengthMismatch {
852            expected: data.len(),
853            got: dst.len(),
854        });
855    }
856
857    for v in &mut dst[..warm_hist] {
858        *v = f64::NAN;
859    }
860
861    macz_compute_into_tail_only(
862        data, vol, fast, slow, sig, lz, lsd, a, b, use_lag, gamma, warm_hist, chosen, dst,
863    )
864}
865
866pub fn macz_scalar(data: &[f64], params: &MaczParams, out: &mut [f64]) -> Result<(), MaczError> {
867    let input = MaczInput::from_slice(data, params.clone());
868    macz_into_slice(out, &input, Kernel::Scalar)
869}
870
871#[inline(always)]
872pub unsafe fn macz_scalar_classic(
873    data: &[f64],
874    vol: Option<&[f64]>,
875    fast: usize,
876    slow: usize,
877    sig: usize,
878    lz: usize,
879    lsd: usize,
880    a: f64,
881    b: f64,
882    use_lag: bool,
883    gamma: f64,
884    first_valid_idx: usize,
885    warm_hist: usize,
886    out: &mut [f64],
887) -> Result<(), MaczError> {
888    let len = data.len();
889    if len == 0 {
890        return Err(MaczError::EmptyInputData);
891    }
892
893    let fast_start = first_valid_idx + fast - 1;
894    let slow_start = first_valid_idx + slow - 1;
895    let lz_start = first_valid_idx + lz - 1;
896    let lsd_start = first_valid_idx + lsd - 1;
897    let warm_m = first_valid_idx + slow.max(lz).max(lsd) - 1;
898
899    let mut sum_fast = 0.0_f64;
900    let mut n_fast_nan = 0usize;
901    let mut sum_slow = 0.0_f64;
902    let mut n_slow_nan = 0usize;
903
904    let mut sum_lz = 0.0_f64;
905    let mut sum2_lz = 0.0_f64;
906    let mut n_lz_nan = 0usize;
907    let mut sum_lsd = 0.0_f64;
908    let mut sum2_lsd = 0.0_f64;
909    let mut n_lsd_nan = 0usize;
910
911    let has_volume = vol.is_some();
912    let vols = if has_volume { vol.unwrap() } else { &[][..] };
913    let mut sum_pv = 0.0_f64;
914    let mut sum_v = 0.0_f64;
915    let mut n_vwap_nan = 0usize;
916
917    let mut l0 = 0.0_f64;
918    let mut l1 = 0.0_f64;
919    let mut l2 = 0.0_f64;
920    let mut l3 = 0.0_f64;
921
922    let mut sig_ring: Vec<f64> = vec![f64::NAN; sig];
923    let mut sig_sum = 0.0_f64;
924    let mut sig_count = 0usize;
925    let mut sig_nan = 0usize;
926    let mut sig_head = 0usize;
927
928    let inv_fast = 1.0 / (fast as f64);
929    let inv_slow = 1.0 / (slow as f64);
930    let inv_lz = 1.0 / (lz as f64);
931    let inv_lsd = 1.0 / (lsd as f64);
932    let inv_sig = 1.0 / (sig as f64);
933
934    for i in first_valid_idx..len {
935        let x = *data.get_unchecked(i);
936        let x_is_nan = x.is_nan();
937
938        if x_is_nan {
939            n_fast_nan += 1;
940            n_slow_nan += 1;
941            n_lz_nan += 1;
942            n_lsd_nan += 1;
943        } else {
944            sum_fast = sum_fast + x;
945            sum_slow = sum_slow + x;
946            sum_lz = sum_lz + x;
947            sum2_lz = sum2_lz + x * x;
948            sum_lsd = sum_lsd + x;
949            sum2_lsd = sum2_lsd + x * x;
950        }
951
952        if has_volume {
953            let v = *vols.get_unchecked(i);
954            if x_is_nan || v.is_nan() {
955                n_vwap_nan += 1;
956            } else {
957                sum_pv = x.mul_add(v, sum_pv);
958                sum_v = sum_v + v;
959            }
960        }
961
962        if i >= first_valid_idx + fast {
963            let xo = *data.get_unchecked(i - fast);
964            if xo.is_nan() {
965                n_fast_nan -= 1;
966            } else {
967                sum_fast -= xo;
968            }
969        }
970        if i >= first_valid_idx + slow {
971            let xo = *data.get_unchecked(i - slow);
972            if xo.is_nan() {
973                n_slow_nan -= 1;
974            } else {
975                sum_slow -= xo;
976            }
977        }
978        if i >= first_valid_idx + lz {
979            let xo = *data.get_unchecked(i - lz);
980            if xo.is_nan() {
981                n_lz_nan -= 1;
982            } else {
983                sum_lz -= xo;
984                sum2_lz -= xo * xo;
985            }
986            if has_volume {
987                let vo = *vols.get_unchecked(i - lz);
988                if xo.is_nan() || vo.is_nan() {
989                    n_vwap_nan -= 1;
990                } else {
991                    sum_pv -= xo * vo;
992                    sum_v -= vo;
993                }
994            }
995        }
996        if i >= first_valid_idx + lsd {
997            let xo = *data.get_unchecked(i - lsd);
998            if xo.is_nan() {
999                n_lsd_nan -= 1;
1000            } else {
1001                sum_lsd -= xo;
1002                sum2_lsd -= xo * xo;
1003            }
1004        }
1005
1006        let have_fast = i >= fast_start && n_fast_nan == 0;
1007        let have_slow = i >= slow_start && n_slow_nan == 0;
1008
1009        let fast_ma = if have_fast {
1010            sum_fast * inv_fast
1011        } else {
1012            f64::NAN
1013        };
1014        let slow_ma = if have_slow {
1015            sum_slow * inv_slow
1016        } else {
1017            f64::NAN
1018        };
1019
1020        let macd = if fast_ma.is_nan() || slow_ma.is_nan() {
1021            f64::NAN
1022        } else {
1023            fast_ma - slow_ma
1024        };
1025
1026        let vwap_i = if i >= lz_start {
1027            if has_volume {
1028                if n_vwap_nan == 0 && sum_v > 0.0 {
1029                    sum_pv / sum_v
1030                } else {
1031                    f64::NAN
1032                }
1033            } else if n_lz_nan == 0 {
1034                sum_lz * inv_lz
1035            } else {
1036                f64::NAN
1037            }
1038        } else {
1039            f64::NAN
1040        };
1041
1042        let zvwap = if i >= lz_start && n_lz_nan == 0 && !vwap_i.is_nan() && x.is_finite() {
1043            let e = sum_lz * inv_lz;
1044            let e2 = sum2_lz * inv_lz;
1045            let var = (-2.0 * vwap_i).mul_add(e, e2) + vwap_i * vwap_i;
1046            let sd = var.max(0.0).sqrt();
1047            if sd > 0.0 {
1048                (x - vwap_i) / sd
1049            } else {
1050                0.0
1051            }
1052        } else {
1053            f64::NAN
1054        };
1055
1056        let sd_src = if i >= lsd_start && n_lsd_nan == 0 {
1057            let e = sum_lsd * inv_lsd;
1058            let e2 = sum2_lsd * inv_lsd;
1059            (e2 - e * e).max(0.0).sqrt()
1060        } else {
1061            f64::NAN
1062        };
1063
1064        let macz_raw = if i >= warm_m
1065            && sd_src.is_finite()
1066            && sd_src > 0.0
1067            && zvwap.is_finite()
1068            && macd.is_finite()
1069        {
1070            zvwap.mul_add(a, (macd / sd_src) * b)
1071        } else {
1072            f64::NAN
1073        };
1074
1075        let macz_val = if use_lag {
1076            if macz_raw.is_finite() {
1077                let one_minus_g = 1.0 - gamma;
1078                let new_l0 = macz_raw.mul_add(one_minus_g, gamma * l0);
1079                let new_l1 = (-gamma).mul_add(new_l0, l0 + gamma * l1);
1080                let new_l2 = (-gamma).mul_add(new_l1, l1 + gamma * l2);
1081                let new_l3 = (-gamma).mul_add(new_l2, l2 + gamma * l3);
1082                l0 = new_l0;
1083                l1 = new_l1;
1084                l2 = new_l2;
1085                l3 = new_l3;
1086                (l0 + 2.0 * l1 + 2.0 * l2 + l3) / 6.0
1087            } else {
1088                f64::NAN
1089            }
1090        } else {
1091            macz_raw
1092        };
1093
1094        if i >= warm_m {
1095            if sig_count == sig {
1096                let leaving = *sig_ring.get_unchecked(sig_head);
1097                if leaving.is_nan() {
1098                    if sig_nan > 0 {
1099                        sig_nan -= 1;
1100                    }
1101                } else {
1102                    sig_sum -= leaving;
1103                }
1104            } else {
1105                sig_count += 1;
1106            }
1107            *sig_ring.get_unchecked_mut(sig_head) = macz_val;
1108            if macz_val.is_nan() {
1109                sig_nan += 1;
1110            } else {
1111                sig_sum += macz_val;
1112            }
1113            sig_head += 1;
1114            if sig_head == sig {
1115                sig_head = 0;
1116            }
1117
1118            if i >= warm_hist {
1119                let signal = if sig_count == sig && sig_nan == 0 {
1120                    sig_sum * inv_sig
1121                } else {
1122                    f64::NAN
1123                };
1124                *out.get_unchecked_mut(i) = if macz_val.is_nan() || signal.is_nan() {
1125                    f64::NAN
1126                } else {
1127                    macz_val - signal
1128                };
1129            }
1130        }
1131    }
1132
1133    Ok(())
1134}
1135
1136#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1137#[target_feature(enable = "avx2,fma")]
1138pub unsafe fn macz_avx2(
1139    data: &[f64],
1140    params: &MaczParams,
1141    out: &mut [f64],
1142) -> Result<(), MaczError> {
1143    macz_scalar(data, params, out)
1144}
1145
1146#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1147#[target_feature(enable = "avx512f,fma")]
1148pub unsafe fn macz_avx512(
1149    data: &[f64],
1150    params: &MaczParams,
1151    out: &mut [f64],
1152) -> Result<(), MaczError> {
1153    macz_scalar(data, params, out)
1154}
1155
1156#[derive(Debug, Clone)]
1157pub struct MaczBatchRange {
1158    pub fast_length: (usize, usize, usize),
1159    pub slow_length: (usize, usize, usize),
1160    pub signal_length: (usize, usize, usize),
1161    pub lengthz: (usize, usize, usize),
1162    pub length_stdev: (usize, usize, usize),
1163    pub a: (f64, f64, f64),
1164    pub b: (f64, f64, f64),
1165}
1166
1167impl Default for MaczBatchRange {
1168    fn default() -> Self {
1169        Self {
1170            fast_length: (12, 12, 1),
1171            slow_length: (25, 25, 1),
1172            signal_length: (9, 9, 1),
1173            lengthz: (20, 20, 1),
1174            length_stdev: (25, 25, 1),
1175            a: (1.0, 1.249, 0.001),
1176            b: (1.0, 1.0, 0.0),
1177        }
1178    }
1179}
1180
1181pub struct MaczBatchOutput {
1182    pub values: Vec<f64>,
1183    pub combos: Vec<MaczParams>,
1184    pub rows: usize,
1185    pub cols: usize,
1186}
1187
1188impl MaczBatchOutput {
1189    pub fn row_for_params(&self, p: &MaczParams) -> Option<usize> {
1190        self.combos.iter().position(|c| {
1191            c.fast_length == p.fast_length
1192                && c.slow_length == p.slow_length
1193                && c.signal_length == p.signal_length
1194                && c.lengthz == p.lengthz
1195                && c.length_stdev == p.length_stdev
1196                && (c.a.unwrap_or(1.0) - p.a.unwrap_or(1.0)).abs() < 1e-12
1197                && (c.b.unwrap_or(1.0) - p.b.unwrap_or(1.0)).abs() < 1e-12
1198        })
1199    }
1200
1201    pub fn values_for(&self, params: &MaczParams) -> Option<&[f64]> {
1202        self.row_for_params(params).map(|idx| {
1203            let start = idx * self.cols;
1204            let end = start + self.cols;
1205            &self.values[start..end]
1206        })
1207    }
1208
1209    pub fn matrix(&self) -> Vec<Vec<f64>> {
1210        self.values
1211            .chunks(self.cols)
1212            .map(|row| row.to_vec())
1213            .collect()
1214    }
1215}
1216
1217#[derive(Debug, Clone)]
1218pub struct MaczBatchBuilder {
1219    range: MaczBatchRange,
1220    kernel: Kernel,
1221}
1222
1223impl Default for MaczBatchBuilder {
1224    fn default() -> Self {
1225        Self {
1226            range: MaczBatchRange::default(),
1227            kernel: Kernel::Auto,
1228        }
1229    }
1230}
1231
1232impl MaczBatchBuilder {
1233    pub fn new() -> Self {
1234        Self::default()
1235    }
1236
1237    pub fn kernel(mut self, k: Kernel) -> Self {
1238        self.kernel = k;
1239        self
1240    }
1241
1242    pub fn fast_range(mut self, start: usize, end: usize, step: usize) -> Self {
1243        self.range.fast_length = (start, end, step);
1244        self
1245    }
1246
1247    pub fn slow_range(mut self, start: usize, end: usize, step: usize) -> Self {
1248        self.range.slow_length = (start, end, step);
1249        self
1250    }
1251
1252    pub fn signal_range(mut self, start: usize, end: usize, step: usize) -> Self {
1253        self.range.signal_length = (start, end, step);
1254        self
1255    }
1256
1257    pub fn lengthz_range(mut self, start: usize, end: usize, step: usize) -> Self {
1258        self.range.lengthz = (start, end, step);
1259        self
1260    }
1261
1262    pub fn length_stdev_range(mut self, start: usize, end: usize, step: usize) -> Self {
1263        self.range.length_stdev = (start, end, step);
1264        self
1265    }
1266
1267    pub fn a_range(mut self, start: f64, end: f64, step: f64) -> Self {
1268        self.range.a = (start, end, step);
1269        self
1270    }
1271
1272    pub fn b_range(mut self, start: f64, end: f64, step: f64) -> Self {
1273        self.range.b = (start, end, step);
1274        self
1275    }
1276
1277    pub fn fast_static(mut self, f: usize) -> Self {
1278        self.range.fast_length = (f, f, 0);
1279        self
1280    }
1281
1282    pub fn slow_static(mut self, s: usize) -> Self {
1283        self.range.slow_length = (s, s, 0);
1284        self
1285    }
1286
1287    pub fn signal_static(mut self, sig: usize) -> Self {
1288        self.range.signal_length = (sig, sig, 0);
1289        self
1290    }
1291
1292    pub fn lengthz_static(mut self, lz: usize) -> Self {
1293        self.range.lengthz = (lz, lz, 0);
1294        self
1295    }
1296
1297    pub fn length_stdev_static(mut self, lsd: usize) -> Self {
1298        self.range.length_stdev = (lsd, lsd, 0);
1299        self
1300    }
1301
1302    pub fn a_static(mut self, a: f64) -> Self {
1303        self.range.a = (a, a, 0.0);
1304        self
1305    }
1306
1307    pub fn b_static(mut self, b: f64) -> Self {
1308        self.range.b = (b, b, 0.0);
1309        self
1310    }
1311
1312    pub fn apply_slice(self, data: &[f64]) -> Result<MaczBatchOutput, MaczError> {
1313        macz_batch_with_kernel(data, &self.range, self.kernel)
1314    }
1315
1316    pub fn with_default_slice(data: &[f64], k: Kernel) -> Result<MaczBatchOutput, MaczError> {
1317        MaczBatchBuilder::new().kernel(k).apply_slice(data)
1318    }
1319
1320    pub fn apply_candles(
1321        self,
1322        candles: &Candles,
1323        source: &str,
1324    ) -> Result<MaczBatchOutput, MaczError> {
1325        let data = source_type(candles, source);
1326        macz_batch_with_kernel_vol(data, Some(&candles.volume), &self.range, self.kernel)
1327    }
1328
1329    pub fn with_default_candles(c: &Candles) -> Result<MaczBatchOutput, MaczError> {
1330        MaczBatchBuilder::new()
1331            .kernel(Kernel::Auto)
1332            .apply_candles(c, "close")
1333    }
1334}
1335
1336fn expand_grid_macz(r: &MaczBatchRange) -> Result<Vec<MaczParams>, MaczError> {
1337    fn axis_usize((start, end, step): (usize, usize, usize)) -> Result<Vec<usize>, MaczError> {
1338        if step == 0 || start == end {
1339            return Ok(vec![start]);
1340        }
1341        if start < end {
1342            return Ok((start..=end).step_by(step.max(1)).collect());
1343        }
1344        let mut v = Vec::new();
1345        let mut x = start as isize;
1346        let end_i = end as isize;
1347        let st = (step as isize).max(1);
1348        while x >= end_i {
1349            v.push(x as usize);
1350            x -= st;
1351        }
1352        if v.is_empty() {
1353            return Err(MaczError::InvalidRange {
1354                start: start.to_string(),
1355                end: end.to_string(),
1356                step: step.to_string(),
1357            });
1358        }
1359        Ok(v)
1360    }
1361    fn axis_f64((start, end, step): (f64, f64, f64)) -> Result<Vec<f64>, MaczError> {
1362        if step.abs() < 1e-12 || (start - end).abs() < 1e-12 {
1363            return Ok(vec![start]);
1364        }
1365        if start < end {
1366            let mut v = Vec::new();
1367            let mut x = start;
1368            let st = step.abs();
1369            while x <= end + 1e-12 {
1370                v.push(x);
1371                x += st;
1372            }
1373            if v.is_empty() {
1374                return Err(MaczError::InvalidRange {
1375                    start: start.to_string(),
1376                    end: end.to_string(),
1377                    step: step.to_string(),
1378                });
1379            }
1380            return Ok(v);
1381        }
1382        let mut v = Vec::new();
1383        let mut x = start;
1384        let st = step.abs();
1385        while x + 1e-12 >= end {
1386            v.push(x);
1387            x -= st;
1388        }
1389        if v.is_empty() {
1390            return Err(MaczError::InvalidRange {
1391                start: start.to_string(),
1392                end: end.to_string(),
1393                step: step.to_string(),
1394            });
1395        }
1396        Ok(v)
1397    }
1398    let fs = axis_usize(r.fast_length)?;
1399    let ss = axis_usize(r.slow_length)?;
1400    let gs = axis_usize(r.signal_length)?;
1401    let zs = axis_usize(r.lengthz)?;
1402    let ds = axis_usize(r.length_stdev)?;
1403    let as_ = axis_f64(r.a)?;
1404    let bs = axis_f64(r.b)?;
1405
1406    let cap = fs
1407        .len()
1408        .checked_mul(ss.len())
1409        .and_then(|v| v.checked_mul(gs.len()))
1410        .and_then(|v| v.checked_mul(zs.len()))
1411        .and_then(|v| v.checked_mul(ds.len()))
1412        .and_then(|v| v.checked_mul(as_.len()))
1413        .and_then(|v| v.checked_mul(bs.len()))
1414        .ok_or_else(|| MaczError::InvalidRange {
1415            start: "cap".into(),
1416            end: "overflow".into(),
1417            step: "mul".into(),
1418        })?;
1419
1420    let mut out = Vec::with_capacity(cap);
1421    for &f in &fs {
1422        for &s in &ss {
1423            for &g in &gs {
1424                for &z in &zs {
1425                    for &d in &ds {
1426                        for &a in &as_ {
1427                            for &b in &bs {
1428                                out.push(MaczParams {
1429                                    fast_length: Some(f),
1430                                    slow_length: Some(s),
1431                                    signal_length: Some(g),
1432                                    lengthz: Some(z),
1433                                    length_stdev: Some(d),
1434                                    a: Some(a),
1435                                    b: Some(b),
1436                                    use_lag: Some(false),
1437                                    gamma: Some(0.02),
1438                                });
1439                            }
1440                        }
1441                    }
1442                }
1443            }
1444        }
1445    }
1446    Ok(out)
1447}
1448
1449pub fn macz_batch_with_kernel(
1450    data: &[f64],
1451    sweep: &MaczBatchRange,
1452    k: Kernel,
1453) -> Result<MaczBatchOutput, MaczError> {
1454    macz_batch_with_kernel_vol(data, None, sweep, k)
1455}
1456
1457pub fn macz_batch_with_kernel_vol(
1458    data: &[f64],
1459    volume: Option<&[f64]>,
1460    sweep: &MaczBatchRange,
1461    k: Kernel,
1462) -> Result<MaczBatchOutput, MaczError> {
1463    let kernel = match k {
1464        Kernel::Auto => Kernel::ScalarBatch,
1465        other if other.is_batch() => other,
1466        _ => {
1467            return Err(MaczError::InvalidKernelForBatch(k));
1468        }
1469    };
1470    macz_batch_par_slice_vol(data, volume, sweep, kernel)
1471}
1472
1473pub fn macz_batch_slice(
1474    data: &[f64],
1475    sweep: &MaczBatchRange,
1476    kern: Kernel,
1477) -> Result<MaczBatchOutput, MaczError> {
1478    macz_batch_inner_vol(data, None, sweep, kern, false)
1479}
1480
1481pub fn macz_batch_par_slice(
1482    data: &[f64],
1483    sweep: &MaczBatchRange,
1484    kern: Kernel,
1485) -> Result<MaczBatchOutput, MaczError> {
1486    macz_batch_inner_vol(data, None, sweep, kern, true)
1487}
1488
1489pub fn macz_batch_slice_vol(
1490    data: &[f64],
1491    volume: Option<&[f64]>,
1492    sweep: &MaczBatchRange,
1493    kern: Kernel,
1494) -> Result<MaczBatchOutput, MaczError> {
1495    macz_batch_inner_vol(data, volume, sweep, kern, false)
1496}
1497
1498pub fn macz_batch_par_slice_vol(
1499    data: &[f64],
1500    volume: Option<&[f64]>,
1501    sweep: &MaczBatchRange,
1502    kern: Kernel,
1503) -> Result<MaczBatchOutput, MaczError> {
1504    macz_batch_inner_vol(data, volume, sweep, kern, true)
1505}
1506
1507fn macz_batch_inner_vol(
1508    data: &[f64],
1509    volume: Option<&[f64]>,
1510    sweep: &MaczBatchRange,
1511    kern: Kernel,
1512    parallel: bool,
1513) -> Result<MaczBatchOutput, MaczError> {
1514    let combos = expand_grid_macz(sweep)?;
1515    let rows = combos.len();
1516    let cols = data.len();
1517    if cols == 0 {
1518        return Err(MaczError::EmptyInputData);
1519    }
1520
1521    if let Some(v) = volume {
1522        if v.len() != cols {
1523            return Err(MaczError::InvalidParameter {
1524                msg: "data and volume length mismatch".into(),
1525            });
1526        }
1527    }
1528
1529    let _ = rows
1530        .checked_mul(cols)
1531        .ok_or_else(|| MaczError::InvalidRange {
1532            start: rows.to_string(),
1533            end: cols.to_string(),
1534            step: "rows*cols".into(),
1535        })?;
1536
1537    let mut buf_mu = make_uninit_matrix(rows, cols);
1538
1539    let first = data
1540        .iter()
1541        .position(|x| !x.is_nan())
1542        .ok_or(MaczError::AllValuesNaN)?;
1543    let warms: Vec<usize> = combos
1544        .iter()
1545        .map(|p| {
1546            let slow = p.slow_length.unwrap_or(25);
1547            let lz = p.lengthz.unwrap_or(20);
1548            let lsd = p.length_stdev.unwrap_or(25);
1549            let sig = p.signal_length.unwrap_or(9);
1550            macz_warm_len(first, slow, lz, lsd, sig)
1551        })
1552        .collect();
1553    init_matrix_prefixes(&mut buf_mu, cols, &warms);
1554
1555    let mut guard = core::mem::ManuallyDrop::new(buf_mu);
1556    let out: &mut [f64] =
1557        unsafe { core::slice::from_raw_parts_mut(guard.as_mut_ptr() as *mut f64, guard.len()) };
1558
1559    let row_kernel = match kern {
1560        Kernel::Avx512Batch => Kernel::Avx512,
1561        Kernel::Avx2Batch => Kernel::Avx2,
1562        Kernel::ScalarBatch => Kernel::Scalar,
1563        _ => kern,
1564    };
1565
1566    let fill_row = |row: usize, dst_row: &mut [f64]| -> Result<(), MaczError> {
1567        let params = combos[row].clone();
1568        let input = if let Some(v) = volume {
1569            MaczInput::from_slice_with_volume(data, v, params)
1570        } else {
1571            MaczInput::from_slice(data, params)
1572        };
1573
1574        macz_into_slice(dst_row, &input, row_kernel)
1575    };
1576
1577    if parallel {
1578        #[cfg(not(target_arch = "wasm32"))]
1579        {
1580            use std::sync::Mutex;
1581            let error: Mutex<Option<MaczError>> = Mutex::new(None);
1582            out.par_chunks_mut(cols).enumerate().for_each(|(r, slice)| {
1583                if let Err(e) = fill_row(r, slice) {
1584                    *error.lock().unwrap() = Some(e);
1585                }
1586            });
1587            if let Some(e) = error.into_inner().unwrap() {
1588                return Err(e);
1589            }
1590        }
1591        #[cfg(target_arch = "wasm32")]
1592        for (r, slice) in out.chunks_mut(cols).enumerate() {
1593            fill_row(r, slice)?;
1594        }
1595    } else {
1596        for (r, slice) in out.chunks_mut(cols).enumerate() {
1597            fill_row(r, slice)?;
1598        }
1599    }
1600
1601    let values = unsafe {
1602        Vec::from_raw_parts(
1603            guard.as_mut_ptr() as *mut f64,
1604            guard.len(),
1605            guard.capacity(),
1606        )
1607    };
1608    core::mem::forget(guard);
1609    Ok(MaczBatchOutput {
1610        values,
1611        combos,
1612        rows,
1613        cols,
1614    })
1615}
1616
1617fn macz_batch_inner_into(
1618    data: &[f64],
1619    sweep: &MaczBatchRange,
1620    kern: Kernel,
1621    parallel: bool,
1622    out_flat: &mut [f64],
1623) -> Result<Vec<MaczParams>, MaczError> {
1624    macz_batch_inner_into_vol(data, None, sweep, kern, parallel, out_flat)
1625}
1626
1627fn macz_batch_inner_into_vol(
1628    data: &[f64],
1629    volume: Option<&[f64]>,
1630    sweep: &MaczBatchRange,
1631    kern: Kernel,
1632    parallel: bool,
1633    out_flat: &mut [f64],
1634) -> Result<Vec<MaczParams>, MaczError> {
1635    let combos = expand_grid_macz(sweep)?;
1636    let rows = combos.len();
1637    let cols = data.len();
1638    let expected = rows
1639        .checked_mul(cols)
1640        .ok_or_else(|| MaczError::InvalidRange {
1641            start: rows.to_string(),
1642            end: cols.to_string(),
1643            step: "rows*cols".into(),
1644        })?;
1645    if out_flat.len() != expected {
1646        return Err(MaczError::OutputLengthMismatch {
1647            expected,
1648            got: out_flat.len(),
1649        });
1650    }
1651    if let Some(v) = volume {
1652        if v.len() != cols {
1653            return Err(MaczError::InvalidParameter {
1654                msg: "data and volume length mismatch".into(),
1655            });
1656        }
1657    }
1658
1659    let row_kernel = match kern {
1660        Kernel::Avx512Batch => Kernel::Avx512,
1661        Kernel::Avx2Batch => Kernel::Avx2,
1662        Kernel::ScalarBatch => Kernel::Scalar,
1663        _ => kern,
1664    };
1665
1666    let write_row = |row: usize, dst: &mut [f64]| -> Result<(), MaczError> {
1667        let params = combos[row].clone();
1668        let input = if let Some(v) = volume {
1669            MaczInput::from_slice_with_volume(data, v, params)
1670        } else {
1671            MaczInput::from_slice(data, params)
1672        };
1673        macz_into_slice(dst, &input, row_kernel)
1674    };
1675
1676    if parallel {
1677        #[cfg(not(target_arch = "wasm32"))]
1678        {
1679            use std::sync::Mutex;
1680            let error: Mutex<Option<MaczError>> = Mutex::new(None);
1681            out_flat
1682                .par_chunks_mut(cols)
1683                .enumerate()
1684                .for_each(|(r, s)| {
1685                    if let Err(e) = write_row(r, s) {
1686                        *error.lock().unwrap() = Some(e);
1687                    }
1688                });
1689            if let Some(e) = error.into_inner().unwrap() {
1690                return Err(e);
1691            }
1692        }
1693        #[cfg(target_arch = "wasm32")]
1694        for (r, s) in out_flat.chunks_mut(cols).enumerate() {
1695            write_row(r, s)?;
1696        }
1697    } else {
1698        for (r, s) in out_flat.chunks_mut(cols).enumerate() {
1699            write_row(r, s)?;
1700        }
1701    }
1702    Ok(combos)
1703}
1704
1705#[derive(Debug, Clone)]
1706pub struct MaczStream {
1707    params: MaczParams,
1708
1709    price_buffer: Vec<f64>,
1710    volume_buffer: Vec<f64>,
1711    buffer_size: usize,
1712
1713    index: usize,
1714
1715    head: usize,
1716
1717    filled: bool,
1718
1719    fast_sum: f64,
1720    slow_sum: f64,
1721
1722    sum_lz: f64,
1723    sum2_lz: f64,
1724
1725    vwap_pv_sum: f64,
1726    vwap_v_sum: f64,
1727
1728    vwap_bad: usize,
1729
1730    stdev_sum: f64,
1731    stdev_sum2: f64,
1732
1733    signal_sum: f64,
1734    signal_buffer: Vec<f64>,
1735
1736    sig_head: usize,
1737    sig_count: usize,
1738    sig_nan: usize,
1739
1740    l0: f64,
1741    l1: f64,
1742    l2: f64,
1743    l3: f64,
1744    use_lag: bool,
1745    gamma: f64,
1746
1747    current_vwap: f64,
1748    current_zvwap: f64,
1749    current_fast_ma: f64,
1750    current_slow_ma: f64,
1751    current_macd: f64,
1752    current_stdev: f64,
1753    current_macz: f64,
1754    current_signal: f64,
1755
1756    fast: usize,
1757    slow: usize,
1758    lz: usize,
1759    lsd: usize,
1760    sig: usize,
1761
1762    a: f64,
1763    b: f64,
1764
1765    inv_fast: f64,
1766    inv_slow: f64,
1767    inv_lz: f64,
1768    inv_lsd: f64,
1769    inv_sig: f64,
1770
1771    warm_m: usize,
1772    warm_hist: usize,
1773
1774    off_fast: usize,
1775    off_slow: usize,
1776    off_lz: usize,
1777    off_lsd: usize,
1778}
1779
1780impl MaczStream {
1781    pub fn try_new(params: MaczParams) -> Result<Self, MaczError> {
1782        let fast = params.fast_length.unwrap_or(12);
1783        let slow = params.slow_length.unwrap_or(25);
1784        let lz = params.lengthz.unwrap_or(20);
1785        let lsd = params.length_stdev.unwrap_or(25);
1786        let sig = params.signal_length.unwrap_or(9);
1787        let use_lag = params.use_lag.unwrap_or(false);
1788        let gamma = params.gamma.unwrap_or(0.02);
1789
1790        if fast == 0 || slow == 0 || lz == 0 || lsd == 0 || sig == 0 {
1791            return Err(MaczError::InvalidParameter {
1792                msg: "periods must be > 0".into(),
1793            });
1794        }
1795        if !(0.0..1.0).contains(&gamma) {
1796            return Err(MaczError::InvalidGamma { gamma });
1797        }
1798        let a = params.a.unwrap_or(1.0);
1799        let b = params.b.unwrap_or(1.0);
1800        if !(-2.0..=2.0).contains(&a) {
1801            return Err(MaczError::InvalidA { a });
1802        }
1803        if !(-2.0..=2.0).contains(&b) {
1804            return Err(MaczError::InvalidB { b });
1805        }
1806
1807        let buffer_size = fast.max(slow).max(lz).max(lsd);
1808
1809        let warm_m = slow.max(lz).max(lsd);
1810        let warm_hist = warm_m + sig - 1;
1811
1812        let inv_fast = 1.0 / (fast as f64);
1813        let inv_slow = 1.0 / (slow as f64);
1814        let inv_lz = 1.0 / (lz as f64);
1815        let inv_lsd = 1.0 / (lsd as f64);
1816        let inv_sig = 1.0 / (sig as f64);
1817
1818        let off = |p: usize| buffer_size - (p % buffer_size);
1819
1820        Ok(Self {
1821            params,
1822            price_buffer: vec![f64::NAN; buffer_size],
1823            volume_buffer: vec![1.0; buffer_size],
1824            buffer_size,
1825            index: 0,
1826            head: 0,
1827            filled: false,
1828
1829            fast_sum: 0.0,
1830            slow_sum: 0.0,
1831
1832            sum_lz: 0.0,
1833            sum2_lz: 0.0,
1834
1835            vwap_pv_sum: 0.0,
1836            vwap_v_sum: 0.0,
1837            vwap_bad: 0,
1838
1839            stdev_sum: 0.0,
1840            stdev_sum2: 0.0,
1841
1842            signal_sum: 0.0,
1843            signal_buffer: vec![f64::NAN; sig],
1844            sig_head: 0,
1845            sig_count: 0,
1846            sig_nan: 0,
1847
1848            l0: 0.0,
1849            l1: 0.0,
1850            l2: 0.0,
1851            l3: 0.0,
1852            use_lag,
1853            gamma,
1854
1855            current_vwap: f64::NAN,
1856            current_zvwap: f64::NAN,
1857            current_fast_ma: f64::NAN,
1858            current_slow_ma: f64::NAN,
1859            current_macd: f64::NAN,
1860            current_stdev: f64::NAN,
1861            current_macz: f64::NAN,
1862            current_signal: f64::NAN,
1863
1864            fast,
1865            slow,
1866            lz,
1867            lsd,
1868            sig,
1869            a,
1870            b,
1871            inv_fast,
1872            inv_slow,
1873            inv_lz,
1874            inv_lsd,
1875            inv_sig,
1876            warm_m,
1877            warm_hist,
1878
1879            off_fast: off(fast),
1880            off_slow: off(slow),
1881            off_lz: off(lz),
1882            off_lsd: off(lsd),
1883        })
1884    }
1885
1886    pub fn new(params: MaczParams) -> Result<Self, MaczError> {
1887        Self::try_new(params)
1888    }
1889
1890    #[inline(always)]
1891    pub fn update(&mut self, value: f64, volume: Option<f64>) -> Option<f64> {
1892        if !value.is_finite() {
1893            return None;
1894        }
1895        let vol = volume.unwrap_or(1.0);
1896        let vol_ok = vol.is_finite() && vol > 0.0;
1897
1898        let bsz = self.buffer_size;
1899        let idx = self.head;
1900
1901        #[inline(always)]
1902        fn add_off(i: usize, off: usize, n: usize) -> usize {
1903            let j = i + off;
1904            if j >= n {
1905                j - n
1906            } else {
1907                j
1908            }
1909        }
1910
1911        let leaving_fast_idx = add_off(idx, self.off_fast, bsz);
1912        let leaving_slow_idx = add_off(idx, self.off_slow, bsz);
1913        let leaving_lz_idx = add_off(idx, self.off_lz, bsz);
1914        let leaving_lsd_idx = add_off(idx, self.off_lsd, bsz);
1915
1916        let exiting_fast = if self.index >= self.fast {
1917            self.price_buffer[leaving_fast_idx]
1918        } else {
1919            0.0
1920        };
1921        let exiting_slow = if self.index >= self.slow {
1922            self.price_buffer[leaving_slow_idx]
1923        } else {
1924            0.0
1925        };
1926        let exiting_lz = if self.index >= self.lz {
1927            self.price_buffer[leaving_lz_idx]
1928        } else {
1929            0.0
1930        };
1931        let exiting_lsd = if self.index >= self.lsd {
1932            self.price_buffer[leaving_lsd_idx]
1933        } else {
1934            0.0
1935        };
1936
1937        let exiting_vwap_price = exiting_lz;
1938        let exiting_vwap_vol = if self.index >= self.lz {
1939            self.volume_buffer[leaving_lz_idx]
1940        } else {
1941            0.0
1942        };
1943        let leaving_vol_ok = exiting_vwap_vol.is_finite() && exiting_vwap_vol > 0.0;
1944
1945        self.price_buffer[idx] = value;
1946        self.volume_buffer[idx] = vol;
1947
1948        self.fast_sum += value - exiting_fast;
1949        self.slow_sum += value - exiting_slow;
1950
1951        self.sum_lz += value - exiting_lz;
1952        self.sum2_lz += value.mul_add(value, -exiting_lz * exiting_lz);
1953
1954        self.vwap_pv_sum += value * vol - exiting_vwap_price * exiting_vwap_vol;
1955        self.vwap_v_sum += vol - exiting_vwap_vol;
1956
1957        if !vol_ok {
1958            self.vwap_bad += 1;
1959        }
1960        if self.index >= self.lz && !leaving_vol_ok && self.vwap_bad > 0 {
1961            self.vwap_bad -= 1;
1962        }
1963
1964        self.stdev_sum += value - exiting_lsd;
1965        self.stdev_sum2 += value.mul_add(value, -exiting_lsd * exiting_lsd);
1966
1967        let i_next = self.index + 1;
1968
1969        if i_next < self.warm_m {
1970            self.index = i_next;
1971            self.head = if idx + 1 == bsz { 0 } else { idx + 1 };
1972            self.filled |= self.index >= bsz;
1973            return None;
1974        }
1975
1976        self.current_fast_ma = self.fast_sum * self.inv_fast;
1977        self.current_slow_ma = self.slow_sum * self.inv_slow;
1978        self.current_macd = self.current_fast_ma - self.current_slow_ma;
1979
1980        if self.vwap_bad == 0 && self.vwap_v_sum > 0.0 {
1981            self.current_vwap = self.vwap_pv_sum / self.vwap_v_sum;
1982
1983            let e = self.sum_lz * self.inv_lz;
1984            let e2 = self.sum2_lz * self.inv_lz;
1985            let var =
1986                (-2.0 * self.current_vwap).mul_add(e, e2) + self.current_vwap * self.current_vwap;
1987            let sd = var.max(0.0).sqrt();
1988            self.current_zvwap = if sd > 0.0 {
1989                (value - self.current_vwap) / sd
1990            } else {
1991                0.0
1992            };
1993        } else {
1994            self.current_vwap = f64::NAN;
1995            self.current_zvwap = f64::NAN;
1996        }
1997
1998        let mean_lsd = self.stdev_sum * self.inv_lsd;
1999        let var_lsd = self.stdev_sum2 * self.inv_lsd - mean_lsd * mean_lsd;
2000        self.current_stdev = var_lsd.max(0.0).sqrt();
2001
2002        let macz_raw = if self.current_stdev.is_finite()
2003            && self.current_stdev > 0.0
2004            && self.current_zvwap.is_finite()
2005            && self.current_macd.is_finite()
2006        {
2007            self.current_zvwap
2008                .mul_add(self.a, (self.current_macd / self.current_stdev) * self.b)
2009        } else {
2010            f64::NAN
2011        };
2012
2013        let macz_val = if self.use_lag && macz_raw.is_finite() {
2014            let one_minus_g = 1.0 - self.gamma;
2015            let new_l0 = macz_raw.mul_add(one_minus_g, self.gamma * self.l0);
2016            let new_l1 = (-self.gamma).mul_add(new_l0, self.l0 + self.gamma * self.l1);
2017            let new_l2 = (-self.gamma).mul_add(new_l1, self.l1 + self.gamma * self.l2);
2018            let new_l3 = (-self.gamma).mul_add(new_l2, self.l2 + self.gamma * self.l3);
2019            self.l0 = new_l0;
2020            self.l1 = new_l1;
2021            self.l2 = new_l2;
2022            self.l3 = new_l3;
2023            (self.l0 + 2.0 * self.l1 + 2.0 * self.l2 + self.l3) / 6.0
2024        } else {
2025            macz_raw
2026        };
2027
2028        if i_next >= self.warm_m {
2029            if self.sig_count == self.sig {
2030                let leaving = self.signal_buffer[self.sig_head];
2031                if leaving.is_nan() {
2032                    if self.sig_nan > 0 {
2033                        self.sig_nan -= 1;
2034                    }
2035                } else {
2036                    self.signal_sum -= leaving;
2037                }
2038            } else {
2039                self.sig_count += 1;
2040            }
2041            self.signal_buffer[self.sig_head] = macz_val;
2042            if macz_val.is_nan() {
2043                self.sig_nan += 1;
2044            } else {
2045                self.signal_sum += macz_val;
2046            }
2047            self.sig_head += 1;
2048            if self.sig_head == self.sig {
2049                self.sig_head = 0;
2050            }
2051        }
2052
2053        self.index = i_next;
2054        self.head = if idx + 1 == bsz { 0 } else { idx + 1 };
2055        self.filled |= self.index >= bsz;
2056
2057        if self.index <= self.warm_hist {
2058            return None;
2059        }
2060
2061        if self.sig_count == self.sig && self.sig_nan == 0 && macz_val.is_finite() {
2062            self.current_macz = macz_val;
2063            self.current_signal = self.signal_sum * self.inv_sig;
2064            Some(self.current_macz - self.current_signal)
2065        } else {
2066            Some(f64::NAN)
2067        }
2068    }
2069}
2070
2071#[cfg(feature = "python")]
2072#[pyfunction(name = "macz")]
2073#[pyo3(signature = (data, volume=None, fast_length=12, slow_length=25, signal_length=9, lengthz=20, length_stdev=25, a=1.0, b=1.0, use_lag=false, gamma=0.02, kernel=None))]
2074pub fn macz_py<'py>(
2075    py: Python<'py>,
2076    data: PyReadonlyArray1<'py, f64>,
2077    volume: Option<PyReadonlyArray1<'py, f64>>,
2078    fast_length: usize,
2079    slow_length: usize,
2080    signal_length: usize,
2081    lengthz: usize,
2082    length_stdev: usize,
2083    a: f64,
2084    b: f64,
2085    use_lag: bool,
2086    gamma: f64,
2087    kernel: Option<&str>,
2088) -> PyResult<Bound<'py, PyArray1<f64>>> {
2089    let slice_in = data.as_slice()?;
2090    let kern = validate_kernel(kernel, false)?;
2091
2092    let params = MaczParams {
2093        fast_length: Some(fast_length),
2094        slow_length: Some(slow_length),
2095        signal_length: Some(signal_length),
2096        lengthz: Some(lengthz),
2097        length_stdev: Some(length_stdev),
2098        a: Some(a),
2099        b: Some(b),
2100        use_lag: Some(use_lag),
2101        gamma: Some(gamma),
2102    };
2103
2104    let result_vec: Vec<f64> = if let Some(vol) = volume {
2105        let v = vol.as_slice()?;
2106        let input = MaczInput::from_slice_with_volume(slice_in, v, params);
2107        py.allow_threads(|| macz_with_kernel(&input, kern).map(|o| o.values))
2108            .map_err(|e| PyValueError::new_err(e.to_string()))?
2109    } else {
2110        let input = MaczInput::from_slice(slice_in, params);
2111        py.allow_threads(|| macz_with_kernel(&input, kern).map(|o| o.values))
2112            .map_err(|e| PyValueError::new_err(e.to_string()))?
2113    };
2114
2115    Ok(result_vec.into_pyarray(py))
2116}
2117
2118#[cfg(all(feature = "python", feature = "cuda"))]
2119use crate::cuda::moving_averages::CudaMacz;
2120#[cfg(all(feature = "python", feature = "cuda"))]
2121use crate::indicators::moving_averages::alma::DeviceArrayF32Py;
2122
2123#[cfg(all(feature = "python", feature = "cuda"))]
2124#[pyfunction(name = "macz_cuda_batch_dev")]
2125#[pyo3(signature = (data_f32, volume_f32=None, fast_length_range=(12,12,0), slow_length_range=(25,25,0), signal_length_range=(9,9,0), lengthz_range=(20,20,0), length_stdev_range=(25,25,0), a_range=(1.0,1.0,0.0), b_range=(1.0,1.0,0.0), device_id=0))]
2126pub fn macz_cuda_batch_dev_py<'py>(
2127    py: Python<'py>,
2128    data_f32: numpy::PyReadonlyArray1<'py, f32>,
2129    volume_f32: Option<numpy::PyReadonlyArray1<'py, f32>>,
2130    fast_length_range: (usize, usize, usize),
2131    slow_length_range: (usize, usize, usize),
2132    signal_length_range: (usize, usize, usize),
2133    lengthz_range: (usize, usize, usize),
2134    length_stdev_range: (usize, usize, usize),
2135    a_range: (f64, f64, f64),
2136    b_range: (f64, f64, f64),
2137    device_id: usize,
2138) -> PyResult<(DeviceArrayF32Py, Bound<'py, PyDict>)> {
2139    use crate::cuda::cuda_available;
2140    if !cuda_available() {
2141        return Err(PyValueError::new_err("CUDA not available"));
2142    }
2143
2144    let price = data_f32.as_slice()?;
2145    let volume_opt: Option<&[f32]> = volume_f32.as_ref().map(|v| v.as_slice()).transpose()?;
2146    let sweep = MaczBatchRange {
2147        fast_length: fast_length_range,
2148        slow_length: slow_length_range,
2149        signal_length: signal_length_range,
2150        lengthz: lengthz_range,
2151        length_stdev: length_stdev_range,
2152        a: a_range,
2153        b: b_range,
2154    };
2155
2156    let ((inner, inner_ctx, inner_dev_id), combos) = py.allow_threads(|| {
2157        let cuda = CudaMacz::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
2158        let ctx = cuda.context_arc();
2159        let dev_id = cuda.device_id();
2160        cuda.macz_batch_dev(price, volume_opt, &sweep)
2161            .map(|(inner, combos)| ((inner, ctx, dev_id), combos))
2162            .map_err(|e| PyValueError::new_err(e.to_string()))
2163    })?;
2164
2165    let dict = PyDict::new(py);
2166    dict.set_item(
2167        "fast_lengths",
2168        combos
2169            .iter()
2170            .map(|p| p.fast_length.unwrap() as u64)
2171            .collect::<Vec<_>>()
2172            .into_pyarray(py),
2173    )?;
2174    dict.set_item(
2175        "slow_lengths",
2176        combos
2177            .iter()
2178            .map(|p| p.slow_length.unwrap() as u64)
2179            .collect::<Vec<_>>()
2180            .into_pyarray(py),
2181    )?;
2182    dict.set_item(
2183        "signal_lengths",
2184        combos
2185            .iter()
2186            .map(|p| p.signal_length.unwrap() as u64)
2187            .collect::<Vec<_>>()
2188            .into_pyarray(py),
2189    )?;
2190    dict.set_item(
2191        "lengthz",
2192        combos
2193            .iter()
2194            .map(|p| p.lengthz.unwrap() as u64)
2195            .collect::<Vec<_>>()
2196            .into_pyarray(py),
2197    )?;
2198    dict.set_item(
2199        "length_stdev",
2200        combos
2201            .iter()
2202            .map(|p| p.length_stdev.unwrap() as u64)
2203            .collect::<Vec<_>>()
2204            .into_pyarray(py),
2205    )?;
2206    dict.set_item(
2207        "a",
2208        combos
2209            .iter()
2210            .map(|p| p.a.unwrap_or(1.0))
2211            .collect::<Vec<_>>()
2212            .into_pyarray(py),
2213    )?;
2214    dict.set_item(
2215        "b",
2216        combos
2217            .iter()
2218            .map(|p| p.b.unwrap_or(1.0))
2219            .collect::<Vec<_>>()
2220            .into_pyarray(py),
2221    )?;
2222
2223    Ok((
2224        DeviceArrayF32Py {
2225            inner,
2226            _ctx: Some(inner_ctx),
2227            device_id: Some(inner_dev_id),
2228        },
2229        dict,
2230    ))
2231}
2232
2233#[cfg(all(feature = "python", feature = "cuda"))]
2234#[pyfunction(name = "macz_cuda_many_series_one_param_dev")]
2235#[pyo3(signature = (close_tm_f32, volume_tm_f32, cols, rows, fast_length=12, slow_length=25, signal_length=9, lengthz=20, length_stdev=25, a=1.0, b=1.0, use_lag=false, gamma=0.02, device_id=0))]
2236pub fn macz_cuda_many_series_one_param_dev_py<'py>(
2237    py: Python<'py>,
2238    close_tm_f32: numpy::PyReadonlyArray1<'py, f32>,
2239    volume_tm_f32: Option<numpy::PyReadonlyArray1<'py, f32>>,
2240    cols: usize,
2241    rows: usize,
2242    fast_length: usize,
2243    slow_length: usize,
2244    signal_length: usize,
2245    lengthz: usize,
2246    length_stdev: usize,
2247    a: f64,
2248    b: f64,
2249    use_lag: bool,
2250    gamma: f64,
2251    device_id: usize,
2252) -> PyResult<DeviceArrayF32Py> {
2253    use crate::cuda::cuda_available;
2254    if !cuda_available() {
2255        return Err(PyValueError::new_err("CUDA not available"));
2256    }
2257    let price_tm = close_tm_f32.as_slice()?;
2258    let vol_tm_opt: Option<&[f32]> = volume_tm_f32.as_ref().map(|v| v.as_slice()).transpose()?;
2259    let params = MaczParams {
2260        fast_length: Some(fast_length),
2261        slow_length: Some(slow_length),
2262        signal_length: Some(signal_length),
2263        lengthz: Some(lengthz),
2264        length_stdev: Some(length_stdev),
2265        a: Some(a),
2266        b: Some(b),
2267        use_lag: Some(use_lag),
2268        gamma: Some(gamma),
2269    };
2270    let (inner, inner_ctx, inner_dev_id) = py.allow_threads(|| {
2271        let cuda = CudaMacz::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
2272        let ctx = cuda.context_arc();
2273        let dev_id = cuda.device_id();
2274        cuda.macz_many_series_one_param_time_major_dev(price_tm, vol_tm_opt, cols, rows, &params)
2275            .map(|inner| (inner, ctx, dev_id))
2276            .map_err(|e| PyValueError::new_err(e.to_string()))
2277    })?;
2278    Ok(DeviceArrayF32Py {
2279        inner,
2280        _ctx: Some(inner_ctx),
2281        device_id: Some(inner_dev_id),
2282    })
2283}
2284
2285#[cfg(feature = "python")]
2286#[pyclass(name = "MaczStream")]
2287pub struct MaczStreamPy {
2288    stream: MaczStream,
2289}
2290
2291#[cfg(feature = "python")]
2292#[pymethods]
2293impl MaczStreamPy {
2294    #[new]
2295    fn new(
2296        fast_length: usize,
2297        slow_length: usize,
2298        signal_length: usize,
2299        lengthz: usize,
2300        length_stdev: usize,
2301        a: f64,
2302        b: f64,
2303        use_lag: bool,
2304        gamma: f64,
2305    ) -> PyResult<Self> {
2306        let params = MaczParams {
2307            fast_length: Some(fast_length),
2308            slow_length: Some(slow_length),
2309            signal_length: Some(signal_length),
2310            lengthz: Some(lengthz),
2311            length_stdev: Some(length_stdev),
2312            a: Some(a),
2313            b: Some(b),
2314            use_lag: Some(use_lag),
2315            gamma: Some(gamma),
2316        };
2317        let stream = MaczStream::new(params).map_err(|e| PyValueError::new_err(e.to_string()))?;
2318        Ok(MaczStreamPy { stream })
2319    }
2320
2321    fn update(&mut self, value: f64, volume: Option<f64>) -> Option<f64> {
2322        self.stream.update(value, volume)
2323    }
2324}
2325
2326#[cfg(feature = "python")]
2327#[pyfunction(name = "macz_batch")]
2328#[pyo3(signature = (data, volume=None, fast_length_range=(12,12,0), slow_length_range=(25,25,0), signal_length_range=(9,9,0), lengthz_range=(20,20,0), length_stdev_range=(25,25,0), a_range=(1.0,1.0,0.0), b_range=(1.0,1.0,0.0), use_lag_range=(false,false,false), gamma_range=(0.02,0.02,0.0), kernel=None))]
2329pub fn macz_batch_py<'py>(
2330    py: Python<'py>,
2331    data: numpy::PyReadonlyArray1<'py, f64>,
2332    volume: Option<numpy::PyReadonlyArray1<'py, f64>>,
2333    fast_length_range: (usize, usize, usize),
2334    slow_length_range: (usize, usize, usize),
2335    signal_length_range: (usize, usize, usize),
2336    lengthz_range: (usize, usize, usize),
2337    length_stdev_range: (usize, usize, usize),
2338    a_range: (f64, f64, f64),
2339    b_range: (f64, f64, f64),
2340    use_lag_range: (bool, bool, bool),
2341    gamma_range: (f64, f64, f64),
2342    kernel: Option<&str>,
2343) -> PyResult<Bound<'py, PyDict>> {
2344    use numpy::{PyArray1, PyArrayMethods};
2345    let slice_in = data.as_slice()?;
2346    let vol_opt: Option<&[f64]> = volume.as_ref().map(|v| v.as_slice()).transpose()?;
2347    let sweep = MaczBatchRange {
2348        fast_length: fast_length_range,
2349        slow_length: slow_length_range,
2350        signal_length: signal_length_range,
2351        lengthz: lengthz_range,
2352        length_stdev: length_stdev_range,
2353        a: a_range,
2354        b: b_range,
2355    };
2356    let kern = validate_kernel(kernel, true)?;
2357    let combos = expand_grid_macz(&sweep).map_err(|e| PyValueError::new_err(e.to_string()))?;
2358    let rows = combos.len();
2359    let cols = slice_in.len();
2360
2361    let total_len = rows
2362        .checked_mul(cols)
2363        .ok_or_else(|| PyValueError::new_err("rows*cols overflow in macz_batch_py"))?;
2364    let out_arr = unsafe { PyArray1::<f64>::new(py, [total_len], false) };
2365    let slice_out = unsafe { out_arr.as_slice_mut()? };
2366
2367    let combos = py
2368        .allow_threads(|| {
2369            let k = match kern {
2370                Kernel::Auto => Kernel::ScalarBatch,
2371                k => k,
2372            };
2373            macz_batch_inner_into_vol(slice_in, vol_opt, &sweep, k, true, slice_out)
2374        })
2375        .map_err(|e| PyValueError::new_err(e.to_string()))?;
2376
2377    let dict = PyDict::new(py);
2378    dict.set_item("values", out_arr.reshape((rows, cols))?)?;
2379    dict.set_item(
2380        "fast_lengths",
2381        combos
2382            .iter()
2383            .map(|p| p.fast_length.unwrap())
2384            .collect::<Vec<_>>()
2385            .into_pyarray(py),
2386    )?;
2387    dict.set_item(
2388        "slow_lengths",
2389        combos
2390            .iter()
2391            .map(|p| p.slow_length.unwrap())
2392            .collect::<Vec<_>>()
2393            .into_pyarray(py),
2394    )?;
2395    dict.set_item(
2396        "signal_lengths",
2397        combos
2398            .iter()
2399            .map(|p| p.signal_length.unwrap())
2400            .collect::<Vec<_>>()
2401            .into_pyarray(py),
2402    )?;
2403    dict.set_item(
2404        "lengthz",
2405        combos
2406            .iter()
2407            .map(|p| p.lengthz.unwrap())
2408            .collect::<Vec<_>>()
2409            .into_pyarray(py),
2410    )?;
2411    dict.set_item(
2412        "length_stdev",
2413        combos
2414            .iter()
2415            .map(|p| p.length_stdev.unwrap())
2416            .collect::<Vec<_>>()
2417            .into_pyarray(py),
2418    )?;
2419    dict.set_item(
2420        "a",
2421        combos
2422            .iter()
2423            .map(|p| p.a.unwrap())
2424            .collect::<Vec<_>>()
2425            .into_pyarray(py),
2426    )?;
2427    dict.set_item(
2428        "b",
2429        combos
2430            .iter()
2431            .map(|p| p.b.unwrap())
2432            .collect::<Vec<_>>()
2433            .into_pyarray(py),
2434    )?;
2435    Ok(dict)
2436}
2437
2438#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2439#[wasm_bindgen]
2440pub fn macz_js(
2441    data: &[f64],
2442    fast_length: usize,
2443    slow_length: usize,
2444    signal_length: usize,
2445    lengthz: usize,
2446    length_stdev: usize,
2447    a: f64,
2448    b: f64,
2449    use_lag: bool,
2450    gamma: f64,
2451) -> Result<Vec<f64>, JsValue> {
2452    let params = MaczParams {
2453        fast_length: Some(fast_length),
2454        slow_length: Some(slow_length),
2455        signal_length: Some(signal_length),
2456        lengthz: Some(lengthz),
2457        length_stdev: Some(length_stdev),
2458        a: Some(a),
2459        b: Some(b),
2460        use_lag: Some(use_lag),
2461        gamma: Some(gamma),
2462    };
2463
2464    let input = MaczInput::from_slice(data, params);
2465
2466    macz(&input)
2467        .map(|o| o.values)
2468        .map_err(|e| JsValue::from_str(&e.to_string()))
2469}
2470
2471#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2472#[wasm_bindgen]
2473pub fn macz_batch_into(
2474    in_ptr: *const f64,
2475    out_ptr: *mut f64,
2476    len: usize,
2477    fast_start: usize,
2478    fast_end: usize,
2479    fast_step: usize,
2480    slow_start: usize,
2481    slow_end: usize,
2482    slow_step: usize,
2483    sig_start: usize,
2484    sig_end: usize,
2485    sig_step: usize,
2486    lz_start: usize,
2487    lz_end: usize,
2488    lz_step: usize,
2489    lsd_start: usize,
2490    lsd_end: usize,
2491    lsd_step: usize,
2492    a_start: f64,
2493    a_end: f64,
2494    a_step: f64,
2495    b_start: f64,
2496    b_end: f64,
2497    b_step: f64,
2498) -> Result<usize, JsValue> {
2499    if in_ptr.is_null() || out_ptr.is_null() {
2500        return Err(JsValue::from_str("null pointer passed to macz_batch_into"));
2501    }
2502    unsafe {
2503        let data = std::slice::from_raw_parts(in_ptr, len);
2504        let sweep = MaczBatchRange {
2505            fast_length: (fast_start, fast_end, fast_step),
2506            slow_length: (slow_start, slow_end, slow_step),
2507            signal_length: (sig_start, sig_end, sig_step),
2508            lengthz: (lz_start, lz_end, lz_step),
2509            length_stdev: (lsd_start, lsd_end, lsd_step),
2510            a: (a_start, a_end, a_step),
2511            b: (b_start, b_end, b_step),
2512        };
2513        let combos = expand_grid_macz(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
2514        let rows = combos.len();
2515        let cols = len;
2516        let total = rows
2517            .checked_mul(cols)
2518            .ok_or_else(|| JsValue::from_str("rows*cols overflow in macz_batch_into"))?;
2519        let out = std::slice::from_raw_parts_mut(out_ptr, total);
2520
2521        macz_batch_inner_into(data, &sweep, detect_best_kernel(), false, out)
2522            .map_err(|e| JsValue::from_str(&e.to_string()))?;
2523        Ok(rows)
2524    }
2525}
2526
2527#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2528#[wasm_bindgen]
2529pub fn macz_batch_into_with_volume(
2530    in_ptr: *const f64,
2531    vol_ptr: *const f64,
2532    out_ptr: *mut f64,
2533    len: usize,
2534    fast_start: usize,
2535    fast_end: usize,
2536    fast_step: usize,
2537    slow_start: usize,
2538    slow_end: usize,
2539    slow_step: usize,
2540    sig_start: usize,
2541    sig_end: usize,
2542    sig_step: usize,
2543    lz_start: usize,
2544    lz_end: usize,
2545    lz_step: usize,
2546    lsd_start: usize,
2547    lsd_end: usize,
2548    lsd_step: usize,
2549    a_start: f64,
2550    a_end: f64,
2551    a_step: f64,
2552    b_start: f64,
2553    b_end: f64,
2554    b_step: f64,
2555) -> Result<usize, JsValue> {
2556    if in_ptr.is_null() || vol_ptr.is_null() || out_ptr.is_null() {
2557        return Err(JsValue::from_str(
2558            "null pointer passed to macz_batch_into_with_volume",
2559        ));
2560    }
2561    unsafe {
2562        let data = std::slice::from_raw_parts(in_ptr, len);
2563        let volume = std::slice::from_raw_parts(vol_ptr, len);
2564        let sweep = MaczBatchRange {
2565            fast_length: (fast_start, fast_end, fast_step),
2566            slow_length: (slow_start, slow_end, slow_step),
2567            signal_length: (sig_start, sig_end, sig_step),
2568            lengthz: (lz_start, lz_end, lz_step),
2569            length_stdev: (lsd_start, lsd_end, lsd_step),
2570            a: (a_start, a_end, a_step),
2571            b: (b_start, b_end, b_step),
2572        };
2573        let combos = expand_grid_macz(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
2574        let rows = combos.len();
2575        let cols = len;
2576        let total = rows.checked_mul(cols).ok_or_else(|| {
2577            JsValue::from_str("rows*cols overflow in macz_batch_into_with_volume")
2578        })?;
2579        let out = std::slice::from_raw_parts_mut(out_ptr, total);
2580
2581        macz_batch_inner_into_vol(data, Some(volume), &sweep, detect_best_kernel(), false, out)
2582            .map_err(|e| JsValue::from_str(&e.to_string()))?;
2583        Ok(rows)
2584    }
2585}
2586
2587#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2588#[wasm_bindgen]
2589pub fn macz_alloc(len: usize) -> *mut f64 {
2590    let mut v = Vec::<f64>::with_capacity(len);
2591    let ptr = v.as_mut_ptr();
2592    std::mem::forget(v);
2593    ptr
2594}
2595
2596#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2597#[wasm_bindgen]
2598pub fn macz_free(ptr: *mut f64, len: usize) {
2599    unsafe {
2600        let _ = Vec::from_raw_parts(ptr, len, len);
2601    }
2602}
2603
2604#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2605#[wasm_bindgen]
2606pub fn macz_into(
2607    in_ptr: *const f64,
2608    out_ptr: *mut f64,
2609    len: usize,
2610    fast_length: usize,
2611    slow_length: usize,
2612    signal_length: usize,
2613    lengthz: usize,
2614    length_stdev: usize,
2615    a: f64,
2616    b: f64,
2617    use_lag: bool,
2618    gamma: f64,
2619) -> Result<(), JsValue> {
2620    if in_ptr.is_null() || out_ptr.is_null() {
2621        return Err(JsValue::from_str("null pointer"));
2622    }
2623    unsafe {
2624        let data = std::slice::from_raw_parts(in_ptr, len);
2625        let params = MaczParams {
2626            fast_length: Some(fast_length),
2627            slow_length: Some(slow_length),
2628            signal_length: Some(signal_length),
2629            lengthz: Some(lengthz),
2630            length_stdev: Some(length_stdev),
2631            a: Some(a),
2632            b: Some(b),
2633            use_lag: Some(use_lag),
2634            gamma: Some(gamma),
2635        };
2636        let input = MaczInput::from_slice(data, params);
2637        let out = std::slice::from_raw_parts_mut(out_ptr, len);
2638        macz_into_slice(out, &input, Kernel::Auto).map_err(|e| JsValue::from_str(&e.to_string()))
2639    }
2640}
2641
2642#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2643#[wasm_bindgen(js_name = "macz")]
2644pub fn macz_wasm_zero_copy(
2645    data: &[f64],
2646    out_ptr: *mut f64,
2647    fast_length: usize,
2648    slow_length: usize,
2649    signal_length: usize,
2650    lengthz: usize,
2651    length_stdev: usize,
2652    a: f64,
2653    b: f64,
2654    use_lag: bool,
2655    gamma: f64,
2656) -> Result<(), JsValue> {
2657    if out_ptr.is_null() {
2658        return Err(JsValue::from_str("Output pointer is null"));
2659    }
2660
2661    unsafe {
2662        let out_slice = std::slice::from_raw_parts_mut(out_ptr, data.len());
2663        let params = MaczParams {
2664            fast_length: Some(fast_length),
2665            slow_length: Some(slow_length),
2666            signal_length: Some(signal_length),
2667            lengthz: Some(lengthz),
2668            length_stdev: Some(length_stdev),
2669            a: Some(a),
2670            b: Some(b),
2671            use_lag: Some(use_lag),
2672            gamma: Some(gamma),
2673        };
2674
2675        let input = MaczInput::from_slice(data, params);
2676        macz_into_slice(out_slice, &input, Kernel::Auto)
2677            .map_err(|e| JsValue::from_str(&e.to_string()))
2678    }
2679}
2680
2681#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2682#[wasm_bindgen]
2683pub fn macz_batch(
2684    data: &[f64],
2685    volume: Option<Vec<f64>>,
2686    fast_length_range: Vec<usize>,
2687    slow_length_range: Vec<usize>,
2688    signal_length_range: Vec<usize>,
2689    lengthz_range: Vec<usize>,
2690    length_stdev_range: Vec<usize>,
2691    a_range: Vec<f64>,
2692    b_range: Vec<f64>,
2693    use_lag_range: JsValue,
2694    gamma_range: Vec<f64>,
2695) -> Result<JsValue, JsValue> {
2696    if fast_length_range.len() != 3
2697        || slow_length_range.len() != 3
2698        || signal_length_range.len() != 3
2699        || lengthz_range.len() != 3
2700        || length_stdev_range.len() != 3
2701        || a_range.len() != 3
2702        || b_range.len() != 3
2703        || gamma_range.len() != 3
2704    {
2705        return Err(JsValue::from_str(
2706            "All ranges must have exactly 3 elements: [start, end, step]",
2707        ));
2708    }
2709
2710    let use_lag_arr = js_sys::Array::from(&use_lag_range);
2711    if use_lag_arr.length() != 3 {
2712        return Err(JsValue::from_str(
2713            "use_lag_range must have exactly 3 elements",
2714        ));
2715    }
2716    let use_lag = use_lag_arr.get(0).as_bool().unwrap_or(false);
2717
2718    let sweep = MaczBatchRange {
2719        fast_length: (
2720            fast_length_range[0],
2721            fast_length_range[1],
2722            fast_length_range[2],
2723        ),
2724        slow_length: (
2725            slow_length_range[0],
2726            slow_length_range[1],
2727            slow_length_range[2],
2728        ),
2729        signal_length: (
2730            signal_length_range[0],
2731            signal_length_range[1],
2732            signal_length_range[2],
2733        ),
2734        lengthz: (lengthz_range[0], lengthz_range[1], lengthz_range[2]),
2735        length_stdev: (
2736            length_stdev_range[0],
2737            length_stdev_range[1],
2738            length_stdev_range[2],
2739        ),
2740        a: (a_range[0], a_range[1], a_range[2]),
2741        b: (b_range[0], b_range[1], b_range[2]),
2742    };
2743
2744    let volume_ref = volume.as_deref();
2745    let output = if let Some(vol) = volume_ref {
2746        macz_batch_slice_vol(data, Some(vol), &sweep, detect_best_kernel())
2747    } else {
2748        macz_batch_slice(data, &sweep, detect_best_kernel())
2749    }
2750    .map_err(|e| JsValue::from_str(&e.to_string()))?;
2751
2752    let result = MaczBatchJsOutput {
2753        values: vec![output.values.clone()],
2754        fast_lengths: output
2755            .combos
2756            .iter()
2757            .map(|c| c.fast_length.unwrap_or(12))
2758            .collect(),
2759        slow_lengths: output
2760            .combos
2761            .iter()
2762            .map(|c| c.slow_length.unwrap_or(25))
2763            .collect(),
2764        signal_lengths: output
2765            .combos
2766            .iter()
2767            .map(|c| c.signal_length.unwrap_or(9))
2768            .collect(),
2769    };
2770
2771    serde_wasm_bindgen::to_value(&result).map_err(|e| JsValue::from_str(&e.to_string()))
2772}
2773
2774#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2775#[wasm_bindgen]
2776pub fn macz_batch_zero_copy(
2777    data: &[f64],
2778    volume: Option<Vec<f64>>,
2779    out_ptr: *mut f64,
2780    fast_length_range: Vec<usize>,
2781    slow_length_range: Vec<usize>,
2782    signal_length_range: Vec<usize>,
2783    lengthz_range: Vec<usize>,
2784    length_stdev_range: Vec<usize>,
2785    a_range: Vec<f64>,
2786    b_range: Vec<f64>,
2787    use_lag_range: JsValue,
2788    gamma_range: Vec<f64>,
2789) -> Result<usize, JsValue> {
2790    if out_ptr.is_null() {
2791        return Err(JsValue::from_str("Output pointer is null"));
2792    }
2793
2794    if fast_length_range.len() != 3
2795        || slow_length_range.len() != 3
2796        || signal_length_range.len() != 3
2797        || lengthz_range.len() != 3
2798        || length_stdev_range.len() != 3
2799        || a_range.len() != 3
2800        || b_range.len() != 3
2801        || gamma_range.len() != 3
2802    {
2803        return Err(JsValue::from_str(
2804            "All ranges must have exactly 3 elements: [start, end, step]",
2805        ));
2806    }
2807
2808    let use_lag_arr = js_sys::Array::from(&use_lag_range);
2809    if use_lag_arr.length() != 3 {
2810        return Err(JsValue::from_str(
2811            "use_lag_range must have exactly 3 elements",
2812        ));
2813    }
2814    let use_lag = use_lag_arr.get(0).as_bool().unwrap_or(false);
2815
2816    let sweep = MaczBatchRange {
2817        fast_length: (
2818            fast_length_range[0],
2819            fast_length_range[1],
2820            fast_length_range[2],
2821        ),
2822        slow_length: (
2823            slow_length_range[0],
2824            slow_length_range[1],
2825            slow_length_range[2],
2826        ),
2827        signal_length: (
2828            signal_length_range[0],
2829            signal_length_range[1],
2830            signal_length_range[2],
2831        ),
2832        lengthz: (lengthz_range[0], lengthz_range[1], lengthz_range[2]),
2833        length_stdev: (
2834            length_stdev_range[0],
2835            length_stdev_range[1],
2836            length_stdev_range[2],
2837        ),
2838        a: (a_range[0], a_range[1], a_range[2]),
2839        b: (b_range[0], b_range[1], b_range[2]),
2840    };
2841
2842    let num_combinations = 1;
2843
2844    unsafe {
2845        let out_slice = std::slice::from_raw_parts_mut(out_ptr, num_combinations * data.len());
2846        let volume_ref = volume.as_deref();
2847
2848        let params = MaczParams {
2849            fast_length: Some(fast_length_range[0]),
2850            slow_length: Some(slow_length_range[0]),
2851            signal_length: Some(signal_length_range[0]),
2852            lengthz: Some(lengthz_range[0]),
2853            length_stdev: Some(length_stdev_range[0]),
2854            a: Some(a_range[0]),
2855            b: Some(b_range[0]),
2856            use_lag: Some(use_lag),
2857            gamma: Some(gamma_range[0]),
2858        };
2859
2860        let input = if let Some(vol) = volume_ref {
2861            MaczInput {
2862                data: MaczData::SliceWithVolume { data, volume: vol },
2863                params,
2864            }
2865        } else {
2866            MaczInput::from_slice(data, params)
2867        };
2868
2869        macz_into_slice(&mut out_slice[..data.len()], &input, Kernel::Auto)
2870            .map_err(|e| JsValue::from_str(&e.to_string()))?;
2871
2872        Ok(num_combinations)
2873    }
2874}
2875
2876#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2877#[derive(Serialize, Deserialize)]
2878pub struct MaczBatchJsOutput {
2879    pub values: Vec<Vec<f64>>,
2880    pub fast_lengths: Vec<usize>,
2881    pub slow_lengths: Vec<usize>,
2882    pub signal_lengths: Vec<usize>,
2883}
2884
2885#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2886#[derive(Serialize, Deserialize)]
2887pub struct MaczBatchConfig {
2888    pub fast_length_min: usize,
2889    pub fast_length_max: usize,
2890    pub fast_length_step: usize,
2891    pub slow_length_min: usize,
2892    pub slow_length_max: usize,
2893    pub slow_length_step: usize,
2894    pub signal_length_min: usize,
2895    pub signal_length_max: usize,
2896    pub signal_length_step: usize,
2897    pub lengthz: usize,
2898    pub length_stdev: usize,
2899    pub a: f64,
2900    pub b: f64,
2901    pub use_lag: bool,
2902    pub gamma: f64,
2903}
2904
2905#[cfg(test)]
2906mod tests {
2907    use super::*;
2908    use crate::skip_if_unsupported;
2909    use crate::utilities::data_loader::read_candles_from_csv;
2910    #[cfg(feature = "proptest")]
2911    use proptest::prelude::*;
2912    use std::error::Error;
2913
2914    #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
2915    #[test]
2916    fn test_macz_into_matches_api() -> Result<(), Box<dyn Error>> {
2917        let n = 256usize;
2918        let mut data = Vec::with_capacity(n);
2919        for i in 0..n {
2920            let t = i as f64 * 0.1;
2921            data.push(50.0 + t.sin() * 5.0 + ((i % 7) as f64) * 0.01);
2922        }
2923
2924        let input = MaczInput::from_slice(&data, MaczParams::default());
2925
2926        let baseline = macz(&input)?.values;
2927
2928        let mut out = vec![0.0; data.len()];
2929        macz_into(&input, &mut out)?;
2930
2931        assert_eq!(out.len(), baseline.len());
2932
2933        let eq =
2934            |a: f64, b: f64| (a.is_nan() && b.is_nan()) || (a == b) || ((a - b).abs() <= 1e-12);
2935        for i in 0..out.len() {
2936            assert!(
2937                eq(out[i], baseline[i]),
2938                "mismatch at {}: into={} api={}",
2939                i,
2940                out[i],
2941                baseline[i]
2942            );
2943        }
2944        Ok(())
2945    }
2946
2947    fn check_macz_partial_params(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2948        skip_if_unsupported!(kernel, test_name);
2949        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2950        let candles = read_candles_from_csv(file_path)?;
2951
2952        let params = MaczParams {
2953            fast_length: None,
2954            slow_length: None,
2955            signal_length: None,
2956            lengthz: None,
2957            length_stdev: None,
2958            a: None,
2959            b: None,
2960            use_lag: None,
2961            gamma: None,
2962        };
2963
2964        let volume = vec![1.0; candles.close.len()];
2965        let input = MaczInput::from_candles_with_volume(&candles, "close", &volume, params);
2966        let output = macz_with_kernel(&input, kernel)?;
2967        assert_eq!(output.values.len(), candles.close.len());
2968
2969        Ok(())
2970    }
2971
2972    fn check_macz_accuracy(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2973        skip_if_unsupported!(kernel, test_name);
2974        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2975        let candles = read_candles_from_csv(file_path)?;
2976
2977        let params = MaczParams::default();
2978        let input = MaczInput::from_candles(&candles, "close", params);
2979        let result = macz_with_kernel(&input, kernel)?;
2980
2981        let expected = vec![0.51988421, 0.23019592, 0.08030845, 0.12276454, -0.56402159];
2982        let actual = &result.values[result.values.len() - 5..];
2983
2984        println!("Last 5 MACZ values: {:?}", actual);
2985        println!("Expected values: {:?}", expected);
2986
2987        for (i, (&exp, &act)) in expected.iter().zip(actual.iter()).enumerate() {
2988            let diff = (act - exp).abs();
2989            println!(
2990                "Value {}: expected={}, actual={}, diff={}",
2991                i, exp, act, diff
2992            );
2993
2994            assert!(
2995                diff < 0.2,
2996                "Value {} mismatch: expected {}, got {}, diff {}",
2997                i,
2998                exp,
2999                act,
3000                diff
3001            );
3002        }
3003
3004        assert!(
3005            true,
3006            "[{}] MAC-Z should produce non-NaN values after warmup",
3007            test_name
3008        );
3009
3010        Ok(())
3011    }
3012
3013    fn check_macz_zero_period(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
3014        skip_if_unsupported!(kernel, test_name);
3015        let input_data = [10.0, 20.0, 30.0];
3016        let params = MaczParams {
3017            fast_length: Some(0),
3018            slow_length: Some(25),
3019            signal_length: Some(9),
3020            lengthz: Some(20),
3021            length_stdev: Some(25),
3022            a: Some(1.0),
3023            b: Some(1.0),
3024            use_lag: Some(false),
3025            gamma: Some(0.02),
3026        };
3027        let input = MaczInput::from_slice(&input_data, params);
3028        let res = macz_with_kernel(&input, kernel);
3029        assert!(
3030            res.is_err(),
3031            "[{}] MAC-Z should fail with zero period",
3032            test_name
3033        );
3034        Ok(())
3035    }
3036
3037    fn check_macz_period_exceeds_length(
3038        test_name: &str,
3039        kernel: Kernel,
3040    ) -> Result<(), Box<dyn Error>> {
3041        skip_if_unsupported!(kernel, test_name);
3042        let data_small = [10.0, 20.0, 30.0];
3043        let params = MaczParams {
3044            fast_length: Some(12),
3045            slow_length: Some(100),
3046            signal_length: Some(9),
3047            lengthz: Some(20),
3048            length_stdev: Some(25),
3049            a: Some(1.0),
3050            b: Some(1.0),
3051            use_lag: Some(false),
3052            gamma: Some(0.02),
3053        };
3054        let input = MaczInput::from_slice(&data_small, params);
3055        let res = macz_with_kernel(&input, kernel);
3056        assert!(
3057            res.is_err(),
3058            "[{}] MAC-Z should fail with period exceeding length",
3059            test_name
3060        );
3061        Ok(())
3062    }
3063
3064    fn check_macz_very_small_dataset(
3065        test_name: &str,
3066        kernel: Kernel,
3067    ) -> Result<(), Box<dyn Error>> {
3068        skip_if_unsupported!(kernel, test_name);
3069        let single_point = [42.0];
3070        let params = MaczParams {
3071            fast_length: Some(12),
3072            slow_length: Some(25),
3073            signal_length: Some(9),
3074            lengthz: Some(20),
3075            length_stdev: Some(25),
3076            a: Some(1.0),
3077            b: Some(1.0),
3078            use_lag: Some(false),
3079            gamma: Some(0.02),
3080        };
3081        let input = MaczInput::from_slice(&single_point, params);
3082        let res = macz_with_kernel(&input, kernel);
3083        assert!(
3084            res.is_err(),
3085            "[{}] MAC-Z should fail with insufficient data",
3086            test_name
3087        );
3088        Ok(())
3089    }
3090
3091    fn check_macz_empty_input(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
3092        skip_if_unsupported!(kernel, test_name);
3093        let empty: [f64; 0] = [];
3094        let input = MaczInput::from_slice(&empty, MaczParams::default());
3095        let res = macz_with_kernel(&input, kernel);
3096        assert!(
3097            matches!(res, Err(MaczError::EmptyInputData)),
3098            "[{}] MAC-Z should fail with empty input",
3099            test_name
3100        );
3101        Ok(())
3102    }
3103
3104    fn check_macz_invalid_a_constant(
3105        test_name: &str,
3106        kernel: Kernel,
3107    ) -> Result<(), Box<dyn Error>> {
3108        skip_if_unsupported!(kernel, test_name);
3109        let data = [1.0, 2.0, 3.0, 4.0, 5.0];
3110        let params = MaczParams {
3111            fast_length: Some(2),
3112            slow_length: Some(3),
3113            signal_length: Some(2),
3114            lengthz: Some(2),
3115            length_stdev: Some(2),
3116            a: Some(3.0),
3117            b: Some(1.0),
3118            use_lag: Some(false),
3119            gamma: Some(0.02),
3120        };
3121        let input = MaczInput::from_slice(&data, params);
3122        let res = macz_with_kernel(&input, kernel);
3123        assert!(
3124            res.is_err(),
3125            "[{}] MAC-Z should fail with invalid A constant",
3126            test_name
3127        );
3128        Ok(())
3129    }
3130
3131    fn check_macz_invalid_b_constant(
3132        test_name: &str,
3133        kernel: Kernel,
3134    ) -> Result<(), Box<dyn Error>> {
3135        skip_if_unsupported!(kernel, test_name);
3136        let data = [1.0, 2.0, 3.0, 4.0, 5.0];
3137        let params = MaczParams {
3138            fast_length: Some(2),
3139            slow_length: Some(3),
3140            signal_length: Some(2),
3141            lengthz: Some(2),
3142            length_stdev: Some(2),
3143            a: Some(1.0),
3144            b: Some(-3.0),
3145            use_lag: Some(false),
3146            gamma: Some(0.02),
3147        };
3148        let input = MaczInput::from_slice(&data, params);
3149        let res = macz_with_kernel(&input, kernel);
3150        assert!(
3151            res.is_err(),
3152            "[{}] MAC-Z should fail with invalid B constant",
3153            test_name
3154        );
3155        Ok(())
3156    }
3157
3158    fn check_macz_invalid_gamma(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
3159        skip_if_unsupported!(kernel, test_name);
3160        let data = [1.0, 2.0, 3.0, 4.0, 5.0];
3161        let params = MaczParams {
3162            fast_length: Some(2),
3163            slow_length: Some(3),
3164            signal_length: Some(2),
3165            lengthz: Some(2),
3166            length_stdev: Some(2),
3167            a: Some(1.0),
3168            b: Some(1.0),
3169            use_lag: Some(true),
3170            gamma: Some(1.5),
3171        };
3172        let input = MaczInput::from_slice(&data, params);
3173        let res = macz_with_kernel(&input, kernel);
3174        assert!(
3175            res.is_err(),
3176            "[{}] MAC-Z should fail with invalid gamma",
3177            test_name
3178        );
3179        Ok(())
3180    }
3181
3182    fn check_macz_nan_handling(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
3183        skip_if_unsupported!(kernel, test_name);
3184
3185        let data_with_nan = vec![
3186            1.0,
3187            2.0,
3188            f64::NAN,
3189            4.0,
3190            5.0,
3191            6.0,
3192            7.0,
3193            8.0,
3194            9.0,
3195            10.0,
3196            11.0,
3197            12.0,
3198            13.0,
3199            14.0,
3200            15.0,
3201            16.0,
3202            17.0,
3203            18.0,
3204            19.0,
3205            20.0,
3206            21.0,
3207            22.0,
3208            23.0,
3209            24.0,
3210            25.0,
3211            26.0,
3212            27.0,
3213            28.0,
3214            29.0,
3215            30.0,
3216            31.0,
3217            32.0,
3218            33.0,
3219            34.0,
3220            35.0,
3221            36.0,
3222            37.0,
3223            38.0,
3224            39.0,
3225            40.0,
3226            41.0,
3227            42.0,
3228            43.0,
3229            44.0,
3230            45.0,
3231        ];
3232
3233        let params = MaczParams {
3234            fast_length: Some(5),
3235            slow_length: Some(10),
3236            signal_length: Some(3),
3237            lengthz: Some(8),
3238            length_stdev: Some(10),
3239            a: Some(1.0),
3240            b: Some(1.0),
3241            use_lag: Some(false),
3242            gamma: Some(0.02),
3243        };
3244
3245        let input = MaczInput::from_slice(&data_with_nan, params);
3246        let res = macz_with_kernel(&input, kernel)?;
3247
3248        assert!(
3249            res.values[2].is_nan(),
3250            "[{}] NaN should be propagated",
3251            test_name
3252        );
3253
3254        Ok(())
3255    }
3256
3257    fn check_macz_streaming(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
3258        skip_if_unsupported!(kernel, test_name);
3259
3260        let test_data: Vec<f64> = (0..100).map(|i| 50.0 + (i as f64).sin() * 10.0).collect();
3261
3262        let params = MaczParams {
3263            fast_length: Some(5),
3264            slow_length: Some(10),
3265            signal_length: Some(3),
3266            lengthz: Some(8),
3267            length_stdev: Some(10),
3268            a: Some(1.0),
3269            b: Some(1.0),
3270            use_lag: Some(false),
3271            gamma: Some(0.02),
3272        };
3273
3274        let batch_input = MaczInput::from_slice(&test_data, params.clone());
3275        let batch_result = macz_with_kernel(&batch_input, kernel)?;
3276
3277        let mut stream = MaczStream::new(params)?;
3278        let mut stream_results = Vec::new();
3279
3280        for &value in &test_data {
3281            let result = stream.update(value, None);
3282            stream_results.push(result.unwrap_or(f64::NAN));
3283        }
3284
3285        let batch_valid: Vec<f64> = batch_result
3286            .values
3287            .iter()
3288            .rev()
3289            .filter(|v| !v.is_nan())
3290            .take(10)
3291            .copied()
3292            .collect();
3293
3294        let stream_valid: Vec<f64> = stream_results
3295            .iter()
3296            .rev()
3297            .filter(|v| !v.is_nan())
3298            .take(10)
3299            .copied()
3300            .collect();
3301
3302        if !batch_valid.is_empty() && !stream_valid.is_empty() {
3303            assert!(
3304                batch_valid.len() > 0 && stream_valid.len() > 0,
3305                "[{}] Both batch and stream should produce valid values",
3306                test_name
3307            );
3308        }
3309
3310        Ok(())
3311    }
3312
3313    fn check_macz_no_poison_kernel(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
3314        skip_if_unsupported!(kernel, test_name);
3315
3316        let data: Vec<f64> = (0..100).map(|i| 50.0 + (i as f64).sin() * 5.0).collect();
3317        let volume: Vec<f64> = (0..100).map(|i| 1000.0 + (i as f64) * 10.0).collect();
3318
3319        let params = MaczParams::default();
3320        let input_with_vol = MaczInput::from_slice_with_volume(&data, &volume, params);
3321        let result_vol = macz_with_kernel(&input_with_vol, kernel)?;
3322
3323        let mut actual_warmup = 0;
3324        for (i, &val) in result_vol.values.iter().enumerate() {
3325            if !val.is_nan() {
3326                actual_warmup = i;
3327                break;
3328            }
3329        }
3330
3331        assert!(
3332            actual_warmup >= 25,
3333            "[{}] Warmup should be at least slow period",
3334            test_name
3335        );
3336        assert!(
3337            actual_warmup < 50,
3338            "[{}] Warmup should be reasonable",
3339            test_name
3340        );
3341
3342        let mut dst = vec![123.456; data.len()];
3343        macz_into_slice(&mut dst, &input_with_vol, kernel)?;
3344
3345        for i in 0..actual_warmup {
3346            assert!(
3347                dst[i].is_nan(),
3348                "[{}] into_slice should preserve NaN warmup at {}",
3349                test_name,
3350                i
3351            );
3352        }
3353
3354        Ok(())
3355    }
3356
3357    fn check_batch_default_row(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
3358        skip_if_unsupported!(kernel, test_name);
3359
3360        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
3361        let candles = read_candles_from_csv(file)?;
3362
3363        let output = MaczBatchBuilder::new()
3364            .kernel(kernel)
3365            .apply_candles(&candles, "close")?;
3366
3367        let def_params = MaczParams::default();
3368        let row = output.values_for(&def_params).expect("default row missing");
3369
3370        assert_eq!(row.len(), candles.close.len());
3371
3372        let warmup = 33;
3373        let valid_count = row[warmup..].iter().filter(|v| !v.is_nan()).count();
3374        assert!(
3375            valid_count > 0,
3376            "[{}] Should have valid values after warmup",
3377            test_name
3378        );
3379
3380        Ok(())
3381    }
3382
3383    fn check_batch_sweep(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
3384        skip_if_unsupported!(kernel, test_name);
3385
3386        let data: Vec<f64> = (0..100).map(|i| 50.0 + (i as f64).sin() * 10.0).collect();
3387
3388        let sweep = MaczBatchRange {
3389            fast_length: (10, 12, 1),
3390            slow_length: (20, 22, 1),
3391            signal_length: (5, 6, 1),
3392            lengthz: (15, 16, 1),
3393            length_stdev: (20, 21, 1),
3394            a: (1.0, 1.0, 0.1),
3395            b: (1.0, 1.0, 0.1),
3396        };
3397
3398        let batch = macz_batch_with_kernel(&data, &sweep, kernel)?;
3399
3400        assert_eq!(batch.cols, data.len());
3401        assert!(batch.rows > 0, "[{}] Batch should produce rows", test_name);
3402
3403        assert!(
3404            !batch.combos.is_empty(),
3405            "[{}] Should have parameter combinations",
3406            test_name
3407        );
3408
3409        Ok(())
3410    }
3411
3412    macro_rules! generate_all_macz_tests {
3413        ($($test_fn:ident),*) => {
3414            paste::paste! {
3415                $(
3416                    #[test]
3417                    fn [<$test_fn _scalar>]() {
3418                        let _ = $test_fn(stringify!([<$test_fn _scalar>]), Kernel::Scalar);
3419                    }
3420                )*
3421                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
3422                $(
3423                    #[test]
3424                    fn [<$test_fn _avx2>]() {
3425                        let _ = $test_fn(stringify!([<$test_fn _avx2>]), Kernel::Avx2);
3426                    }
3427                    #[test]
3428                    fn [<$test_fn _avx512>]() {
3429                        let _ = $test_fn(stringify!([<$test_fn _avx512>]), Kernel::Avx512);
3430                    }
3431                )*
3432            }
3433        }
3434    }
3435
3436    fn check_macz_default_candles(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
3437        skip_if_unsupported!(kernel, test_name);
3438        let fp = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
3439        let c = read_candles_from_csv(fp)?;
3440        let input = MaczInput::with_default_candles_auto_volume(&c);
3441        match input.data {
3442            MaczData::Candles { source, .. } => assert_eq!(source, "close"),
3443            _ => panic!("Expected MaczData::Candles"),
3444        }
3445        let out = macz_with_kernel(&input, kernel)?;
3446        assert_eq!(out.values.len(), c.close.len());
3447        Ok(())
3448    }
3449
3450    fn check_macz_reinput(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
3451        skip_if_unsupported!(kernel, test_name);
3452        let fp = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
3453        let c = read_candles_from_csv(fp)?;
3454        let v = vec![1.0; c.close.len()];
3455        let first = MaczInput::from_candles_with_volume(&c, "close", &v, MaczParams::default());
3456        let a = macz_with_kernel(&first, kernel)?;
3457        let second = MaczInput::from_slice(&a.values, MaczParams::default());
3458        let b = macz_with_kernel(&second, kernel)?;
3459        assert_eq!(b.values.len(), a.values.len());
3460        Ok(())
3461    }
3462
3463    #[cfg(debug_assertions)]
3464    fn check_macz_no_poison(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
3465        skip_if_unsupported!(kernel, test_name);
3466        let fp = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
3467        let c = read_candles_from_csv(fp)?;
3468        let v = vec![1.0; c.close.len()];
3469        let input = MaczInput::from_candles_with_volume(&c, "close", &v, MaczParams::default());
3470        let out = macz_with_kernel(&input, kernel)?;
3471
3472        for (i, &val) in out.values.iter().enumerate() {
3473            if val.is_nan() {
3474                continue;
3475            }
3476            let bits = val.to_bits();
3477            assert_ne!(
3478                bits, 0x11111111_11111111,
3479                "[{}] alloc_with_nan_prefix poison at {}",
3480                test_name, i
3481            );
3482            assert_ne!(
3483                bits, 0x22222222_22222222,
3484                "[{}] init_matrix_prefixes poison at {}",
3485                test_name, i
3486            );
3487            assert_ne!(
3488                bits, 0x33333333_33333333,
3489                "[{}] make_uninit_matrix poison at {}",
3490                test_name, i
3491            );
3492        }
3493        Ok(())
3494    }
3495    #[cfg(not(debug_assertions))]
3496    fn check_macz_no_poison(_: &str, _: Kernel) -> Result<(), Box<dyn Error>> {
3497        Ok(())
3498    }
3499
3500    #[cfg(debug_assertions)]
3501    fn check_batch_no_poison(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
3502        skip_if_unsupported!(kernel, test);
3503        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
3504        let c = read_candles_from_csv(file)?;
3505        let out = MaczBatchBuilder::new()
3506            .kernel(kernel)
3507            .fast_range(10, 12, 1)
3508            .slow_range(20, 22, 1)
3509            .signal_range(5, 6, 1)
3510            .lengthz_static(20)
3511            .length_stdev_static(25)
3512            .a_static(1.0)
3513            .b_static(1.0)
3514            .apply_candles(&c, "close")?;
3515
3516        for (idx, &v) in out.values.iter().enumerate() {
3517            if v.is_nan() {
3518                continue;
3519            }
3520            let b = v.to_bits();
3521            assert_ne!(b, 0x11111111_11111111, "[{}] alloc poison at {}", test, idx);
3522            assert_ne!(b, 0x22222222_22222222, "[{}] init poison at {}", test, idx);
3523            assert_ne!(b, 0x33333333_33333333, "[{}] make poison at {}", test, idx);
3524        }
3525        Ok(())
3526    }
3527    #[cfg(not(debug_assertions))]
3528    fn check_batch_no_poison(_: &str, _: Kernel) -> Result<(), Box<dyn Error>> {
3529        Ok(())
3530    }
3531
3532    fn check_batch_with_volume(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
3533        skip_if_unsupported!(kernel, test);
3534        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
3535        let c = read_candles_from_csv(file)?;
3536        let out = MaczBatchBuilder::new()
3537            .kernel(kernel)
3538            .apply_candles(&c, "close")?;
3539        assert_eq!(out.cols, c.close.len());
3540        assert!(out.rows >= 1);
3541        Ok(())
3542    }
3543
3544    generate_all_macz_tests!(
3545        check_macz_partial_params,
3546        check_macz_accuracy,
3547        check_macz_zero_period,
3548        check_macz_period_exceeds_length,
3549        check_macz_very_small_dataset,
3550        check_macz_empty_input,
3551        check_macz_invalid_a_constant,
3552        check_macz_invalid_b_constant,
3553        check_macz_invalid_gamma,
3554        check_macz_nan_handling,
3555        check_macz_streaming,
3556        check_macz_no_poison_kernel,
3557        check_macz_default_candles,
3558        check_macz_reinput,
3559        check_macz_no_poison
3560    );
3561
3562    macro_rules! gen_batch_tests {
3563        ($fn_name:ident) => {
3564            paste::paste! {
3565                #[test]
3566                fn [<$fn_name _scalar>]() {
3567                    let _ = $fn_name(stringify!([<$fn_name _scalar>]), Kernel::ScalarBatch);
3568                }
3569                #[cfg(all(feature="nightly-avx", target_arch="x86_64"))]
3570                #[test]
3571                fn [<$fn_name _avx2>]() {
3572                    let _ = $fn_name(stringify!([<$fn_name _avx2>]), Kernel::Avx2Batch);
3573                }
3574                #[cfg(all(feature="nightly-avx", target_arch="x86_64"))]
3575                #[test]
3576                fn [<$fn_name _avx512>]() {
3577                    let _ = $fn_name(stringify!([<$fn_name _avx512>]), Kernel::Avx512Batch);
3578                }
3579                #[test]
3580                fn [<$fn_name _auto_detect>]() {
3581                    let _ = $fn_name(stringify!([<$fn_name _auto_detect>]), Kernel::Auto);
3582                }
3583            }
3584        };
3585    }
3586
3587    gen_batch_tests!(check_batch_default_row);
3588    gen_batch_tests!(check_batch_sweep);
3589    gen_batch_tests!(check_batch_no_poison);
3590    gen_batch_tests!(check_batch_with_volume);
3591
3592    #[cfg(feature = "proptest")]
3593    fn check_macz_property(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
3594        use proptest::prelude::*;
3595        skip_if_unsupported!(kernel, test_name);
3596
3597        let strat = (
3598            prop::collection::vec(
3599                (-1e4f64..1e4f64).prop_filter("finite", |x| x.is_finite()),
3600                50..200,
3601            ),
3602            prop::collection::vec(
3603                (100f64..10000f64).prop_filter("positive", |x| x.is_finite() && *x > 0.0),
3604                50..200,
3605            ),
3606            5usize..15,
3607            15usize..30,
3608            3usize..10,
3609            10usize..25,
3610            15usize..30,
3611            -1.5f64..1.5f64,
3612            -1.5f64..1.5f64,
3613            prop::bool::ANY,
3614            0.01f64..0.1f64,
3615        );
3616
3617        proptest::test_runner::TestRunner::default().run(
3618            &strat,
3619            |(data, volume, fast, slow, sig, lz, lsd, a, b, use_lag, gamma)| {
3620                let min_len = data.len().min(volume.len());
3621                let data = &data[..min_len];
3622                let volume = &volume[..min_len];
3623
3624                if slow >= data.len() || lz >= data.len() || lsd >= data.len() {
3625                    return Ok(());
3626                }
3627
3628                let params = MaczParams {
3629                    fast_length: Some(fast),
3630                    slow_length: Some(slow),
3631                    signal_length: Some(sig),
3632                    lengthz: Some(lz),
3633                    length_stdev: Some(lsd),
3634                    a: Some(a),
3635                    b: Some(b),
3636                    use_lag: Some(use_lag),
3637                    gamma: Some(gamma),
3638                };
3639
3640                let input = MaczInput::from_slice_with_volume(data, volume, params);
3641
3642                let result = macz_with_kernel(&input, kernel);
3643                prop_assert!(
3644                    result.is_ok(),
3645                    "MAC-Z calculation failed: {:?}",
3646                    result.err()
3647                );
3648
3649                let output = result.unwrap();
3650                prop_assert_eq!(output.values.len(), data.len(), "Output length mismatch");
3651
3652                if kernel != Kernel::Scalar {
3653                    let scalar_result = macz_with_kernel(&input, Kernel::Scalar).unwrap();
3654
3655                    for (i, (&simd_val, &scalar_val)) in output
3656                        .values
3657                        .iter()
3658                        .zip(scalar_result.values.iter())
3659                        .enumerate()
3660                    {
3661                        if !simd_val.is_nan() && !scalar_val.is_nan() {
3662                            let diff = (simd_val - scalar_val).abs();
3663                            let tolerance = 1e-10 * scalar_val.abs().max(1.0);
3664                            prop_assert!(
3665                                diff < tolerance,
3666                                "[{}] Kernel mismatch at index {}: SIMD={}, Scalar={}, diff={}",
3667                                test_name,
3668                                i,
3669                                simd_val,
3670                                scalar_val,
3671                                diff
3672                            );
3673                        }
3674                    }
3675                }
3676
3677                Ok(())
3678            },
3679        )?;
3680
3681        Ok(())
3682    }
3683
3684    #[cfg(feature = "proptest")]
3685    generate_all_macz_tests!(check_macz_property);
3686
3687    #[test]
3688    fn test_macz_basic() {
3689        let data = vec![
3690            59243.26, 59234.77, 59223.21, 59265.62, 59397.48, 59499.99, 59564.95, 59686.73,
3691            59793.59, 59800.41, 59867.59, 59841.97, 59909.83, 60050.61, 60077.85, 60184.65,
3692            60255.36, 60317.44, 60278.45, 60210.49, 60304.89, 60394.25, 60353.87, 60470.57,
3693            60464.01, 60405.50, 60356.46, 60406.48, 60419.10, 60432.29, 60496.55, 60625.25,
3694            60609.84, 60718.37, 60641.10, 60619.52, 60646.73, 60713.42, 60609.51, 60598.68,
3695            60635.36, 60648.74, 60741.47, 60650.16, 60614.54, 60579.84, 60543.59, 60565.12,
3696            60522.53, 60460.89,
3697        ];
3698
3699        let params = MaczParams::default();
3700        let input = MaczInput::from_slice(&data, params);
3701        let result = macz(&input).unwrap();
3702
3703        let expected = vec![0.51988421, 0.23019592, 0.08030845, 0.12276454, -0.56402159];
3704        let actual = &result.values[result.values.len() - 5..];
3705
3706        println!("Last 5 actual values: {:?}", actual);
3707        println!("Expected values: {:?}", expected);
3708
3709        let warmup = 33;
3710        assert!(
3711            result.values[warmup..].iter().any(|&v| !v.is_nan()),
3712            "Should have non-NaN values after warmup"
3713        );
3714    }
3715
3716    #[test]
3717    fn test_macz_empty_input() {
3718        let params = MaczParams::default();
3719        let input = MaczInput::from_slice(&[], params);
3720        let result = macz(&input);
3721        assert!(result.is_err());
3722    }
3723
3724    #[test]
3725    fn test_macz_all_nan() {
3726        let data = vec![f64::NAN; 50];
3727        let params = MaczParams::default();
3728        let input = MaczInput::from_slice(&data, params);
3729        let result = macz(&input);
3730        assert!(result.is_err());
3731    }
3732
3733    #[test]
3734    fn test_macz_builder() {
3735        let data = [1.0, 2.0, 3.0, 4.0, 5.0].repeat(10);
3736
3737        let result = MaczBuilder::new()
3738            .fast_length(10)
3739            .slow_length(20)
3740            .signal_length(5)
3741            .kernel(Kernel::Scalar)
3742            .apply_slice(&data);
3743
3744        assert!(result.is_ok());
3745        let output = result.unwrap();
3746        assert_eq!(output.values.len(), data.len());
3747    }
3748
3749    #[test]
3750    fn test_macz_batch_builder() {
3751        let data = [1.0, 2.0, 3.0, 4.0, 5.0].repeat(10);
3752
3753        let result = MaczBatchBuilder::new()
3754            .fast_range(10, 12, 1)
3755            .slow_range(20, 22, 1)
3756            .signal_range(5, 6, 1)
3757            .kernel(Kernel::ScalarBatch)
3758            .apply_slice(&data);
3759
3760        assert!(result.is_ok());
3761        let output = result.unwrap();
3762        assert_eq!(output.cols, data.len());
3763        assert!(output.rows > 0);
3764    }
3765
3766    #[test]
3767    fn test_macz_into_slice() {
3768        let data: Vec<f64> = (0..50).map(|i| i as f64).collect();
3769        let mut dst = vec![0.0; 50];
3770        let params = MaczParams::default();
3771        let input = MaczInput::from_slice(&data, params);
3772
3773        let result = macz_into_slice(&mut dst, &input, Kernel::Scalar);
3774        assert!(result.is_ok(), "Error: {:?}", result);
3775    }
3776
3777    #[test]
3778    fn test_macz_batch_processing() {
3779        let data = [1.0, 2.0, 3.0, 4.0, 5.0].repeat(10);
3780
3781        let sweep = MaczBatchRange {
3782            fast_length: (10, 11, 1),
3783            slow_length: (20, 21, 1),
3784            signal_length: (5, 5, 1),
3785            lengthz: (15, 15, 1),
3786            length_stdev: (20, 20, 1),
3787            a: (1.0, 1.0, 0.1),
3788            b: (1.0, 1.0, 0.1),
3789        };
3790
3791        let result_seq = macz_batch_slice(&data, &sweep, Kernel::Scalar);
3792        assert!(result_seq.is_ok());
3793    }
3794
3795    #[test]
3796    fn test_macz_streaming() {
3797        let params = MaczParams {
3798            fast_length: Some(5),
3799            slow_length: Some(10),
3800            signal_length: Some(3),
3801            lengthz: Some(8),
3802            length_stdev: Some(10),
3803            a: Some(1.0),
3804            b: Some(1.0),
3805            use_lag: Some(false),
3806            gamma: Some(0.02),
3807        };
3808
3809        let mut stream = MaczStream::new(params).unwrap();
3810
3811        for i in 0..20 {
3812            let val = i as f64;
3813            let _ = stream.update(val, None);
3814        }
3815    }
3816
3817    #[test]
3818    fn test_macz_no_poison_legacy() {
3819        let data: Vec<f64> = (0..100).map(|i| 50.0 + (i as f64).sin() * 5.0).collect();
3820        let volume: Vec<f64> = (0..100).map(|i| 1000.0 + (i as f64) * 10.0).collect();
3821
3822        let params = MaczParams::default();
3823        let input_with_vol = MaczInput::from_slice_with_volume(&data, &volume, params);
3824        let result_vol = macz(&input_with_vol).unwrap();
3825
3826        let mut actual_warmup = 0;
3827        for (i, &val) in result_vol.values.iter().enumerate() {
3828            if !val.is_nan() {
3829                actual_warmup = i;
3830                break;
3831            }
3832        }
3833        println!("Actual warmup period: {}", actual_warmup);
3834
3835        assert!(actual_warmup >= 25, "Warmup should be at least slow period");
3836        assert!(actual_warmup < 50, "Warmup should be reasonable");
3837
3838        for i in 0..actual_warmup {
3839            assert!(
3840                result_vol.values[i].is_nan(),
3841                "Expected NaN at index {} during warmup, got {}",
3842                i,
3843                result_vol.values[i]
3844            );
3845        }
3846        for i in actual_warmup..result_vol.values.len().min(actual_warmup + 10) {
3847            assert!(
3848                !result_vol.values[i].is_nan(),
3849                "Expected non-NaN at index {} after warmup, got NaN",
3850                i
3851            );
3852        }
3853
3854        let mut dst = vec![123.456; data.len()];
3855        macz_into_slice(&mut dst, &input_with_vol, Kernel::Scalar).unwrap();
3856
3857        for i in 0..actual_warmup {
3858            assert!(
3859                dst[i].is_nan(),
3860                "into_slice should preserve NaN warmup at {}",
3861                i
3862            );
3863        }
3864
3865        for i in actual_warmup..dst.len() {
3866            assert!(
3867                (dst[i] - result_vol.values[i]).abs() < 1e-10,
3868                "into_slice result mismatch at index {}",
3869                i
3870            );
3871        }
3872
3873        let sweep = MaczBatchRange {
3874            fast_length: (12, 13, 1),
3875            slow_length: (25, 26, 1),
3876            signal_length: (9, 9, 1),
3877            lengthz: (20, 20, 1),
3878            length_stdev: (25, 25, 1),
3879            a: (1.0, 1.0, 0.1),
3880            b: (1.0, 1.0, 0.1),
3881        };
3882
3883        let batch_result = macz_batch_slice(&data, &sweep, Kernel::ScalarBatch).unwrap();
3884
3885        assert_eq!(batch_result.cols, data.len());
3886        assert!(batch_result.rows > 0);
3887
3888        let mut non_nan_count = 0;
3889        for val in &batch_result.values {
3890            if !val.is_nan() {
3891                non_nan_count += 1;
3892            }
3893        }
3894        assert!(non_nan_count > 0, "Batch processing produced all NaNs");
3895    }
3896
3897    #[cfg(debug_assertions)]
3898    #[test]
3899    fn check_macz_batch_no_poison() -> Result<(), Box<dyn std::error::Error>> {
3900        use crate::utilities::data_loader::read_candles_from_csv;
3901        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
3902        let c = read_candles_from_csv(file)?;
3903        let sweep = MaczBatchRange::default();
3904        let out = macz_batch_with_kernel(&c.close, &sweep, Kernel::ScalarBatch)?;
3905        for (idx, &v) in out.values.iter().enumerate() {
3906            if v.is_nan() {
3907                continue;
3908            }
3909            let bits = v.to_bits();
3910            assert_ne!(
3911                bits, 0x11111111_11111111,
3912                "alloc_with_nan_prefix poison at {idx}"
3913            );
3914            assert_ne!(
3915                bits, 0x22222222_22222222,
3916                "init_matrix_prefixes poison at {idx}"
3917            );
3918            assert_ne!(
3919                bits, 0x33333333_33333333,
3920                "make_uninit_matrix poison at {idx}"
3921            );
3922        }
3923        Ok(())
3924    }
3925}