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