kestrel_chartkit/indicator/
macd.rs1use 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}