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