Skip to main content

mantis_ta/indicators/trend/
macd.rs

1use super::EMA;
2use crate::indicators::Indicator;
3use crate::types::{Candle, MacdOutput};
4
5/// Moving Average Convergence Divergence over closing prices.
6///
7/// # Examples
8/// ```rust
9/// use mantis_ta::indicators::{Indicator, MACD};
10/// use mantis_ta::types::Candle;
11///
12/// // 12/26/9 MACD emits after slow + signal warmup
13/// let prices = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0];
14/// let candles: Vec<Candle> = prices
15///     .iter()
16///     .enumerate()
17///     .map(|(i, p)| Candle {
18///         timestamp: i as i64,
19///         open: *p,
20///         high: *p,
21///         low: *p,
22///         close: *p,
23///         volume: 0.0,
24///     })
25///     .collect();
26///
27/// let out = MACD::new(2, 4, 2).calculate(&candles);
28/// assert!(out.iter().take(4).all(|v| v.is_none()));
29/// assert!(out.iter().skip(3).any(|v| v.is_some()));
30/// ```
31#[derive(Debug, Clone)]
32pub struct MACD {
33    fast: EMA,
34    slow: EMA,
35    signal: EMA,
36    slow_period: usize,
37    signal_period: usize,
38}
39
40impl MACD {
41    pub fn new(fast: usize, slow: usize, signal: usize) -> Self {
42        assert!(fast > 0 && slow > 0 && signal > 0, "periods must be > 0");
43        assert!(fast < slow, "fast period must be < slow period");
44        Self {
45            fast: EMA::new(fast),
46            slow: EMA::new(slow),
47            signal: EMA::new(signal),
48            slow_period: slow,
49            signal_period: signal,
50        }
51    }
52
53    #[inline]
54    fn macd_candle(value: f64) -> Candle {
55        Candle {
56            timestamp: 0,
57            open: value,
58            high: value,
59            low: value,
60            close: value,
61            volume: 0.0,
62        }
63    }
64}
65
66impl Indicator for MACD {
67    type Output = MacdOutput;
68
69    fn next(&mut self, candle: &Candle) -> Option<Self::Output> {
70        let slow_val = self.slow.next(candle);
71        let fast_val = self.fast.next(candle);
72
73        let macd_line = match (fast_val, slow_val) {
74            (Some(f), Some(s)) => f - s,
75            _ => return None,
76        };
77
78        let macd_candle = Self::macd_candle(macd_line);
79        let signal_line = self.signal.next(&macd_candle)?;
80
81        let histogram = macd_line - signal_line;
82        Some(MacdOutput {
83            macd_line,
84            signal_line,
85            histogram,
86        })
87    }
88
89    fn reset(&mut self) {
90        self.fast.reset();
91        self.slow.reset();
92        self.signal.reset();
93    }
94
95    fn warmup_period(&self) -> usize {
96        // Need slow EMA warmup, then signal warmup on MACD line.
97        self.slow_period + self.signal_period - 1
98    }
99
100    fn clone_boxed(&self) -> Box<dyn Indicator<Output = Self::Output>> {
101        Box::new(self.clone())
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn macd_emits_after_warmup() {
111        let mut macd = MACD::new(2, 4, 2);
112        let prices = [1.0, 2.0, 3.0, 4.0, 5.0];
113        let candles: Vec<Candle> = prices
114            .iter()
115            .map(|p| Candle {
116                timestamp: 0,
117                open: *p,
118                high: *p,
119                low: *p,
120                close: *p,
121                volume: 0.0,
122            })
123            .collect();
124
125        let outputs: Vec<_> = candles.iter().map(|c| macd.next(c)).collect();
126        assert!(
127            outputs
128                .iter()
129                .take(macd.warmup_period() - 1)
130                .all(|o| o.is_none())
131        );
132        assert!(
133            outputs
134                .iter()
135                .skip(macd.warmup_period() - 1)
136                .any(|o| o.is_some())
137        );
138    }
139}