1use crate::error::{Error, Result};
4use crate::indicators::ema::Ema;
5use crate::traits::Indicator;
6
7#[derive(Debug, Clone, Copy, PartialEq)]
9pub struct MacdOutput {
10 pub macd: f64,
12 pub signal: f64,
14 pub histogram: f64,
16}
17
18#[derive(Debug, Clone)]
38pub struct MacdIndicator {
39 fast: Ema,
40 slow: Ema,
41 signal_ema: Ema,
42 fast_period: usize,
43 slow_period: usize,
44 signal_period: usize,
45 last: Option<MacdOutput>,
46}
47
48impl MacdIndicator {
49 pub fn new(fast: usize, slow: usize, signal: usize) -> Result<Self> {
56 if fast == 0 || slow == 0 || signal == 0 {
57 return Err(Error::PeriodZero);
58 }
59 if fast >= slow {
60 return Err(Error::InvalidPeriod {
61 message: "fast period must be strictly less than slow period",
62 });
63 }
64 Ok(Self {
65 fast: Ema::new(fast)?,
66 slow: Ema::new(slow)?,
67 signal_ema: Ema::new(signal)?,
68 fast_period: fast,
69 slow_period: slow,
70 signal_period: signal,
71 last: None,
72 })
73 }
74
75 pub(crate) fn fixed_12_26(signal: usize) -> Result<Self> {
79 if signal == 0 {
80 return Err(Error::PeriodZero);
81 }
82 Ok(Self {
83 fast: Ema::with_period_and_alpha(12, 0.15),
84 slow: Ema::with_period_and_alpha(26, 0.075),
85 signal_ema: Ema::new(signal)?,
86 fast_period: 12,
87 slow_period: 26,
88 signal_period: signal,
89 last: None,
90 })
91 }
92
93 pub fn classic() -> Self {
95 Self::new(12, 26, 9).expect("classic MACD periods are valid")
96 }
97
98 pub const fn periods(&self) -> (usize, usize, usize) {
100 (self.fast_period, self.slow_period, self.signal_period)
101 }
102
103 pub const fn value(&self) -> Option<MacdOutput> {
105 self.last
106 }
107
108 pub fn batch_macd(&mut self, inputs: &[f64]) -> Vec<f64> {
116 let mut out = vec![0.0; inputs.len() * 3];
117 self.batch_macd_into(inputs, &mut out);
118 out
119 }
120
121 pub fn batch_macd_fast_into(&mut self, inputs: &[f64], out: &mut [f64]) {
135 let n = inputs.len();
136 assert_eq!(
137 out.len(),
138 n * 3,
139 "batch_macd output must hold three values per input"
140 );
141 let (fp, sp, gp) = (self.fast_period, self.slow_period, self.signal_period);
142 if self.last.is_some()
143 || !self.fast.is_fresh()
144 || !self.slow.is_fresh()
145 || !self.signal_ema.is_fresh()
146 || n < sp + gp - 1
147 || !crate::fast::in_range(inputs)
148 {
149 self.batch_macd_into(inputs, out);
150 return;
151 }
152 let (fast, slow, signal) = wickra_simd::dispatch(crate::fast::MacdFast {
153 x: inputs,
154 periods: (fp, sp, gp),
155 alphas: (
156 self.fast.alpha(),
157 self.slow.alpha(),
158 self.signal_ema.alpha(),
159 ),
160 out,
161 _borrow: std::marker::PhantomData,
162 });
163 self.fast.seed_to(fast);
164 self.slow.seed_to(slow);
165 self.signal_ema.seed_to(signal);
166 let tail = &out[(n - 1) * 3..];
167 self.last = Some(MacdOutput {
168 macd: tail[0],
169 signal: tail[1],
170 histogram: tail[2],
171 });
172 }
173
174 pub fn batch_macd_fast(&mut self, inputs: &[f64]) -> Vec<f64> {
177 let mut out = vec![0.0; inputs.len() * 3];
178 self.batch_macd_fast_into(inputs, &mut out);
179 out
180 }
181
182 pub fn batch_macd_into(&mut self, inputs: &[f64], out: &mut [f64]) {
201 let n = inputs.len();
202 assert_eq!(
203 out.len(),
204 n * 3,
205 "batch_macd output must hold three values per input"
206 );
207 let (fp, sp, gp) = (self.fast_period, self.slow_period, self.signal_period);
208 if self.last.is_some()
211 || !self.fast.is_fresh()
212 || !self.slow.is_fresh()
213 || !self.signal_ema.is_fresh()
214 || n < sp + gp - 1
215 || !inputs.iter().all(|x| x.abs() <= 1e300)
216 {
217 for (row, &x) in out.chunks_exact_mut(3).zip(inputs) {
218 match self.update(x) {
219 Some(o) => row.copy_from_slice(&[o.macd, o.signal, o.histogram]),
220 None => row.fill(f64::NAN),
221 }
222 }
223 return;
224 }
225
226 let (fast_val, slow_val, sig) = wickra_simd::dispatch(FusedMacd {
227 inputs,
228 out,
229 periods: (fp, sp, gp),
230 alphas: (
231 self.fast.alpha(),
232 self.slow.alpha(),
233 self.signal_ema.alpha(),
234 ),
235 });
236
237 self.fast.seed_to(fast_val);
239 self.slow.seed_to(slow_val);
240 self.signal_ema.seed_to(sig);
241 let tail = &out[(n - 1) * 3..];
242 self.last = Some(MacdOutput {
243 macd: tail[0],
244 signal: tail[1],
245 histogram: tail[2],
246 });
247 }
248}
249
250struct FusedMacd<'a> {
255 inputs: &'a [f64],
256 out: &'a mut [f64],
257 periods: (usize, usize, usize),
258 alphas: (f64, f64, f64),
259}
260
261#[allow(clippy::inline_always)]
264impl wickra_simd::Kernel for FusedMacd<'_> {
265 type Output = (f64, f64, f64);
266
267 #[inline(always)]
268 fn run<S: wickra_simd::Simd>(self, _simd: S) -> (f64, f64, f64) {
269 let Self {
270 inputs,
271 out,
272 periods: (fp, sp, gp),
273 alphas: (fa, sa, ga),
274 } = self;
275 let (fo, so, go) = (1.0 - fa, 1.0 - sa, 1.0 - ga);
276 let first_full = sp + gp - 2;
277
278 out[..first_full * 3].fill(f64::NAN);
280
281 let mut fsum = -0.0_f64;
284 for &x in &inputs[..fp] {
285 fsum += x;
286 }
287 let mut fast_val = fsum / fp as f64;
288 let mut ssum = -0.0_f64;
289 for &x in &inputs[..sp] {
290 ssum += x;
291 }
292 for &x in &inputs[fp..sp] {
293 fast_val = fa.mul_add(x, fo * fast_val);
294 }
295 let mut slow_val = ssum / sp as f64;
296
297 let mut gsum = -0.0_f64 + (fast_val - slow_val);
300 for &x in &inputs[sp..=first_full] {
301 fast_val = fa.mul_add(x, fo * fast_val);
302 slow_val = sa.mul_add(x, so * slow_val);
303 gsum += fast_val - slow_val;
304 }
305 let mut sig = gsum / gp as f64;
306 let macd = fast_val - slow_val;
307 out[first_full * 3..first_full * 3 + 3].copy_from_slice(&[macd, sig, macd - sig]);
308
309 for (row, &x) in out[(first_full + 1) * 3..]
311 .chunks_exact_mut(3)
312 .zip(&inputs[first_full + 1..])
313 {
314 fast_val = fa.mul_add(x, fo * fast_val);
315 slow_val = sa.mul_add(x, so * slow_val);
316 let macd = fast_val - slow_val;
317 sig = ga.mul_add(macd, go * sig);
318 row.copy_from_slice(&[macd, sig, macd - sig]);
319 }
320 (fast_val, slow_val, sig)
321 }
322}
323
324impl Indicator for MacdIndicator {
325 type Input = f64;
326 type Output = MacdOutput;
327
328 #[inline]
329 fn update(&mut self, input: f64) -> Option<MacdOutput> {
330 if !input.is_finite() {
331 return None;
332 }
333
334 let fast = self.fast.update(input);
335 let slow = self.slow.update(input);
336
337 match (fast, slow) {
338 (Some(f), Some(s)) => {
339 let macd = f - s;
340 let signal = self.signal_ema.update(macd)?;
341 let out = MacdOutput {
342 macd,
343 signal,
344 histogram: macd - signal,
345 };
346 self.last = Some(out);
347 Some(out)
348 }
349 _ => None,
350 }
351 }
352
353 fn reset(&mut self) {
354 self.fast.reset();
355 self.slow.reset();
356 self.signal_ema.reset();
357 self.last = None;
358 }
359
360 #[inline]
361 fn warmup_period(&self) -> usize {
362 self.slow_period + self.signal_period - 1
364 }
365
366 #[inline]
367 fn is_ready(&self) -> bool {
368 self.last.is_some()
369 }
370
371 #[inline]
372 fn name(&self) -> &'static str {
373 "MACD"
374 }
375}
376
377#[cfg(test)]
378mod tests {
379 use super::*;
380 use crate::traits::BatchExt;
381 use approx::assert_relative_eq;
382
383 #[test]
384 fn rejects_fast_geq_slow() {
385 assert!(matches!(
386 MacdIndicator::new(26, 12, 9),
387 Err(Error::InvalidPeriod { .. })
388 ));
389 assert!(matches!(
390 MacdIndicator::new(12, 12, 9),
391 Err(Error::InvalidPeriod { .. })
392 ));
393 }
394
395 #[test]
399 fn accessors_and_metadata() {
400 let mut m = MacdIndicator::new(12, 26, 9).unwrap();
401 assert_eq!(m.periods(), (12, 26, 9));
402 assert_eq!(m.name(), "MACD");
403 assert!(m.value().is_none());
404 for i in 1..=m.warmup_period() {
405 m.update(100.0 + f64::from(u32::try_from(i).unwrap()));
406 }
407 assert!(m.value().is_some());
408 }
409
410 #[test]
411 fn rejects_zero_periods() {
412 assert!(matches!(
413 MacdIndicator::new(0, 26, 9),
414 Err(Error::PeriodZero)
415 ));
416 assert!(matches!(
417 MacdIndicator::new(12, 0, 9),
418 Err(Error::PeriodZero)
419 ));
420 assert!(matches!(
421 MacdIndicator::new(12, 26, 0),
422 Err(Error::PeriodZero)
423 ));
424 }
425
426 #[test]
427 fn first_emission_matches_warmup_period() {
428 let prices: Vec<f64> = (1..=60).map(f64::from).collect();
429 let mut macd = MacdIndicator::classic();
430 let out = macd.batch(&prices);
431 let warmup = macd.warmup_period();
432 for x in out.iter().take(warmup - 1) {
436 assert!(x.is_none(), "expected None within warmup");
437 }
438 assert!(
439 out[warmup - 1].is_some(),
440 "expected first emission at warmup_period - 1 ({warmup} idx)"
441 );
442 }
443
444 #[test]
445 fn histogram_equals_macd_minus_signal() {
446 let prices: Vec<f64> = (1..=80).map(|i| f64::from(i) * 0.5).collect();
447 let mut macd = MacdIndicator::classic();
448 for v in macd.batch(&prices).into_iter().flatten() {
449 assert_relative_eq!(v.histogram, v.macd - v.signal, epsilon = 1e-12);
450 }
451 }
452
453 #[test]
454 fn constant_series_yields_zero_macd_eventually() {
455 let mut macd = MacdIndicator::classic();
456 let out = macd.batch(&[100.0_f64; 200]);
457 let last = out.iter().rev().flatten().next().expect("emits a value");
459 assert_relative_eq!(last.macd, 0.0, epsilon = 1e-9);
460 assert_relative_eq!(last.signal, 0.0, epsilon = 1e-9);
461 assert_relative_eq!(last.histogram, 0.0, epsilon = 1e-9);
462 }
463
464 #[test]
465 fn rising_series_macd_positive_then_signal_catches_up() {
466 let prices: Vec<f64> = (1..=200).map(f64::from).collect();
467 let mut macd = MacdIndicator::classic();
468 let out = macd.batch(&prices);
469 let last = out.iter().rev().flatten().next().unwrap();
470 assert!(last.macd > 0.0, "rising series must yield positive MACD");
471 }
472
473 #[test]
474 fn batch_equals_streaming() {
475 let prices: Vec<f64> = (1..=100)
476 .map(|i| (f64::from(i) * 0.4).cos() * 10.0)
477 .collect();
478 let mut a = MacdIndicator::classic();
479 let mut b = MacdIndicator::classic();
480 assert_eq!(
481 a.batch(&prices),
482 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
483 );
484 }
485
486 #[test]
487 fn reset_clears_state() {
488 let mut macd = MacdIndicator::classic();
489 macd.batch(&(1..=80).map(f64::from).collect::<Vec<_>>());
490 assert!(macd.is_ready());
491 macd.reset();
492 assert!(!macd.is_ready());
493 assert_eq!(macd.update(1.0), None);
494 }
495
496 fn bits_eq(a: &[f64], b: &[f64]) -> bool {
497 a.len() == b.len()
498 && a.iter()
499 .zip(b)
500 .all(|(x, y)| x == y || (x.is_nan() && y.is_nan()))
501 }
502
503 fn macd_replay(series: &[f64]) -> Vec<f64> {
505 let mut m = MacdIndicator::classic();
506 let mut out = Vec::with_capacity(series.len() * 3);
507 for &x in series {
508 match m.update(x) {
509 Some(o) => out.extend_from_slice(&[o.macd, o.signal, o.histogram]),
510 None => out.extend_from_slice(&[f64::NAN; 3]),
511 }
512 }
513 out
514 }
515
516 #[test]
517 fn batch_macd_fast_path_is_bit_identical() {
518 let series: Vec<f64> = (0..300)
519 .map(|i| (f64::from(i) * 0.4).cos() * 10.0 + 100.0)
520 .collect();
521 let mut macd = MacdIndicator::classic();
522 let got = macd.batch_macd(&series);
523 assert!(bits_eq(&got, &macd_replay(&series)));
524 let mut ref_macd = MacdIndicator::classic();
526 for &x in &series {
527 ref_macd.update(x);
528 }
529 let (a, b) = (macd.update(101.0), ref_macd.update(101.0));
530 assert_eq!(a.is_some(), b.is_some());
531 assert_relative_eq!(a.unwrap().macd, b.unwrap().macd, epsilon = 1e-12);
532 }
533
534 fn to_bits(v: &[f64]) -> Vec<u64> {
537 v.iter().map(|x| x.to_bits()).collect()
538 }
539
540 #[test]
544 fn fused_kernel_is_identical_on_every_dispatch_path() {
545 let series: Vec<f64> = (0..3000)
546 .map(|i| (f64::from(i) * 0.093).sin() * 7.0 + f64::from(i % 13) * 0.4 + 80.0)
547 .collect();
548 let alphas = (2.0 / 13.0, 2.0 / 27.0, 2.0 / 10.0);
549 let mut dispatched = vec![0.0; series.len() * 3];
550 let mut baseline = vec![0.0; series.len() * 3];
551 let a = wickra_simd::dispatch(FusedMacd {
552 inputs: &series,
553 out: &mut dispatched,
554 periods: (12, 26, 9),
555 alphas,
556 });
557 let b = wickra_simd::run_baseline(FusedMacd {
558 inputs: &series,
559 out: &mut baseline,
560 periods: (12, 26, 9),
561 alphas,
562 });
563 assert_eq!(to_bits(&dispatched), to_bits(&baseline));
564 assert_eq!(
565 [a.0.to_bits(), a.1.to_bits(), a.2.to_bits()],
566 [b.0.to_bits(), b.1.to_bits(), b.2.to_bits()]
567 );
568 }
569
570 #[test]
573 fn batch_macd_negative_zero_series_matches_to_the_bit() {
574 let series = vec![-0.0_f64; 60];
575 let got = MacdIndicator::classic().batch_macd(&series);
576 assert_eq!(to_bits(&got), to_bits(&macd_replay(&series)));
577 }
578
579 #[test]
582 fn batch_macd_into_overwrites_a_dirty_buffer() {
583 let series: Vec<f64> = (0..200)
584 .map(|i| (f64::from(i) * 0.21).sin() * 3.0 + 50.0)
585 .collect();
586 let mut out = vec![9.0; series.len() * 3];
587 MacdIndicator::classic().batch_macd_into(&series, &mut out);
588 assert_eq!(to_bits(&out), to_bits(&macd_replay(&series)));
589 }
590
591 #[test]
594 fn batch_macd_with_signal_period_one_matches_replay() {
595 let series: Vec<f64> = (0..80).map(|i| f64::from(i % 9) * 1.25 + 30.0).collect();
596 let mut fused = MacdIndicator::new(3, 7, 1).unwrap();
597 let mut replay = MacdIndicator::new(3, 7, 1).unwrap();
598 let want: Vec<f64> = series
599 .iter()
600 .flat_map(|&x| match replay.update(x) {
601 Some(o) => [o.macd, o.signal, o.histogram],
602 None => [f64::NAN; 3],
603 })
604 .collect();
605 assert_eq!(to_bits(&fused.batch_macd(&series)), to_bits(&want));
606 }
607
608 #[test]
612 fn batch_macd_hands_huge_values_to_the_replay() {
613 let mut series: Vec<f64> = (0..60).map(|i| f64::from(i) + 100.0).collect();
614 series[45] = 1.7e308;
615 series[46] = -1.7e308;
616 let got = MacdIndicator::classic().batch_macd(&series);
617 assert!(bits_eq(&got, &macd_replay(&series)));
618 }
619
620 #[test]
621 #[should_panic(expected = "batch_macd output must hold three values per input")]
622 fn batch_macd_into_rejects_a_short_buffer() {
623 let mut out = vec![0.0; 5];
624 MacdIndicator::classic().batch_macd_into(&[1.0, 2.0], &mut out);
625 }
626
627 #[test]
628 fn batch_macd_falls_back_on_non_finite() {
629 let mut series: Vec<f64> = (0..60).map(|i| f64::from(i) + 100.0).collect();
630 series[40] = f64::NAN;
631 let mut macd = MacdIndicator::classic();
632 assert!(bits_eq(&macd.batch_macd(&series), &macd_replay(&series)));
633 }
634
635 #[test]
636 fn batch_macd_falls_back_when_not_fresh() {
637 let series: Vec<f64> = (0..60).map(|i| f64::from(i) + 100.0).collect();
638 let mut macd = MacdIndicator::classic();
639 macd.update(50.0);
640 let mut ref_macd = MacdIndicator::classic();
641 ref_macd.update(50.0);
642 let mut want = Vec::new();
643 for &x in &series {
644 match ref_macd.update(x) {
645 Some(o) => want.extend_from_slice(&[o.macd, o.signal, o.histogram]),
646 None => want.extend_from_slice(&[f64::NAN; 3]),
647 }
648 }
649 assert!(bits_eq(&macd.batch_macd(&series), &want));
650 }
651
652 #[test]
653 fn batch_macd_too_short_for_output_falls_back() {
654 let series: Vec<f64> = (0..20).map(|i| f64::from(i) + 100.0).collect();
656 let mut macd = MacdIndicator::classic();
657 let got = macd.batch_macd(&series);
658 assert!(bits_eq(&got, &macd_replay(&series)));
659 assert!(got.iter().all(|x| x.is_nan()));
660 }
661
662 #[test]
663 fn ignores_non_finite_input() {
664 let mut macd = MacdIndicator::classic();
665 macd.batch(&(1..=80).map(f64::from).collect::<Vec<_>>());
666 let before = macd.value();
667 assert!(before.is_some());
668 assert_eq!(macd.update(f64::NAN), None);
670 assert_eq!(macd.update(f64::INFINITY), None);
671 assert_eq!(macd.value(), before);
672 }
673
674 #[test]
675 fn fixed_12_26_rejects_invalid_signal() {
676 assert!(matches!(
677 MacdIndicator::fixed_12_26(0),
678 Err(Error::PeriodZero)
679 ));
680 let too_big = crate::error::MAX_PERIOD + 1;
681 assert!(matches!(
682 MacdIndicator::fixed_12_26(too_big),
683 Err(Error::InvalidPeriod { .. })
684 ));
685 }
686
687 #[test]
688 fn fixed_12_26_configuration() {
689 let m = MacdIndicator::fixed_12_26(9).unwrap();
690 assert_eq!(m.periods(), (12, 26, 9));
691 assert_eq!(m.warmup_period(), 34);
692 assert_eq!(m.fast.alpha().to_bits(), 0.15_f64.to_bits());
693 assert_eq!(m.slow.alpha().to_bits(), 0.075_f64.to_bits());
694 assert_eq!(m.signal_ema.alpha().to_bits(), (2.0_f64 / 10.0).to_bits());
695 assert!(!m.is_ready());
696 }
697
698 #[test]
699 fn fixed_12_26_hand_computed_step() {
700 let mut m = MacdIndicator::fixed_12_26(1).unwrap();
707 let mut prices = vec![100.0; 26];
708 prices.extend_from_slice(&[110.0, 110.0]);
709 let out = m.batch(&prices);
710 assert!(out[..25].iter().all(Option::is_none));
711 assert_relative_eq!(out[25].unwrap().macd, 0.0, epsilon = 1e-12);
712 let o26 = out[26].unwrap();
713 assert_relative_eq!(o26.macd, 0.75, epsilon = 1e-12);
714 assert_relative_eq!(o26.signal, 0.75, epsilon = 1e-12);
715 assert_relative_eq!(o26.histogram, 0.0, epsilon = 1e-12);
716 assert_relative_eq!(out[27].unwrap().macd, 1.331_25, epsilon = 1e-12);
717 }
718}