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, ¶ms)
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}