Skip to main content

kestrel_chartkit/indicator/
macd.rs

1use std::collections::HashMap;
2
3use crate::model::Bar;
4
5use super::smoothing::{crossed_over, crossed_under, Ema};
6use super::{Indicator, IndicatorAlert, IndicatorOutput};
7
8pub struct Macd {
9    fast_ema: Ema,
10    slow_ema: Ema,
11    signal_ema: Ema,
12    slow_len: usize,
13
14    prev_macd: Option<f64>,
15    prev_signal: Option<f64>,
16    bars_seen: usize,
17
18    alerts: MacdAlerts,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Default)]
22pub struct MacdAlerts {
23    pub bull_cross: bool,
24    pub bear_cross: bool,
25    pub bull_zero_cross: bool,
26    pub bear_zero_cross: bool,
27}
28
29impl Macd {
30    pub fn new(fast_len: usize, slow_len: usize, signal_len: usize) -> Self {
31        Self {
32            fast_ema: Ema::new(fast_len),
33            slow_ema: Ema::new(slow_len),
34            signal_ema: Ema::new(signal_len),
35            slow_len,
36            prev_macd: None,
37            prev_signal: None,
38            bars_seen: 0,
39            alerts: MacdAlerts::default(),
40        }
41    }
42
43    pub fn with_defaults() -> Self {
44        Self::new(12, 26, 9)
45    }
46}
47
48impl Indicator for Macd {
49    fn name(&self) -> &str {
50        "macd"
51    }
52
53    fn warmup_period(&self) -> usize {
54        self.slow_len
55    }
56
57    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
58        self.alerts = MacdAlerts::default();
59        self.bars_seen += 1;
60
61        let close = bar.close;
62        let fast = self.fast_ema.update(close);
63        let slow = self.slow_ema.update(close);
64
65        if self.bars_seen < self.slow_len {
66            return None;
67        }
68
69        let macd_line = fast - slow;
70        let signal_line = self.signal_ema.update(macd_line);
71        let hist = macd_line - signal_line;
72
73        if let (Some(prev_m), Some(prev_s)) = (self.prev_macd, self.prev_signal) {
74            self.alerts.bull_cross = crossed_over(prev_m, prev_s, macd_line, signal_line);
75            self.alerts.bear_cross = crossed_under(prev_m, prev_s, macd_line, signal_line);
76            self.alerts.bull_zero_cross = crossed_over(prev_m, 0.0, macd_line, 0.0);
77            self.alerts.bear_zero_cross = crossed_under(prev_m, 0.0, macd_line, 0.0);
78        }
79
80        self.prev_macd = Some(macd_line);
81        self.prev_signal = Some(signal_line);
82
83        let mut extra = HashMap::new();
84        extra.insert("signal".to_string(), signal_line);
85        extra.insert("hist".to_string(), hist);
86
87        Some(IndicatorOutput::with_extra(macd_line, extra))
88    }
89
90    fn reset(&mut self) {
91        self.fast_ema.reset();
92        self.slow_ema.reset();
93        self.signal_ema.reset();
94        self.prev_macd = None;
95        self.prev_signal = None;
96        self.bars_seen = 0;
97        self.alerts = MacdAlerts::default();
98    }
99
100    fn alerts(&self) -> Vec<IndicatorAlert> {
101        let a = self.alerts;
102        let mut out = Vec::new();
103        if a.bull_cross {
104            out.push(IndicatorAlert {
105                kind: "bull_cross".to_string(),
106                note: "MACD · BULL CROSS SIGNAL".to_string(),
107                strength: 1.0,
108            });
109        }
110        if a.bear_cross {
111            out.push(IndicatorAlert {
112                kind: "bear_cross".to_string(),
113                note: "MACD · BEAR CROSS SIGNAL".to_string(),
114                strength: 1.0,
115            });
116        }
117        if a.bull_zero_cross {
118            out.push(IndicatorAlert {
119                kind: "bull_zero_cross".to_string(),
120                note: "MACD · CROSS ABOVE ZERO".to_string(),
121                strength: 1.0,
122            });
123        }
124        if a.bear_zero_cross {
125            out.push(IndicatorAlert {
126                kind: "bear_zero_cross".to_string(),
127                note: "MACD · CROSS BELOW ZERO".to_string(),
128                strength: 1.0,
129            });
130        }
131        out
132    }
133}