Skip to main content

egui_charts/studies/builtin/
ema.rs

1//! Exponential Moving Average (EMA) indicator.
2//!
3//! The EMA gives exponentially more weight to recent prices, making it more
4//! responsive to new information than the SMA. It is one of the most widely
5//! used technical indicators and serves as a building block for many others
6//! (MACD, Bollinger Bands, Keltner Channels, etc.).
7//!
8//! # Formula
9//!
10//! ```text
11//! multiplier = 2 / (period + 1)
12//! EMA(t) = (Close[t] - EMA[t-1]) * multiplier + EMA[t-1]
13//! ```
14//!
15//! # Seeding convention
16//!
17//! The first output value equals the first closing price (`values[0] ==
18//! data[0].close`); recursive smoothing begins at the second bar. There is no
19//! `None` warmup. This matches the seeding used by the EMAs inside [`MACD`],
20//! so the standalone indicator and the composite agree bar-for-bar.
21//!
22//! # Interpretation
23//!
24//! - Faster to react to price changes than SMA of the same period.
25//! - Common periods: 12 and 26 (used in MACD), 9 (short-term), 50/200.
26//! - EMA crossovers are popular entry/exit signals.
27//!
28//! # Default colour
29//!
30//! Blue from [`palette::BLUE`](crate::studies::palette::BLUE).
31//!
32//! # Example
33//!
34//! ```rust
35//! # let bars: Vec<egui_charts::model::Bar> = (0..60).map(|i| { let p = 100.0 + (i as f64 * 0.3).sin() * 5.0; egui_charts::model::Bar::new(chrono::Utc::now(), p, p + 1.0, p - 1.0, p + 0.5, 1_000.0) }).collect();
36//! use egui_charts::studies::{EMA, Indicator};
37//!
38//! let mut ema = EMA::new(12);
39//! ema.calculate(&bars);
40//! ```
41
42use crate::model::Bar;
43use crate::studies::palette;
44use crate::studies::stepper::Stepper;
45use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
46use egui::Color32;
47
48/// Exponential Moving Average indicator.
49///
50/// Applies an exponential weighting to closing prices so that recent bars
51/// have a greater influence. This is an overlay indicator.
52#[derive(Clone)]
53pub struct EMA {
54    period: usize,
55    values: IndicatorValues,
56    /// The EMA of the bars stepped so far, `None` before the first bar.
57    stepper: Stepper<Option<f64>>,
58    color: Color32,
59    visible: bool,
60}
61
62impl EMA {
63    /// Create a new EMA indicator.
64    ///
65    /// # Arguments
66    /// * `period` -- The smoothing period (e.g. 12, 26, 50).
67    pub fn new(period: usize) -> Self {
68        Self {
69            period: period.max(1),
70            values: IndicatorValues::new(),
71            stepper: Stepper::default(),
72            color: palette::BLUE, // Blue
73            visible: true,
74        }
75    }
76
77    /// Set a custom line colour (builder pattern).
78    pub fn with_color(mut self, color: Color32) -> Self {
79        self.color = color;
80        self
81    }
82}
83
84/// Construct with the conventional default parameters.
85impl Default for EMA {
86    fn default() -> Self {
87        Self::new(9)
88    }
89}
90
91impl Indicator for EMA {
92    fn source(&self) -> Option<crate::studies::StudySource> {
93        Some(crate::studies::StudySource::Price(
94            crate::model::PriceSource::Close,
95        ))
96    }
97
98    crate::studies::inputs::study_inputs!(self;
99        period: length("Period", 1, usize::MAX)
100    ; {});
101
102    fn name(&self) -> &str {
103        "EMA"
104    }
105
106    fn desc(&self) -> &str {
107        "Exponential Moving Avg - Weighted avg giving more importance to recent prices"
108    }
109
110    fn calculate(&mut self, data: &[Bar]) {
111        self.update(data, 0);
112    }
113
114    fn update(&mut self, data: &[Bar], from: usize) {
115        let multiplier = 2.0 / (self.period as f64 + 1.0);
116        self.stepper.run(data, from, &mut self.values, |ema, bar| {
117            // A bar without a finite close has no value and leaves the average as it was.
118            if !bar.close.is_finite() {
119                return IndicatorValue::None;
120            }
121            // Seed with the first close, then smooth from the second bar onward.
122            let next = match *ema {
123                None => bar.close,
124                Some(prev) => (bar.close - prev) * multiplier + prev,
125            };
126            *ema = Some(next);
127            IndicatorValue::Single(next)
128        });
129    }
130
131    fn is_incremental(&self) -> bool {
132        true
133    }
134
135    fn values(&self) -> &IndicatorValues {
136        &self.values
137    }
138
139    fn colors(&self) -> Vec<Color32> {
140        vec![self.color]
141    }
142
143    fn set_colors(&mut self, colors: Vec<Color32>) {
144        if !colors.is_empty() {
145            self.color = colors[0];
146        }
147    }
148
149    fn is_overlay(&self) -> bool {
150        true
151    }
152
153    fn is_visible(&self) -> bool {
154        self.visible
155    }
156
157    fn set_visible(&mut self, visible: bool) {
158        self.visible = visible;
159    }
160
161    fn clone_box(&self) -> Box<dyn Indicator> {
162        Box::new(self.clone())
163    }
164
165    fn line_names(&self) -> Vec<String> {
166        vec![format!("EMA({})", self.period)]
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173    use chrono::{Duration, Utc};
174
175    fn bars_from_closes(closes: &[f64]) -> Vec<Bar> {
176        let start = Utc::now();
177        closes
178            .iter()
179            .enumerate()
180            .map(|(i, &c)| Bar {
181                time: start + Duration::minutes(i as i64),
182                open: c,
183                high: c,
184                low: c,
185                close: c,
186                volume: 1.0,
187                colors: None,
188                payload: None,
189            })
190            .collect()
191    }
192
193    /// Reference values from pandas 3.0.2, `Series.ewm(span=4, adjust=False).mean()`,
194    /// which seeds with the first value and applies `alpha = 2 / (span + 1)`.
195    #[test]
196    fn matches_pandas_reference() {
197        let closes = [
198            22.27, 22.19, 22.08, 22.17, 22.18, 22.13, 22.23, 22.43, 22.24, 22.29,
199        ];
200        let expected = [
201            22.27,
202            22.238,
203            22.1748,
204            22.17288,
205            22.175728,
206            22.1574368,
207            22.18646208,
208            22.283877248,
209            22.2663263488,
210            22.27579580928,
211        ];
212        let bars = bars_from_closes(&closes);
213
214        let mut ema = EMA::new(4);
215        ema.calculate(&bars);
216
217        assert_eq!(ema.values().len(), expected.len());
218        for (i, (v, e)) in ema.values().to_rows().iter().zip(expected).enumerate() {
219            match v {
220                IndicatorValue::Single(v) => assert!((v - e).abs() < 1e-9, "bar {i}: {v} != {e}"),
221                _ => panic!("bar {i} must carry a value"),
222            }
223        }
224    }
225}