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::traits::Next;
6use talib_rs::MaType;
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]
174                .iter()
175                .sum::<f64>()
176                / self.signalperiod as f64;
177            self.signal_ema = signal_seed;
178            let macd = self.macd_values[self.signalperiod - 1];
179            return (macd, signal_seed, macd - signal_seed);
180        }
181
182        self.update_emas(input);
183        let macd = *self.macd_values.last().unwrap_or(&f64::NAN);
184        self.signal_ema = self
185            .k_signal
186            .mul_add(macd - self.signal_ema, self.signal_ema);
187        (macd, self.signal_ema, macd - self.signal_ema)
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194    use proptest::prelude::*;
195
196    proptest! {
197        #[test]
198        fn test_macdext_ema_parity(input in prop::collection::vec(0.1..100.0, 1..100)) {
199            let fast = 12usize;
200            let slow = 26usize;
201            let signal = 9usize;
202            let matype = MaType::Ema;
203            let mut ext = MACDEXT::new(fast, matype, slow, matype, signal, matype);
204            let streaming: Vec<_> = input.iter().map(|&x| ext.next(x)).collect();
205            let (b_macd, b_signal, b_hist) = talib_rs::momentum::macd_ext(
206                &input, fast, matype, slow, matype, signal, matype,
207            ).unwrap_or_else(|_| {
208                (vec![f64::NAN; input.len()], vec![f64::NAN; input.len()], vec![f64::NAN; input.len()])
209            });
210            for (i, (s_m, s_s, s_h)) in streaming.into_iter().enumerate() {
211                if s_m.is_nan() { assert!(b_macd[i].is_nan()); }
212                else { approx::assert_relative_eq!(s_m, b_macd[i], epsilon = 1e-6); }
213                if s_s.is_nan() { assert!(b_signal[i].is_nan()); }
214                else { approx::assert_relative_eq!(s_s, b_signal[i], epsilon = 1e-6); }
215                if s_h.is_nan() { assert!(b_hist[i].is_nan()); }
216                else { approx::assert_relative_eq!(s_h, b_hist[i], epsilon = 1e-6); }
217            }
218        }
219
220        #[test]
221        fn test_macdfix_parity(input in prop::collection::vec(0.1..100.0, 1..100)) {
222            let signal = 9usize;
223            let mut fix = MACDFIX::new(signal);
224            let streaming: Vec<_> = input.iter().map(|&x| fix.next(x)).collect();
225            let (b_macd, b_signal, b_hist) = talib_rs::momentum::macd_fix(&input, signal)
226                .unwrap_or_else(|_| {
227                    (vec![f64::NAN; input.len()], vec![f64::NAN; input.len()], vec![f64::NAN; input.len()])
228                });
229            for (i, (s_m, s_s, s_h)) in streaming.into_iter().enumerate() {
230                if s_m.is_nan() { assert!(b_macd[i].is_nan()); }
231                else { approx::assert_relative_eq!(s_m, b_macd[i], epsilon = 1e-6); }
232                if s_s.is_nan() { assert!(b_signal[i].is_nan()); }
233                else { approx::assert_relative_eq!(s_s, b_signal[i], epsilon = 1e-6); }
234                if s_h.is_nan() { assert!(b_hist[i].is_nan()); }
235                else { approx::assert_relative_eq!(s_h, b_hist[i], epsilon = 1e-6); }
236            }
237        }
238    }
239}