mantis_ta/indicators/trend/
macd.rs1use super::EMA;
2use crate::indicators::Indicator;
3use crate::types::{Candle, MacdOutput};
4
5#[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 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}