Skip to main content

egui_charts/studies/builtin/
sma.rs

1//! Simple Moving Average (SMA) indicator.
2//!
3//! The SMA is the most fundamental moving average, computed as the
4//! unweighted arithmetic mean of the closing prices over the last `N`
5//! periods. It is an overlay indicator drawn directly on the price chart.
6//!
7//! # Formula
8//!
9//! ```text
10//! SMA(t) = (Close[t] + Close[t-1] + ... + Close[t-N+1]) / N
11//! ```
12//!
13//! # Interpretation
14//!
15//! - Price above SMA: bullish bias.
16//! - Price below SMA: bearish bias.
17//! - Popular periods: 20 (short-term), 50 (medium-term), 200 (long-term).
18//! - SMA crossovers (e.g. 50/200 "golden cross") are widely-used signals.
19//!
20//! # Default colour
21//!
22//! Blue from [`palette::BLUE`](crate::studies::palette::BLUE).
23//!
24//! # Example
25//!
26//! ```rust
27//! # 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();
28//! use egui_charts::studies::{SMA, Indicator, IndicatorValue};
29//!
30//! let mut sma = SMA::new(20);
31//! sma.calculate(&bars);
32//! // The first 19 values are warmup.
33//! assert!(sma.values().to_rows()[..19].iter().all(|v| matches!(v, IndicatorValue::None)));
34//! assert!(matches!(sma.values().to_rows()[19], IndicatorValue::Single(_)));
35//! ```
36
37use crate::model::Bar;
38use crate::studies::palette;
39use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
40use egui::Color32;
41
42/// Simple Moving Average indicator.
43///
44/// Computes the arithmetic mean of closing prices over a rolling window of
45/// `period` bars. This is an overlay indicator (drawn on the price chart).
46#[derive(Clone)]
47pub struct SMA {
48    period: usize,
49    values: IndicatorValues,
50    color: Color32,
51    visible: bool,
52}
53
54impl SMA {
55    /// Create a new SMA indicator.
56    ///
57    /// # Arguments
58    /// * `period` -- The number of bars in the averaging window (e.g. 20, 50, 200).
59    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    /// Set a custom line colour (builder pattern).
69    pub fn with_color(mut self, color: Color32) -> Self {
70        self.color = color;
71        self
72    }
73}
74
75/// Construct with the conventional default parameters.
76impl 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                // Summed afresh each bar so long series do not accumulate rounding drift.
113                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    /// Closes 11 to 17 give a 5-bar SMA of 13, 14 and 15, the means of 11 to 15, 12 to 16
164    /// and 13 to 17.
165    #[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}