use crate::error::{Error, Result};
use crate::indicators::ema::Ema;
use crate::traits::Indicator;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MacdOutput {
pub macd: f64,
pub signal: f64,
pub histogram: f64,
}
#[derive(Debug, Clone)]
pub struct MacdIndicator {
fast: Ema,
slow: Ema,
signal_ema: Ema,
fast_period: usize,
slow_period: usize,
signal_period: usize,
last: Option<MacdOutput>,
}
impl MacdIndicator {
pub fn new(fast: usize, slow: usize, signal: usize) -> Result<Self> {
if fast == 0 || slow == 0 || signal == 0 {
return Err(Error::PeriodZero);
}
if fast >= slow {
return Err(Error::InvalidPeriod {
message: "fast period must be strictly less than slow period",
});
}
Ok(Self {
fast: Ema::new(fast)?,
slow: Ema::new(slow)?,
signal_ema: Ema::new(signal)?,
fast_period: fast,
slow_period: slow,
signal_period: signal,
last: None,
})
}
pub(crate) fn fixed_12_26(signal: usize) -> Result<Self> {
if signal == 0 {
return Err(Error::PeriodZero);
}
Ok(Self {
fast: Ema::with_period_and_alpha(12, 0.15),
slow: Ema::with_period_and_alpha(26, 0.075),
signal_ema: Ema::new(signal)?,
fast_period: 12,
slow_period: 26,
signal_period: signal,
last: None,
})
}
pub fn classic() -> Self {
Self::new(12, 26, 9).expect("classic MACD periods are valid")
}
pub const fn periods(&self) -> (usize, usize, usize) {
(self.fast_period, self.slow_period, self.signal_period)
}
pub const fn value(&self) -> Option<MacdOutput> {
self.last
}
pub fn batch_macd(&mut self, inputs: &[f64]) -> Vec<f64> {
let mut out = vec![0.0; inputs.len() * 3];
self.batch_macd_into(inputs, &mut out);
out
}
pub fn batch_macd_fast_into(&mut self, inputs: &[f64], out: &mut [f64]) {
let n = inputs.len();
assert_eq!(
out.len(),
n * 3,
"batch_macd output must hold three values per input"
);
let (fp, sp, gp) = (self.fast_period, self.slow_period, self.signal_period);
if self.last.is_some()
|| !self.fast.is_fresh()
|| !self.slow.is_fresh()
|| !self.signal_ema.is_fresh()
|| n < sp + gp - 1
|| !crate::fast::in_range(inputs)
{
self.batch_macd_into(inputs, out);
return;
}
let (fast, slow, signal) = wickra_simd::dispatch(crate::fast::MacdFast {
x: inputs,
periods: (fp, sp, gp),
alphas: (
self.fast.alpha(),
self.slow.alpha(),
self.signal_ema.alpha(),
),
out,
_borrow: std::marker::PhantomData,
});
self.fast.seed_to(fast);
self.slow.seed_to(slow);
self.signal_ema.seed_to(signal);
let tail = &out[(n - 1) * 3..];
self.last = Some(MacdOutput {
macd: tail[0],
signal: tail[1],
histogram: tail[2],
});
}
pub fn batch_macd_fast(&mut self, inputs: &[f64]) -> Vec<f64> {
let mut out = vec![0.0; inputs.len() * 3];
self.batch_macd_fast_into(inputs, &mut out);
out
}
pub fn batch_macd_into(&mut self, inputs: &[f64], out: &mut [f64]) {
let n = inputs.len();
assert_eq!(
out.len(),
n * 3,
"batch_macd output must hold three values per input"
);
let (fp, sp, gp) = (self.fast_period, self.slow_period, self.signal_period);
if self.last.is_some()
|| !self.fast.is_fresh()
|| !self.slow.is_fresh()
|| !self.signal_ema.is_fresh()
|| n < sp + gp - 1
|| !inputs.iter().all(|x| x.abs() <= 1e300)
{
for (row, &x) in out.chunks_exact_mut(3).zip(inputs) {
match self.update(x) {
Some(o) => row.copy_from_slice(&[o.macd, o.signal, o.histogram]),
None => row.fill(f64::NAN),
}
}
return;
}
let (fast_val, slow_val, sig) = wickra_simd::dispatch(FusedMacd {
inputs,
out,
periods: (fp, sp, gp),
alphas: (
self.fast.alpha(),
self.slow.alpha(),
self.signal_ema.alpha(),
),
});
self.fast.seed_to(fast_val);
self.slow.seed_to(slow_val);
self.signal_ema.seed_to(sig);
let tail = &out[(n - 1) * 3..];
self.last = Some(MacdOutput {
macd: tail[0],
signal: tail[1],
histogram: tail[2],
});
}
}
struct FusedMacd<'a> {
inputs: &'a [f64],
out: &'a mut [f64],
periods: (usize, usize, usize),
alphas: (f64, f64, f64),
}
#[allow(clippy::inline_always)]
impl wickra_simd::Kernel for FusedMacd<'_> {
type Output = (f64, f64, f64);
#[inline(always)]
fn run<S: wickra_simd::Simd>(self, _simd: S) -> (f64, f64, f64) {
let Self {
inputs,
out,
periods: (fp, sp, gp),
alphas: (fa, sa, ga),
} = self;
let (fo, so, go) = (1.0 - fa, 1.0 - sa, 1.0 - ga);
let first_full = sp + gp - 2;
out[..first_full * 3].fill(f64::NAN);
let mut fsum = -0.0_f64;
for &x in &inputs[..fp] {
fsum += x;
}
let mut fast_val = fsum / fp as f64;
let mut ssum = -0.0_f64;
for &x in &inputs[..sp] {
ssum += x;
}
for &x in &inputs[fp..sp] {
fast_val = fa.mul_add(x, fo * fast_val);
}
let mut slow_val = ssum / sp as f64;
let mut gsum = -0.0_f64 + (fast_val - slow_val);
for &x in &inputs[sp..=first_full] {
fast_val = fa.mul_add(x, fo * fast_val);
slow_val = sa.mul_add(x, so * slow_val);
gsum += fast_val - slow_val;
}
let mut sig = gsum / gp as f64;
let macd = fast_val - slow_val;
out[first_full * 3..first_full * 3 + 3].copy_from_slice(&[macd, sig, macd - sig]);
for (row, &x) in out[(first_full + 1) * 3..]
.chunks_exact_mut(3)
.zip(&inputs[first_full + 1..])
{
fast_val = fa.mul_add(x, fo * fast_val);
slow_val = sa.mul_add(x, so * slow_val);
let macd = fast_val - slow_val;
sig = ga.mul_add(macd, go * sig);
row.copy_from_slice(&[macd, sig, macd - sig]);
}
(fast_val, slow_val, sig)
}
}
impl Indicator for MacdIndicator {
type Input = f64;
type Output = MacdOutput;
#[inline]
fn update(&mut self, input: f64) -> Option<MacdOutput> {
if !input.is_finite() {
return None;
}
let fast = self.fast.update(input);
let slow = self.slow.update(input);
match (fast, slow) {
(Some(f), Some(s)) => {
let macd = f - s;
let signal = self.signal_ema.update(macd)?;
let out = MacdOutput {
macd,
signal,
histogram: macd - signal,
};
self.last = Some(out);
Some(out)
}
_ => None,
}
}
fn reset(&mut self) {
self.fast.reset();
self.slow.reset();
self.signal_ema.reset();
self.last = None;
}
#[inline]
fn warmup_period(&self) -> usize {
self.slow_period + self.signal_period - 1
}
#[inline]
fn is_ready(&self) -> bool {
self.last.is_some()
}
#[inline]
fn name(&self) -> &'static str {
"MACD"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
#[test]
fn rejects_fast_geq_slow() {
assert!(matches!(
MacdIndicator::new(26, 12, 9),
Err(Error::InvalidPeriod { .. })
));
assert!(matches!(
MacdIndicator::new(12, 12, 9),
Err(Error::InvalidPeriod { .. })
));
}
#[test]
fn accessors_and_metadata() {
let mut m = MacdIndicator::new(12, 26, 9).unwrap();
assert_eq!(m.periods(), (12, 26, 9));
assert_eq!(m.name(), "MACD");
assert!(m.value().is_none());
for i in 1..=m.warmup_period() {
m.update(100.0 + f64::from(u32::try_from(i).unwrap()));
}
assert!(m.value().is_some());
}
#[test]
fn rejects_zero_periods() {
assert!(matches!(
MacdIndicator::new(0, 26, 9),
Err(Error::PeriodZero)
));
assert!(matches!(
MacdIndicator::new(12, 0, 9),
Err(Error::PeriodZero)
));
assert!(matches!(
MacdIndicator::new(12, 26, 0),
Err(Error::PeriodZero)
));
}
#[test]
fn first_emission_matches_warmup_period() {
let prices: Vec<f64> = (1..=60).map(f64::from).collect();
let mut macd = MacdIndicator::classic();
let out = macd.batch(&prices);
let warmup = macd.warmup_period();
for x in out.iter().take(warmup - 1) {
assert!(x.is_none(), "expected None within warmup");
}
assert!(
out[warmup - 1].is_some(),
"expected first emission at warmup_period - 1 ({warmup} idx)"
);
}
#[test]
fn histogram_equals_macd_minus_signal() {
let prices: Vec<f64> = (1..=80).map(|i| f64::from(i) * 0.5).collect();
let mut macd = MacdIndicator::classic();
for v in macd.batch(&prices).into_iter().flatten() {
assert_relative_eq!(v.histogram, v.macd - v.signal, epsilon = 1e-12);
}
}
#[test]
fn constant_series_yields_zero_macd_eventually() {
let mut macd = MacdIndicator::classic();
let out = macd.batch(&[100.0_f64; 200]);
let last = out.iter().rev().flatten().next().expect("emits a value");
assert_relative_eq!(last.macd, 0.0, epsilon = 1e-9);
assert_relative_eq!(last.signal, 0.0, epsilon = 1e-9);
assert_relative_eq!(last.histogram, 0.0, epsilon = 1e-9);
}
#[test]
fn rising_series_macd_positive_then_signal_catches_up() {
let prices: Vec<f64> = (1..=200).map(f64::from).collect();
let mut macd = MacdIndicator::classic();
let out = macd.batch(&prices);
let last = out.iter().rev().flatten().next().unwrap();
assert!(last.macd > 0.0, "rising series must yield positive MACD");
}
#[test]
fn batch_equals_streaming() {
let prices: Vec<f64> = (1..=100)
.map(|i| (f64::from(i) * 0.4).cos() * 10.0)
.collect();
let mut a = MacdIndicator::classic();
let mut b = MacdIndicator::classic();
assert_eq!(
a.batch(&prices),
prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
);
}
#[test]
fn reset_clears_state() {
let mut macd = MacdIndicator::classic();
macd.batch(&(1..=80).map(f64::from).collect::<Vec<_>>());
assert!(macd.is_ready());
macd.reset();
assert!(!macd.is_ready());
assert_eq!(macd.update(1.0), None);
}
fn bits_eq(a: &[f64], b: &[f64]) -> bool {
a.len() == b.len()
&& a.iter()
.zip(b)
.all(|(x, y)| x == y || (x.is_nan() && y.is_nan()))
}
fn macd_replay(series: &[f64]) -> Vec<f64> {
let mut m = MacdIndicator::classic();
let mut out = Vec::with_capacity(series.len() * 3);
for &x in series {
match m.update(x) {
Some(o) => out.extend_from_slice(&[o.macd, o.signal, o.histogram]),
None => out.extend_from_slice(&[f64::NAN; 3]),
}
}
out
}
#[test]
fn batch_macd_fast_path_is_bit_identical() {
let series: Vec<f64> = (0..300)
.map(|i| (f64::from(i) * 0.4).cos() * 10.0 + 100.0)
.collect();
let mut macd = MacdIndicator::classic();
let got = macd.batch_macd(&series);
assert!(bits_eq(&got, &macd_replay(&series)));
let mut ref_macd = MacdIndicator::classic();
for &x in &series {
ref_macd.update(x);
}
let (a, b) = (macd.update(101.0), ref_macd.update(101.0));
assert_eq!(a.is_some(), b.is_some());
assert_relative_eq!(a.unwrap().macd, b.unwrap().macd, epsilon = 1e-12);
}
fn to_bits(v: &[f64]) -> Vec<u64> {
v.iter().map(|x| x.to_bits()).collect()
}
#[test]
fn fused_kernel_is_identical_on_every_dispatch_path() {
let series: Vec<f64> = (0..3000)
.map(|i| (f64::from(i) * 0.093).sin() * 7.0 + f64::from(i % 13) * 0.4 + 80.0)
.collect();
let alphas = (2.0 / 13.0, 2.0 / 27.0, 2.0 / 10.0);
let mut dispatched = vec![0.0; series.len() * 3];
let mut baseline = vec![0.0; series.len() * 3];
let a = wickra_simd::dispatch(FusedMacd {
inputs: &series,
out: &mut dispatched,
periods: (12, 26, 9),
alphas,
});
let b = wickra_simd::run_baseline(FusedMacd {
inputs: &series,
out: &mut baseline,
periods: (12, 26, 9),
alphas,
});
assert_eq!(to_bits(&dispatched), to_bits(&baseline));
assert_eq!(
[a.0.to_bits(), a.1.to_bits(), a.2.to_bits()],
[b.0.to_bits(), b.1.to_bits(), b.2.to_bits()]
);
}
#[test]
fn batch_macd_negative_zero_series_matches_to_the_bit() {
let series = vec![-0.0_f64; 60];
let got = MacdIndicator::classic().batch_macd(&series);
assert_eq!(to_bits(&got), to_bits(&macd_replay(&series)));
}
#[test]
fn batch_macd_into_overwrites_a_dirty_buffer() {
let series: Vec<f64> = (0..200)
.map(|i| (f64::from(i) * 0.21).sin() * 3.0 + 50.0)
.collect();
let mut out = vec![9.0; series.len() * 3];
MacdIndicator::classic().batch_macd_into(&series, &mut out);
assert_eq!(to_bits(&out), to_bits(&macd_replay(&series)));
}
#[test]
fn batch_macd_with_signal_period_one_matches_replay() {
let series: Vec<f64> = (0..80).map(|i| f64::from(i % 9) * 1.25 + 30.0).collect();
let mut fused = MacdIndicator::new(3, 7, 1).unwrap();
let mut replay = MacdIndicator::new(3, 7, 1).unwrap();
let want: Vec<f64> = series
.iter()
.flat_map(|&x| match replay.update(x) {
Some(o) => [o.macd, o.signal, o.histogram],
None => [f64::NAN; 3],
})
.collect();
assert_eq!(to_bits(&fused.batch_macd(&series)), to_bits(&want));
}
#[test]
fn batch_macd_hands_huge_values_to_the_replay() {
let mut series: Vec<f64> = (0..60).map(|i| f64::from(i) + 100.0).collect();
series[45] = 1.7e308;
series[46] = -1.7e308;
let got = MacdIndicator::classic().batch_macd(&series);
assert!(bits_eq(&got, &macd_replay(&series)));
}
#[test]
#[should_panic(expected = "batch_macd output must hold three values per input")]
fn batch_macd_into_rejects_a_short_buffer() {
let mut out = vec![0.0; 5];
MacdIndicator::classic().batch_macd_into(&[1.0, 2.0], &mut out);
}
#[test]
fn batch_macd_falls_back_on_non_finite() {
let mut series: Vec<f64> = (0..60).map(|i| f64::from(i) + 100.0).collect();
series[40] = f64::NAN;
let mut macd = MacdIndicator::classic();
assert!(bits_eq(&macd.batch_macd(&series), &macd_replay(&series)));
}
#[test]
fn batch_macd_falls_back_when_not_fresh() {
let series: Vec<f64> = (0..60).map(|i| f64::from(i) + 100.0).collect();
let mut macd = MacdIndicator::classic();
macd.update(50.0);
let mut ref_macd = MacdIndicator::classic();
ref_macd.update(50.0);
let mut want = Vec::new();
for &x in &series {
match ref_macd.update(x) {
Some(o) => want.extend_from_slice(&[o.macd, o.signal, o.histogram]),
None => want.extend_from_slice(&[f64::NAN; 3]),
}
}
assert!(bits_eq(&macd.batch_macd(&series), &want));
}
#[test]
fn batch_macd_too_short_for_output_falls_back() {
let series: Vec<f64> = (0..20).map(|i| f64::from(i) + 100.0).collect();
let mut macd = MacdIndicator::classic();
let got = macd.batch_macd(&series);
assert!(bits_eq(&got, &macd_replay(&series)));
assert!(got.iter().all(|x| x.is_nan()));
}
#[test]
fn ignores_non_finite_input() {
let mut macd = MacdIndicator::classic();
macd.batch(&(1..=80).map(f64::from).collect::<Vec<_>>());
let before = macd.value();
assert!(before.is_some());
assert_eq!(macd.update(f64::NAN), None);
assert_eq!(macd.update(f64::INFINITY), None);
assert_eq!(macd.value(), before);
}
#[test]
fn fixed_12_26_rejects_invalid_signal() {
assert!(matches!(
MacdIndicator::fixed_12_26(0),
Err(Error::PeriodZero)
));
let too_big = crate::error::MAX_PERIOD + 1;
assert!(matches!(
MacdIndicator::fixed_12_26(too_big),
Err(Error::InvalidPeriod { .. })
));
}
#[test]
fn fixed_12_26_configuration() {
let m = MacdIndicator::fixed_12_26(9).unwrap();
assert_eq!(m.periods(), (12, 26, 9));
assert_eq!(m.warmup_period(), 34);
assert_eq!(m.fast.alpha().to_bits(), 0.15_f64.to_bits());
assert_eq!(m.slow.alpha().to_bits(), 0.075_f64.to_bits());
assert_eq!(m.signal_ema.alpha().to_bits(), (2.0_f64 / 10.0).to_bits());
assert!(!m.is_ready());
}
#[test]
fn fixed_12_26_hand_computed_step() {
let mut m = MacdIndicator::fixed_12_26(1).unwrap();
let mut prices = vec![100.0; 26];
prices.extend_from_slice(&[110.0, 110.0]);
let out = m.batch(&prices);
assert!(out[..25].iter().all(Option::is_none));
assert_relative_eq!(out[25].unwrap().macd, 0.0, epsilon = 1e-12);
let o26 = out[26].unwrap();
assert_relative_eq!(o26.macd, 0.75, epsilon = 1e-12);
assert_relative_eq!(o26.signal, 0.75, epsilon = 1e-12);
assert_relative_eq!(o26.histogram, 0.0, epsilon = 1e-12);
assert_relative_eq!(out[27].unwrap().macd, 1.331_25, epsilon = 1e-12);
}
}