Skip to main content

quantwave_core/indicators/incremental/
macd.rs

1//! Native O(1) MACD — TA-Lib aligned EMA seeding parity.
2
3use crate::traits::Next;
4
5const NAN_TRIPLE: (f64, f64, f64) = (f64::NAN, f64::NAN, f64::NAN);
6
7/// MACD (12, 26, 9 default) — matches `talib_rs::momentum::macd` aligned internal EMAs.
8#[derive(Debug, Clone)]
9#[allow(non_camel_case_types)]
10pub struct MACD {
11    pub fastperiod: usize,
12    pub slowperiod: usize,
13    pub signalperiod: usize,
14    fp: usize,
15    sp: usize,
16    k_fast: f64,
17    k_slow: f64,
18    k_signal: f64,
19    out_start: usize,
20    bars_seen: usize,
21    seed_closes: Vec<f64>,
22    slow_ema: f64,
23    fast_ema: f64,
24    macd_values: Vec<f64>,
25    signal_ema: f64,
26}
27
28impl MACD {
29    pub fn new(fastperiod: usize, slowperiod: usize, signalperiod: usize) -> Self {
30        let (fp, sp) = if fastperiod < slowperiod {
31            (fastperiod, slowperiod)
32        } else {
33            (slowperiod, fastperiod)
34        };
35        Self {
36            fastperiod,
37            slowperiod,
38            signalperiod,
39            fp,
40            sp,
41            k_fast: 2.0 / (fp as f64 + 1.0),
42            k_slow: 2.0 / (sp as f64 + 1.0),
43            k_signal: 2.0 / (signalperiod as f64 + 1.0),
44            out_start: sp - 1 + signalperiod - 1,
45            bars_seen: 0,
46            seed_closes: Vec::with_capacity(sp),
47            slow_ema: 0.0,
48            fast_ema: 0.0,
49            macd_values: Vec::new(),
50            signal_ema: 0.0,
51        }
52    }
53
54    #[inline]
55    fn update_emas(&mut self, input: f64) {
56        self.slow_ema = self.k_slow.mul_add(input - self.slow_ema, self.slow_ema);
57        self.fast_ema = self.k_fast.mul_add(input - self.fast_ema, self.fast_ema);
58        self.macd_values.push(self.fast_ema - self.slow_ema);
59    }
60}
61
62impl Next<f64> for MACD {
63    type Output = (f64, f64, f64);
64
65    fn next(&mut self, input: f64) -> Self::Output {
66        let i = self.bars_seen;
67        self.bars_seen += 1;
68
69        if i < self.sp - 1 {
70            self.seed_closes.push(input);
71            return NAN_TRIPLE;
72        }
73
74        if i == self.sp - 1 {
75            self.seed_closes.push(input);
76            let slow_seed: f64 =
77                self.seed_closes.iter().sum::<f64>() / self.sp as f64;
78            let fast_seed: f64 = self.seed_closes[self.sp - self.fp..self.sp]
79                .iter()
80                .sum::<f64>()
81                / self.fp as f64;
82            self.slow_ema = slow_seed;
83            self.fast_ema = fast_seed;
84            let macd0 = fast_seed - slow_seed;
85            self.macd_values.push(macd0);
86            if self.out_start == self.sp - 1 {
87                let signal_seed = macd0;
88                self.signal_ema = signal_seed;
89                return (macd0, signal_seed, 0.0);
90            }
91            return NAN_TRIPLE;
92        }
93
94        if i < self.out_start {
95            self.update_emas(input);
96            return NAN_TRIPLE;
97        }
98
99        if i == self.out_start {
100            if i >= self.sp {
101                self.update_emas(input);
102            }
103            let signal_seed: f64 = self.macd_values[..self.signalperiod]
104                .iter()
105                .sum::<f64>()
106                / self.signalperiod as f64;
107            self.signal_ema = signal_seed;
108            let macd = self.macd_values[self.signalperiod - 1];
109            return (macd, signal_seed, macd - signal_seed);
110        }
111
112        self.update_emas(input);
113        let macd = *self.macd_values.last().unwrap_or(&f64::NAN);
114        self.signal_ema = self
115            .k_signal
116            .mul_add(macd - self.signal_ema, self.signal_ema);
117        (macd, self.signal_ema, macd - self.signal_ema)
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use proptest::prelude::*;
125
126    proptest! {
127        #[test]
128        fn test_macd_parity(input in prop::collection::vec(0.1..100.0, 1..100)) {
129            let fast = 12;
130            let slow = 26;
131            let signal = 9;
132            let mut macd = MACD::new(fast, slow, signal);
133            let streaming_results: Vec<(f64, f64, f64)> =
134                input.iter().map(|&x| macd.next(x)).collect();
135            let (b_macd, b_signal, b_hist) = talib_rs::momentum::macd(&input, fast, slow, signal)
136                .unwrap_or_else(|_| {
137                    (
138                        vec![f64::NAN; input.len()],
139                        vec![f64::NAN; input.len()],
140                        vec![f64::NAN; input.len()],
141                    )
142                });
143
144            for (i, (s_macd, s_signal, s_hist)) in streaming_results.into_iter().enumerate() {
145                if s_macd.is_nan() {
146                    assert!(b_macd[i].is_nan());
147                } else {
148                    approx::assert_relative_eq!(s_macd, b_macd[i], epsilon = 1e-6);
149                }
150                if s_signal.is_nan() {
151                    assert!(b_signal[i].is_nan());
152                } else {
153                    approx::assert_relative_eq!(s_signal, b_signal[i], epsilon = 1e-6);
154                }
155                if s_hist.is_nan() {
156                    assert!(b_hist[i].is_nan());
157                } else {
158                    approx::assert_relative_eq!(s_hist, b_hist[i], epsilon = 1e-6);
159                }
160            }
161        }
162    }
163}