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