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 fn classic() -> Self {
77 Self::new(12, 26, 9).expect("classic MACD periods are valid")
78 }
79
80 pub const fn periods(&self) -> (usize, usize, usize) {
82 (self.fast_period, self.slow_period, self.signal_period)
83 }
84
85 pub const fn value(&self) -> Option<MacdOutput> {
87 self.last
88 }
89
90 pub fn batch_macd(&mut self, inputs: &[f64]) -> Vec<f64> {
98 let mut out = vec![0.0; inputs.len() * 3];
99 self.batch_macd_into(inputs, &mut out);
100 out
101 }
102
103 pub fn batch_macd_fast_into(&mut self, inputs: &[f64], out: &mut [f64]) {
117 let n = inputs.len();
118 assert_eq!(
119 out.len(),
120 n * 3,
121 "batch_macd output must hold three values per input"
122 );
123 let (fp, sp, gp) = (self.fast_period, self.slow_period, self.signal_period);
124 if self.last.is_some()
125 || !self.fast.is_fresh()
126 || !self.slow.is_fresh()
127 || !self.signal_ema.is_fresh()
128 || n < sp + gp - 1
129 || !crate::fast::in_range(inputs)
130 {
131 self.batch_macd_into(inputs, out);
132 return;
133 }
134 let (fast, slow, signal) = wickra_simd::dispatch(crate::fast::MacdFast {
135 x: inputs,
136 periods: (fp, sp, gp),
137 alphas: (
138 self.fast.alpha(),
139 self.slow.alpha(),
140 self.signal_ema.alpha(),
141 ),
142 out,
143 _borrow: std::marker::PhantomData,
144 });
145 self.fast.seed_to(fast);
146 self.slow.seed_to(slow);
147 self.signal_ema.seed_to(signal);
148 let tail = &out[(n - 1) * 3..];
149 self.last = Some(MacdOutput {
150 macd: tail[0],
151 signal: tail[1],
152 histogram: tail[2],
153 });
154 }
155
156 pub fn batch_macd_fast(&mut self, inputs: &[f64]) -> Vec<f64> {
159 let mut out = vec![0.0; inputs.len() * 3];
160 self.batch_macd_fast_into(inputs, &mut out);
161 out
162 }
163
164 pub fn batch_macd_into(&mut self, inputs: &[f64], out: &mut [f64]) {
183 let n = inputs.len();
184 assert_eq!(
185 out.len(),
186 n * 3,
187 "batch_macd output must hold three values per input"
188 );
189 let (fp, sp, gp) = (self.fast_period, self.slow_period, self.signal_period);
190 if self.last.is_some()
193 || !self.fast.is_fresh()
194 || !self.slow.is_fresh()
195 || !self.signal_ema.is_fresh()
196 || n < sp + gp - 1
197 || !inputs.iter().all(|x| x.abs() <= 1e300)
198 {
199 for (row, &x) in out.chunks_exact_mut(3).zip(inputs) {
200 match self.update(x) {
201 Some(o) => row.copy_from_slice(&[o.macd, o.signal, o.histogram]),
202 None => row.fill(f64::NAN),
203 }
204 }
205 return;
206 }
207
208 let (fast_val, slow_val, sig) = wickra_simd::dispatch(FusedMacd {
209 inputs,
210 out,
211 periods: (fp, sp, gp),
212 alphas: (
213 self.fast.alpha(),
214 self.slow.alpha(),
215 self.signal_ema.alpha(),
216 ),
217 });
218
219 self.fast.seed_to(fast_val);
221 self.slow.seed_to(slow_val);
222 self.signal_ema.seed_to(sig);
223 let tail = &out[(n - 1) * 3..];
224 self.last = Some(MacdOutput {
225 macd: tail[0],
226 signal: tail[1],
227 histogram: tail[2],
228 });
229 }
230}
231
232struct FusedMacd<'a> {
237 inputs: &'a [f64],
238 out: &'a mut [f64],
239 periods: (usize, usize, usize),
240 alphas: (f64, f64, f64),
241}
242
243#[allow(clippy::inline_always)]
246impl wickra_simd::Kernel for FusedMacd<'_> {
247 type Output = (f64, f64, f64);
248
249 #[inline(always)]
250 fn run<S: wickra_simd::Simd>(self, _simd: S) -> (f64, f64, f64) {
251 let Self {
252 inputs,
253 out,
254 periods: (fp, sp, gp),
255 alphas: (fa, sa, ga),
256 } = self;
257 let (fo, so, go) = (1.0 - fa, 1.0 - sa, 1.0 - ga);
258 let first_full = sp + gp - 2;
259
260 out[..first_full * 3].fill(f64::NAN);
262
263 let mut fsum = -0.0_f64;
266 for &x in &inputs[..fp] {
267 fsum += x;
268 }
269 let mut fast_val = fsum / fp as f64;
270 let mut ssum = -0.0_f64;
271 for &x in &inputs[..sp] {
272 ssum += x;
273 }
274 for &x in &inputs[fp..sp] {
275 fast_val = fa.mul_add(x, fo * fast_val);
276 }
277 let mut slow_val = ssum / sp as f64;
278
279 let mut gsum = -0.0_f64 + (fast_val - slow_val);
282 for &x in &inputs[sp..=first_full] {
283 fast_val = fa.mul_add(x, fo * fast_val);
284 slow_val = sa.mul_add(x, so * slow_val);
285 gsum += fast_val - slow_val;
286 }
287 let mut sig = gsum / gp as f64;
288 let macd = fast_val - slow_val;
289 out[first_full * 3..first_full * 3 + 3].copy_from_slice(&[macd, sig, macd - sig]);
290
291 for (row, &x) in out[(first_full + 1) * 3..]
293 .chunks_exact_mut(3)
294 .zip(&inputs[first_full + 1..])
295 {
296 fast_val = fa.mul_add(x, fo * fast_val);
297 slow_val = sa.mul_add(x, so * slow_val);
298 let macd = fast_val - slow_val;
299 sig = ga.mul_add(macd, go * sig);
300 row.copy_from_slice(&[macd, sig, macd - sig]);
301 }
302 (fast_val, slow_val, sig)
303 }
304}
305
306impl Indicator for MacdIndicator {
307 type Input = f64;
308 type Output = MacdOutput;
309
310 #[inline]
311 fn update(&mut self, input: f64) -> Option<MacdOutput> {
312 if !input.is_finite() {
313 return None;
314 }
315
316 let fast = self.fast.update(input);
317 let slow = self.slow.update(input);
318
319 match (fast, slow) {
320 (Some(f), Some(s)) => {
321 let macd = f - s;
322 let signal = self.signal_ema.update(macd)?;
323 let out = MacdOutput {
324 macd,
325 signal,
326 histogram: macd - signal,
327 };
328 self.last = Some(out);
329 Some(out)
330 }
331 _ => None,
332 }
333 }
334
335 fn reset(&mut self) {
336 self.fast.reset();
337 self.slow.reset();
338 self.signal_ema.reset();
339 self.last = None;
340 }
341
342 #[inline]
343 fn warmup_period(&self) -> usize {
344 self.slow_period + self.signal_period - 1
346 }
347
348 #[inline]
349 fn is_ready(&self) -> bool {
350 self.last.is_some()
351 }
352
353 #[inline]
354 fn name(&self) -> &'static str {
355 "MACD"
356 }
357}
358
359#[cfg(test)]
360mod tests {
361 use super::*;
362 use crate::traits::BatchExt;
363 use approx::assert_relative_eq;
364
365 #[test]
366 fn rejects_fast_geq_slow() {
367 assert!(matches!(
368 MacdIndicator::new(26, 12, 9),
369 Err(Error::InvalidPeriod { .. })
370 ));
371 assert!(matches!(
372 MacdIndicator::new(12, 12, 9),
373 Err(Error::InvalidPeriod { .. })
374 ));
375 }
376
377 #[test]
381 fn accessors_and_metadata() {
382 let mut m = MacdIndicator::new(12, 26, 9).unwrap();
383 assert_eq!(m.periods(), (12, 26, 9));
384 assert_eq!(m.name(), "MACD");
385 assert!(m.value().is_none());
386 for i in 1..=m.warmup_period() {
387 m.update(100.0 + f64::from(u32::try_from(i).unwrap()));
388 }
389 assert!(m.value().is_some());
390 }
391
392 #[test]
393 fn rejects_zero_periods() {
394 assert!(matches!(
395 MacdIndicator::new(0, 26, 9),
396 Err(Error::PeriodZero)
397 ));
398 assert!(matches!(
399 MacdIndicator::new(12, 0, 9),
400 Err(Error::PeriodZero)
401 ));
402 assert!(matches!(
403 MacdIndicator::new(12, 26, 0),
404 Err(Error::PeriodZero)
405 ));
406 }
407
408 #[test]
409 fn first_emission_matches_warmup_period() {
410 let prices: Vec<f64> = (1..=60).map(f64::from).collect();
411 let mut macd = MacdIndicator::classic();
412 let out = macd.batch(&prices);
413 let warmup = macd.warmup_period();
414 for x in out.iter().take(warmup - 1) {
418 assert!(x.is_none(), "expected None within warmup");
419 }
420 assert!(
421 out[warmup - 1].is_some(),
422 "expected first emission at warmup_period - 1 ({warmup} idx)"
423 );
424 }
425
426 #[test]
427 fn histogram_equals_macd_minus_signal() {
428 let prices: Vec<f64> = (1..=80).map(|i| f64::from(i) * 0.5).collect();
429 let mut macd = MacdIndicator::classic();
430 for v in macd.batch(&prices).into_iter().flatten() {
431 assert_relative_eq!(v.histogram, v.macd - v.signal, epsilon = 1e-12);
432 }
433 }
434
435 #[test]
436 fn constant_series_yields_zero_macd_eventually() {
437 let mut macd = MacdIndicator::classic();
438 let out = macd.batch(&[100.0_f64; 200]);
439 let last = out.iter().rev().flatten().next().expect("emits a value");
441 assert_relative_eq!(last.macd, 0.0, epsilon = 1e-9);
442 assert_relative_eq!(last.signal, 0.0, epsilon = 1e-9);
443 assert_relative_eq!(last.histogram, 0.0, epsilon = 1e-9);
444 }
445
446 #[test]
447 fn rising_series_macd_positive_then_signal_catches_up() {
448 let prices: Vec<f64> = (1..=200).map(f64::from).collect();
449 let mut macd = MacdIndicator::classic();
450 let out = macd.batch(&prices);
451 let last = out.iter().rev().flatten().next().unwrap();
452 assert!(last.macd > 0.0, "rising series must yield positive MACD");
453 }
454
455 #[test]
456 fn batch_equals_streaming() {
457 let prices: Vec<f64> = (1..=100)
458 .map(|i| (f64::from(i) * 0.4).cos() * 10.0)
459 .collect();
460 let mut a = MacdIndicator::classic();
461 let mut b = MacdIndicator::classic();
462 assert_eq!(
463 a.batch(&prices),
464 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
465 );
466 }
467
468 #[test]
469 fn reset_clears_state() {
470 let mut macd = MacdIndicator::classic();
471 macd.batch(&(1..=80).map(f64::from).collect::<Vec<_>>());
472 assert!(macd.is_ready());
473 macd.reset();
474 assert!(!macd.is_ready());
475 assert_eq!(macd.update(1.0), None);
476 }
477
478 fn bits_eq(a: &[f64], b: &[f64]) -> bool {
479 a.len() == b.len()
480 && a.iter()
481 .zip(b)
482 .all(|(x, y)| x == y || (x.is_nan() && y.is_nan()))
483 }
484
485 fn macd_replay(series: &[f64]) -> Vec<f64> {
487 let mut m = MacdIndicator::classic();
488 let mut out = Vec::with_capacity(series.len() * 3);
489 for &x in series {
490 match m.update(x) {
491 Some(o) => out.extend_from_slice(&[o.macd, o.signal, o.histogram]),
492 None => out.extend_from_slice(&[f64::NAN; 3]),
493 }
494 }
495 out
496 }
497
498 #[test]
499 fn batch_macd_fast_path_is_bit_identical() {
500 let series: Vec<f64> = (0..300)
501 .map(|i| (f64::from(i) * 0.4).cos() * 10.0 + 100.0)
502 .collect();
503 let mut macd = MacdIndicator::classic();
504 let got = macd.batch_macd(&series);
505 assert!(bits_eq(&got, &macd_replay(&series)));
506 let mut ref_macd = MacdIndicator::classic();
508 for &x in &series {
509 ref_macd.update(x);
510 }
511 let (a, b) = (macd.update(101.0), ref_macd.update(101.0));
512 assert_eq!(a.is_some(), b.is_some());
513 assert_relative_eq!(a.unwrap().macd, b.unwrap().macd, epsilon = 1e-12);
514 }
515
516 fn to_bits(v: &[f64]) -> Vec<u64> {
519 v.iter().map(|x| x.to_bits()).collect()
520 }
521
522 #[test]
526 fn fused_kernel_is_identical_on_every_dispatch_path() {
527 let series: Vec<f64> = (0..3000)
528 .map(|i| (f64::from(i) * 0.093).sin() * 7.0 + f64::from(i % 13) * 0.4 + 80.0)
529 .collect();
530 let alphas = (2.0 / 13.0, 2.0 / 27.0, 2.0 / 10.0);
531 let mut dispatched = vec![0.0; series.len() * 3];
532 let mut baseline = vec![0.0; series.len() * 3];
533 let a = wickra_simd::dispatch(FusedMacd {
534 inputs: &series,
535 out: &mut dispatched,
536 periods: (12, 26, 9),
537 alphas,
538 });
539 let b = wickra_simd::run_baseline(FusedMacd {
540 inputs: &series,
541 out: &mut baseline,
542 periods: (12, 26, 9),
543 alphas,
544 });
545 assert_eq!(to_bits(&dispatched), to_bits(&baseline));
546 assert_eq!(
547 [a.0.to_bits(), a.1.to_bits(), a.2.to_bits()],
548 [b.0.to_bits(), b.1.to_bits(), b.2.to_bits()]
549 );
550 }
551
552 #[test]
555 fn batch_macd_negative_zero_series_matches_to_the_bit() {
556 let series = vec![-0.0_f64; 60];
557 let got = MacdIndicator::classic().batch_macd(&series);
558 assert_eq!(to_bits(&got), to_bits(&macd_replay(&series)));
559 }
560
561 #[test]
564 fn batch_macd_into_overwrites_a_dirty_buffer() {
565 let series: Vec<f64> = (0..200)
566 .map(|i| (f64::from(i) * 0.21).sin() * 3.0 + 50.0)
567 .collect();
568 let mut out = vec![9.0; series.len() * 3];
569 MacdIndicator::classic().batch_macd_into(&series, &mut out);
570 assert_eq!(to_bits(&out), to_bits(&macd_replay(&series)));
571 }
572
573 #[test]
576 fn batch_macd_with_signal_period_one_matches_replay() {
577 let series: Vec<f64> = (0..80).map(|i| f64::from(i % 9) * 1.25 + 30.0).collect();
578 let mut fused = MacdIndicator::new(3, 7, 1).unwrap();
579 let mut replay = MacdIndicator::new(3, 7, 1).unwrap();
580 let want: Vec<f64> = series
581 .iter()
582 .flat_map(|&x| match replay.update(x) {
583 Some(o) => [o.macd, o.signal, o.histogram],
584 None => [f64::NAN; 3],
585 })
586 .collect();
587 assert_eq!(to_bits(&fused.batch_macd(&series)), to_bits(&want));
588 }
589
590 #[test]
594 fn batch_macd_hands_huge_values_to_the_replay() {
595 let mut series: Vec<f64> = (0..60).map(|i| f64::from(i) + 100.0).collect();
596 series[45] = 1.7e308;
597 series[46] = -1.7e308;
598 let got = MacdIndicator::classic().batch_macd(&series);
599 assert!(bits_eq(&got, &macd_replay(&series)));
600 }
601
602 #[test]
603 #[should_panic(expected = "batch_macd output must hold three values per input")]
604 fn batch_macd_into_rejects_a_short_buffer() {
605 let mut out = vec![0.0; 5];
606 MacdIndicator::classic().batch_macd_into(&[1.0, 2.0], &mut out);
607 }
608
609 #[test]
610 fn batch_macd_falls_back_on_non_finite() {
611 let mut series: Vec<f64> = (0..60).map(|i| f64::from(i) + 100.0).collect();
612 series[40] = f64::NAN;
613 let mut macd = MacdIndicator::classic();
614 assert!(bits_eq(&macd.batch_macd(&series), &macd_replay(&series)));
615 }
616
617 #[test]
618 fn batch_macd_falls_back_when_not_fresh() {
619 let series: Vec<f64> = (0..60).map(|i| f64::from(i) + 100.0).collect();
620 let mut macd = MacdIndicator::classic();
621 macd.update(50.0);
622 let mut ref_macd = MacdIndicator::classic();
623 ref_macd.update(50.0);
624 let mut want = Vec::new();
625 for &x in &series {
626 match ref_macd.update(x) {
627 Some(o) => want.extend_from_slice(&[o.macd, o.signal, o.histogram]),
628 None => want.extend_from_slice(&[f64::NAN; 3]),
629 }
630 }
631 assert!(bits_eq(&macd.batch_macd(&series), &want));
632 }
633
634 #[test]
635 fn batch_macd_too_short_for_output_falls_back() {
636 let series: Vec<f64> = (0..20).map(|i| f64::from(i) + 100.0).collect();
638 let mut macd = MacdIndicator::classic();
639 let got = macd.batch_macd(&series);
640 assert!(bits_eq(&got, &macd_replay(&series)));
641 assert!(got.iter().all(|x| x.is_nan()));
642 }
643
644 #[test]
645 fn ignores_non_finite_input() {
646 let mut macd = MacdIndicator::classic();
647 macd.batch(&(1..=80).map(f64::from).collect::<Vec<_>>());
648 let before = macd.value();
649 assert!(before.is_some());
650 assert_eq!(macd.update(f64::NAN), None);
652 assert_eq!(macd.update(f64::INFINITY), None);
653 assert_eq!(macd.value(), before);
654 }
655}