egui_charts/studies/builtin/
ema.rs1use crate::model::Bar;
43use crate::studies::palette;
44use crate::studies::stepper::Stepper;
45use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
46use egui::Color32;
47
48#[derive(Clone)]
53pub struct EMA {
54 period: usize,
55 values: IndicatorValues,
56 stepper: Stepper<Option<f64>>,
58 color: Color32,
59 visible: bool,
60}
61
62impl EMA {
63 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, visible: true,
74 }
75 }
76
77 pub fn with_color(mut self, color: Color32) -> Self {
79 self.color = color;
80 self
81 }
82}
83
84impl 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 if !bar.close.is_finite() {
119 return IndicatorValue::None;
120 }
121 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 #[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}