egui_charts/studies/builtin/
sma.rs1use crate::model::Bar;
38use crate::studies::palette;
39use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
40use egui::Color32;
41
42#[derive(Clone)]
47pub struct SMA {
48 period: usize,
49 values: IndicatorValues,
50 color: Color32,
51 visible: bool,
52}
53
54impl SMA {
55 pub fn new(period: usize) -> Self {
60 Self {
61 period: period.max(1),
62 values: IndicatorValues::new(),
63 color: palette::BLUE,
64 visible: true,
65 }
66 }
67
68 pub fn with_color(mut self, color: Color32) -> Self {
70 self.color = color;
71 self
72 }
73}
74
75impl Default for SMA {
77 fn default() -> Self {
78 Self::new(9)
79 }
80}
81
82impl Indicator for SMA {
83 fn source(&self) -> Option<crate::studies::StudySource> {
84 Some(crate::studies::StudySource::Price(
85 crate::model::PriceSource::Close,
86 ))
87 }
88
89 crate::studies::inputs::study_inputs!(self;
90 period: length("Period", 1, usize::MAX)
91 ; {});
92
93 fn name(&self) -> &str {
94 "SMA"
95 }
96
97 fn desc(&self) -> &str {
98 "Simple Moving Avg - Avg price over N periods"
99 }
100
101 fn calculate(&mut self, data: &[Bar]) {
102 self.update(data, 0);
103 }
104
105 fn update(&mut self, data: &[Bar], from: usize) {
106 let period = self.period;
107 self.values.truncate(from.min(data.len()));
108 for i in self.values.len()..data.len() {
109 self.values.push(if i + 1 < period {
110 IndicatorValue::None
111 } else {
112 let window = &data[i + 1 - period..=i];
114 IndicatorValue::Single(window.iter().map(|b| b.close).sum::<f64>() / period as f64)
115 });
116 }
117 }
118
119 fn is_incremental(&self) -> bool {
120 true
121 }
122
123 fn values(&self) -> &IndicatorValues {
124 &self.values
125 }
126
127 fn colors(&self) -> Vec<Color32> {
128 vec![self.color]
129 }
130
131 fn set_colors(&mut self, colors: Vec<Color32>) {
132 if !colors.is_empty() {
133 self.color = colors[0];
134 }
135 }
136
137 fn is_overlay(&self) -> bool {
138 true
139 }
140
141 fn is_visible(&self) -> bool {
142 self.visible
143 }
144
145 fn set_visible(&mut self, visible: bool) {
146 self.visible = visible;
147 }
148
149 fn clone_box(&self) -> Box<dyn Indicator> {
150 Box::new(self.clone())
151 }
152
153 fn line_names(&self) -> Vec<String> {
154 vec![format!("SMA({})", self.period)]
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161 use chrono::{Duration, Utc};
162
163 #[test]
166 fn matches_published_example() {
167 let start = Utc::now();
168 let bars: Vec<Bar> = (11..=17)
169 .enumerate()
170 .map(|(i, c)| {
171 let c = f64::from(c);
172 Bar {
173 time: start + Duration::minutes(i as i64),
174 open: c,
175 high: c,
176 low: c,
177 close: c,
178 volume: 1.0,
179 colors: None,
180 payload: None,
181 }
182 })
183 .collect();
184
185 let mut sma = SMA::new(5);
186 sma.calculate(&bars);
187
188 let got: Vec<Option<f64>> = sma
189 .values()
190 .to_rows()
191 .iter()
192 .map(|v| match v {
193 IndicatorValue::Single(x) => Some(*x),
194 _ => None,
195 })
196 .collect();
197 assert_eq!(
198 got,
199 [None, None, None, None, Some(13.0), Some(14.0), Some(15.0)]
200 );
201 }
202}