Skip to main content

quantwave_core/indicators/incremental/
macd_ext.rs

1//! Native O(1) MACDEXT and MACDFIX — TA-Lib parity.
2
3use crate::indicators::incremental::ma_stream::MaStream;
4use crate::indicators::incremental::macd::MACD;
5use crate::indicators::ma_type::MaType;
6use crate::traits::Next;
7
8const NAN_TRIPLE: (f64, f64, f64) = (f64::NAN, f64::NAN, f64::NAN);
9
10/// MACD with controllable MA types — matches `talib_rs::momentum::macd_ext`.
11#[derive(Debug, Clone)]
12#[allow(non_camel_case_types)]
13pub struct MACDEXT {
14    pub fastperiod: usize,
15    pub fastmatype: MaType,
16    pub slowperiod: usize,
17    pub slowmatype: MaType,
18    pub signalperiod: usize,
19    pub signalmatype: MaType,
20    ema_macd: Option<MACD>,
21    fast_ma: MaStream,
22    slow_ma: MaStream,
23    signal_ma: MaStream,
24    macd_valid_count: usize,
25}
26
27impl MACDEXT {
28    pub fn new(
29        fastperiod: usize,
30        fastmatype: MaType,
31        slowperiod: usize,
32        slowmatype: MaType,
33        signalperiod: usize,
34        signalmatype: MaType,
35    ) -> Self {
36        let ema_macd = if fastmatype == MaType::Ema
37            && slowmatype == MaType::Ema
38            && signalmatype == MaType::Ema
39        {
40            Some(MACD::new(fastperiod, slowperiod, signalperiod))
41        } else {
42            None
43        };
44        Self {
45            fastperiod,
46            fastmatype,
47            slowperiod,
48            slowmatype,
49            signalperiod,
50            signalmatype,
51            ema_macd,
52            fast_ma: MaStream::new(fastperiod, fastmatype),
53            slow_ma: MaStream::new(slowperiod, slowmatype),
54            signal_ma: MaStream::new(signalperiod, signalmatype),
55            macd_valid_count: 0,
56        }
57    }
58}
59
60impl Next<f64> for MACDEXT {
61    type Output = (f64, f64, f64);
62
63    fn next(&mut self, input: f64) -> Self::Output {
64        if let Some(ref mut macd) = self.ema_macd {
65            return macd.next(input);
66        }
67
68        let fast = self.fast_ma.next(input);
69        let slow = self.slow_ma.next(input);
70        if fast.is_nan() || slow.is_nan() {
71            return NAN_TRIPLE;
72        }
73
74        let macd_line = fast - slow;
75        self.macd_valid_count += 1;
76        let signal = self.signal_ma.next(macd_line);
77        if signal.is_nan() {
78            return NAN_TRIPLE;
79        }
80
81        (macd_line, signal, macd_line - signal)
82    }
83}
84
85/// MACD Fix (12/26 fixed) — matches `talib_rs::momentum::macd_fix`.
86#[derive(Debug, Clone)]
87#[allow(non_camel_case_types)]
88pub struct MACDFIX {
89    pub signalperiod: usize,
90    fp: usize,
91    sp: usize,
92    k_fast: f64,
93    k_slow: f64,
94    k_signal: f64,
95    out_start: usize,
96    bars_seen: usize,
97    seed_closes: Vec<f64>,
98    slow_ema: f64,
99    fast_ema: f64,
100    macd_values: Vec<f64>,
101    signal_ema: f64,
102}
103
104impl MACDFIX {
105    pub fn new(signalperiod: usize) -> Self {
106        let fp = 12usize;
107        let sp = 26usize;
108        Self {
109            signalperiod,
110            fp,
111            sp,
112            k_fast: 0.15,
113            k_slow: 0.075,
114            k_signal: 2.0 / (signalperiod as f64 + 1.0),
115            out_start: sp - 1 + signalperiod - 1,
116            bars_seen: 0,
117            seed_closes: Vec::with_capacity(sp),
118            slow_ema: 0.0,
119            fast_ema: 0.0,
120            macd_values: Vec::new(),
121            signal_ema: 0.0,
122        }
123    }
124
125    #[inline]
126    fn update_emas(&mut self, input: f64) {
127        self.slow_ema = self.k_slow.mul_add(input - self.slow_ema, self.slow_ema);
128        self.fast_ema = self.k_fast.mul_add(input - self.fast_ema, self.fast_ema);
129        self.macd_values.push(self.fast_ema - self.slow_ema);
130    }
131}
132
133impl Next<f64> for MACDFIX {
134    type Output = (f64, f64, f64);
135
136    fn next(&mut self, input: f64) -> Self::Output {
137        let i = self.bars_seen;
138        self.bars_seen += 1;
139
140        if i < self.sp - 1 {
141            self.seed_closes.push(input);
142            return NAN_TRIPLE;
143        }
144
145        if i == self.sp - 1 {
146            self.seed_closes.push(input);
147            let slow_seed: f64 = self.seed_closes.iter().sum::<f64>() / self.sp as f64;
148            let fast_seed: f64 = self.seed_closes[self.sp - self.fp..self.sp]
149                .iter()
150                .sum::<f64>()
151                / self.fp as f64;
152            self.slow_ema = slow_seed;
153            self.fast_ema = fast_seed;
154            let macd0 = fast_seed - slow_seed;
155            self.macd_values.push(macd0);
156            if self.out_start == self.sp - 1 {
157                let signal_seed = macd0;
158                self.signal_ema = signal_seed;
159                return (macd0, signal_seed, 0.0);
160            }
161            return NAN_TRIPLE;
162        }
163
164        if i < self.out_start {
165            self.update_emas(input);
166            return NAN_TRIPLE;
167        }
168
169        if i == self.out_start {
170            if i >= self.sp {
171                self.update_emas(input);
172            }
173            let signal_seed: f64 = self.macd_values[..self.signalperiod].iter().sum::<f64>()
174                / self.signalperiod as f64;
175            self.signal_ema = signal_seed;
176            let macd = self.macd_values[self.signalperiod - 1];
177            return (macd, signal_seed, macd - signal_seed);
178        }
179
180        self.update_emas(input);
181        let macd = *self.macd_values.last().unwrap_or(&f64::NAN);
182        self.signal_ema = self
183            .k_signal
184            .mul_add(macd - self.signal_ema, self.signal_ema);
185        (macd, self.signal_ema, macd - self.signal_ema)
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192    use proptest::prelude::*;
193
194    type Triples = (Vec<f64>, Vec<f64>, Vec<f64>);
195
196    fn assert_parity(streaming: &[(f64, f64, f64)], batch: &Triples, label: &str) {
197        let (b_macd, b_signal, b_hist) = batch;
198        for (i, &(s_m, s_s, s_h)) in streaming.iter().enumerate() {
199            for (s, b, name) in [
200                (s_m, b_macd[i], "macd"),
201                (s_s, b_signal[i], "signal"),
202                (s_h, b_hist[i], "hist"),
203            ] {
204                if s.is_nan() {
205                    assert!(
206                        b.is_nan(),
207                        "{label} bar {i} {name}: streaming NaN, batch {b}"
208                    );
209                } else {
210                    assert!(
211                        !b.is_nan(),
212                        "{label} bar {i} {name}: streaming {s}, batch NaN"
213                    );
214                    approx::assert_relative_eq!(s, b, epsilon = 1e-6);
215                }
216            }
217        }
218    }
219
220    proptest! {
221        #[test]
222        fn test_macdext_ema_parity(input in prop::collection::vec(0.1..100.0, 1..100)) {
223            let fast = 12usize;
224            let slow = 26usize;
225            let signal = 9usize;
226            let matype = MaType::Ema;
227            let mut ext = MACDEXT::new(fast, matype, slow, matype, signal, matype);
228            let streaming: Vec<_> = input.iter().map(|&x| ext.next(x)).collect();
229            let batch = talib_rs::momentum::macd_ext(
230                &input, fast, matype.into(), slow, matype.into(), signal, matype.into(),
231            ).unwrap_or_else(|_| {
232                (vec![f64::NAN; input.len()], vec![f64::NAN; input.len()], vec![f64::NAN; input.len()])
233            });
234            assert_parity(&streaming, &batch, "macdext/ema");
235        }
236
237        /// MACDEXT smooths all three legs through `MaStream`, so every matype
238        /// except EMA silently became SMA. Cover all nine.
239        #[test]
240        fn test_macdext_parity_all_matypes(
241            input in prop::collection::vec(0.1..100.0, 150..220),
242            idx in 0usize..9,
243        ) {
244            let matype = MaType::ALL[idx];
245            let (fast, slow, signal) = (12usize, 26usize, 9usize);
246            let mut ext = MACDEXT::new(fast, matype, slow, matype, signal, matype);
247            let streaming: Vec<_> = input.iter().map(|&x| ext.next(x)).collect();
248            let batch = talib_rs::momentum::macd_ext(
249                &input, fast, matype.into(), slow, matype.into(), signal, matype.into(),
250            ).unwrap_or_else(|_| {
251                (vec![f64::NAN; input.len()], vec![f64::NAN; input.len()], vec![f64::NAN; input.len()])
252            });
253            assert_parity(&streaming, &batch, &format!("macdext/{matype}"));
254        }
255
256        #[test]
257        fn test_macdfix_parity(input in prop::collection::vec(0.1..100.0, 1..100)) {
258            let signal = 9usize;
259            let mut fix = MACDFIX::new(signal);
260            let streaming: Vec<_> = input.iter().map(|&x| fix.next(x)).collect();
261            let batch = talib_rs::momentum::macd_fix(&input, signal)
262                .unwrap_or_else(|_| {
263                    (vec![f64::NAN; input.len()], vec![f64::NAN; input.len()], vec![f64::NAN; input.len()])
264                });
265            assert_parity(&streaming, &batch, "macdfix");
266        }
267    }
268}