egui_charts/studies/builtin/
macd.rs1use super::price_oscillator::{Mean, OscillatorAverage};
52use crate::model::Bar;
53use crate::studies::palette;
54use crate::studies::stepper::Stepper;
55use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
56use egui::Color32;
57
58#[derive(Clone)]
63pub struct MACD {
64 fast_period: usize,
65 slow_period: usize,
66 signal_period: usize,
67 oscillator_average: OscillatorAverage,
69 signal_average: OscillatorAverage,
71 values: IndicatorValues,
72 stepper: Stepper<State>,
73 colors: Vec<Color32>,
74 visible: bool,
75}
76
77#[derive(Clone, Default)]
78struct State {
79 usable: usize,
80 defined: usize,
82 fast: f64,
83 slow: f64,
84 signal: f64,
85 means: [Mean; 3],
87}
88
89impl MACD {
90 pub fn new(fast_period: usize, slow_period: usize, signal_period: usize) -> Self {
97 Self {
98 fast_period: fast_period.max(1),
99 slow_period: slow_period.max(1),
100 signal_period: signal_period.max(1),
101 oscillator_average: OscillatorAverage::Exponential,
102 signal_average: OscillatorAverage::Exponential,
103 values: IndicatorValues::new(),
104 stepper: Stepper::default(),
105 colors: vec![
106 palette::BLUE, palette::ORANGE, palette::HISTOGRAM_TEAL, palette::LIGHT_TEAL,
110 palette::HISTOGRAM_RED,
111 palette::LIGHT_RED,
112 ],
113 visible: true,
114 }
115 }
116
117 pub fn with_averages(
120 mut self,
121 oscillator: OscillatorAverage,
122 signal: OscillatorAverage,
123 ) -> Self {
124 self.oscillator_average = oscillator;
125 self.signal_average = signal;
126 self
127 }
128}
129
130impl Default for MACD {
132 fn default() -> Self {
133 Self::new(12, 26, 9)
134 }
135}
136
137impl Indicator for MACD {
138 fn source(&self) -> Option<crate::studies::StudySource> {
139 Some(crate::studies::StudySource::Price(
140 crate::model::PriceSource::Close,
141 ))
142 }
143
144 crate::studies::inputs::study_inputs!(self;
145 fast_period: length("Fast period", 1, usize::MAX),
146 slow_period: length("Slow period", 1, usize::MAX),
147 signal_period: length("Signal period", 1, usize::MAX),
148 oscillator_average: choice("Oscillator MA", [OscillatorAverage::Exponential, OscillatorAverage::Simple]),
149 signal_average: choice("Signal MA", [OscillatorAverage::Exponential, OscillatorAverage::Simple])
150 ; {});
151
152 fn plot_style(&self, line: usize) -> crate::studies::PlotStyle {
153 crate::studies::PlotStyle {
154 color: self.colors.get(line).copied().unwrap_or(Color32::WHITE),
155 width: 1.0,
156 pattern: crate::model::LineStyle::Solid,
157 plot_type: if line == 2 {
158 crate::studies::PlotType::Columns
159 } else {
160 crate::studies::PlotType::Line
161 },
162 visible: true,
163 }
164 }
165
166 fn plot_color(&self, line: usize, bar: usize) -> Option<Color32> {
167 if line != 2 {
168 return None;
169 }
170 histogram_color(&self.values, bar, &self.colors[2..])
171 }
172
173 fn value_format(&self) -> crate::scales::StudyFormat {
174 crate::scales::StudyFormat::Price
175 }
176
177 fn name(&self) -> &str {
178 "MACD"
179 }
180
181 fn desc(&self) -> &str {
182 "MACD - Trend-following momentum indicator"
183 }
184
185 fn calculate(&mut self, data: &[Bar]) {
186 self.update(data, 0);
187 }
188
189 fn update(&mut self, data: &[Bar], from: usize) {
190 let threshold = self.slow_period.max(self.signal_period);
191 let from = if self.stepper.state().usable < threshold {
193 0
194 } else {
195 from
196 };
197 let fast_mult = 2.0 / (self.fast_period as f64 + 1.0);
198 let slow_mult = 2.0 / (self.slow_period as f64 + 1.0);
199 let signal_mult = 2.0 / (self.signal_period as f64 + 1.0);
200 self.stepper
201 .run(data, from, &mut self.values, |state, bar| {
202 if !bar.close.is_finite() {
203 return IndicatorValue::None;
204 }
205 let (fast, slow) = match self.oscillator_average {
206 OscillatorAverage::Exponential => {
207 if state.usable == 0 {
208 state.fast = bar.close;
209 state.slow = bar.close;
210 } else {
211 state.fast = (bar.close - state.fast) * fast_mult + state.fast;
212 state.slow = (bar.close - state.slow) * slow_mult + state.slow;
213 }
214 (state.fast, state.slow)
215 }
216 OscillatorAverage::Simple => (
217 state.means[0].next(OscillatorAverage::Simple, bar.close, self.fast_period),
218 state.means[1].next(OscillatorAverage::Simple, bar.close, self.slow_period),
219 ),
220 };
221 state.usable += 1;
222 let macd = fast - slow;
223 if !macd.is_finite() {
225 return IndicatorValue::None;
226 }
227 let signal = match self.signal_average {
228 OscillatorAverage::Exponential => {
229 state.signal = if state.defined == 0 {
230 macd
231 } else {
232 (macd - state.signal) * signal_mult + state.signal
233 };
234 state.signal
235 }
236 OscillatorAverage::Simple => {
237 state.means[2].next(OscillatorAverage::Simple, macd, self.signal_period)
238 }
239 };
240 state.defined += 1;
241 IndicatorValue::Multiple(vec![macd, signal, macd - signal])
242 });
243 if self.stepper.state().usable < threshold {
244 self.values.fill(IndicatorValue::None);
245 }
246 }
247
248 fn is_incremental(&self) -> bool {
249 true
250 }
251
252 fn values(&self) -> &IndicatorValues {
253 &self.values
254 }
255
256 fn colors(&self) -> Vec<Color32> {
257 self.colors.clone()
258 }
259
260 fn set_colors(&mut self, colors: Vec<Color32>) {
261 if colors.len() >= 3 {
262 self.colors = colors;
263 if self.colors.len() < 6 {
264 self.colors.truncate(3);
265 }
266 self.colors.resize(6, self.colors[2]);
267 } else if !colors.is_empty() {
268 self.colors = vec![colors[0]; 6];
269 }
270 }
271
272 fn set_color(&mut self, line: usize, color: Color32) {
273 match line {
274 0 | 1 => self.colors[line] = color,
275 2 => self.colors[2..].fill(color),
276 _ => {}
277 }
278 }
279
280 fn pane(&self) -> crate::studies::StudyPane {
281 crate::studies::StudyPane {
282 height: None,
283 range: None,
284 levels: vec![(0.0, palette::MUTED.gamma_multiply_u8(128), "Zero Line")],
285 fill_between_levels: false,
286 }
287 }
288
289 fn is_overlay(&self) -> bool {
290 false }
292
293 fn line_count(&self) -> usize {
294 3
295 }
296
297 fn line_names(&self) -> Vec<String> {
298 vec![
299 format!(
300 "MACD({},{},{})",
301 self.fast_period, self.slow_period, self.signal_period
302 ),
303 "Signal".to_string(),
304 "Histogram".to_string(),
305 ]
306 }
307
308 fn is_visible(&self) -> bool {
309 self.visible
310 }
311
312 fn set_visible(&mut self, visible: bool) {
313 self.visible = visible;
314 }
315
316 fn clone_box(&self) -> Box<dyn Indicator> {
317 Box::new(self.clone())
318 }
319}
320
321pub(super) fn histogram_color(
324 values: &IndicatorValues,
325 bar: usize,
326 shades: &[Color32],
327) -> Option<Color32> {
328 let value = values.value(2, bar)?;
329 let previous = (0..bar).rev().find_map(|bar| values.value(2, bar));
330 let slot = if value >= 0.0 {
331 usize::from(!previous.is_some_and(|p| value > p))
332 } else {
333 2 + usize::from(previous.is_some_and(|p| value > p))
334 };
335 shades.get(slot).copied()
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341 use crate::studies::ConfiguredStudy;
342
343 #[test]
344 fn histogram_palette_survives_edits_and_saved_explicit_colours() {
345 let study = MACD {
346 values: [1.0, 2.0, 1.5, f64::NAN, -1.0, -2.0, -1.5, 0.0]
347 .into_iter()
348 .map(|value| IndicatorValue::Multiple(vec![0.0, 0.0, value]))
349 .collect(),
350 ..MACD::default()
351 };
352 let expected = [
353 Some(palette::LIGHT_TEAL),
354 Some(palette::HISTOGRAM_TEAL),
355 Some(palette::LIGHT_TEAL),
356 None,
357 Some(palette::HISTOGRAM_RED),
358 Some(palette::HISTOGRAM_RED),
359 Some(palette::LIGHT_RED),
360 Some(palette::HISTOGRAM_TEAL),
361 ];
362 let colours = |study: &dyn Indicator| -> Vec<_> {
363 (0..8).map(|bar| study.plot_color(2, bar)).collect()
364 };
365 assert_eq!(colours(&study), expected);
366 assert_eq!(study.plot_color(0, 1), None);
367 let mut configured = ConfiguredStudy::new(Box::new(study.clone()));
368 configured
369 .apply_settings(&[], configured.settings())
370 .unwrap();
371 assert_eq!(colours(&configured), expected);
372 let mut settings = configured.settings();
373 settings.plots[0].color = Color32::YELLOW;
374 configured.apply_settings(&[], settings).unwrap();
375 assert_eq!(colours(&configured), expected);
376 let snapshot = configured.snapshot_settings();
377 configured.set_color(2, Color32::WHITE);
378 assert_eq!(configured.plot_color(2, 1), Some(Color32::WHITE));
379 configured.restore_settings(&snapshot).unwrap();
380 assert_eq!(colours(&configured), expected);
381 for color in [palette::UP, palette::HISTOGRAM_TEAL, Color32::YELLOW] {
383 let mut legacy = study.clone();
384 legacy.set_colors(vec![palette::BLUE, palette::ORANGE, color]);
385 assert!(
386 (0..8)
387 .filter(|&bar| bar != 3)
388 .all(|bar| legacy.plot_color(2, bar) == Some(color))
389 );
390 let snapshot = legacy.snapshot_settings();
391 configured.restore_settings(&snapshot).unwrap();
392 assert_eq!(colours(&configured), colours(&legacy));
393 }
394 }
395}