Skip to main content

egui_charts/studies/builtin/
std_dev.rs

1//! Standard Deviation Indicator
2//!
3//! Standard Deviation measures price volatility by calculating the dispersion
4//! of prices from their moving avg.
5//!
6//! # Formula
7//! StdDev = sqrt(Σ(price - SMA)² / period)
8//!
9//! # Interpretation
10//! - High StdDev: High volatility, larger price movements
11//! - Low StdDev: Low volatility, consolidation period
12//! - Often used as component of Bollinger Bands
13//!
14//! # Example
15//! ```
16//! # 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();
17//! use egui_charts::studies::{StandardDeviation, Indicator};
18//!
19//! let mut std_dev = StandardDeviation::new(20);
20//! std_dev.calculate(&bars);
21//! ```
22
23use crate::model::Bar;
24use crate::studies::palette;
25use crate::studies::stepper::update_window;
26use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
27use egui::Color32;
28
29/// Standard Deviation indicator
30#[derive(Clone)]
31pub struct StandardDeviation {
32    period: usize,
33    values: IndicatorValues,
34    color: Color32,
35    visible: bool,
36}
37
38impl StandardDeviation {
39    /// Create a new Standard Deviation indicator
40    ///
41    /// # Arguments
42    /// * `period` - Lookback period (typically 20)
43    pub fn new(period: usize) -> Self {
44        Self {
45            period: period.max(1),
46            values: IndicatorValues::new(),
47            color: palette::TEAL,
48            visible: true,
49        }
50    }
51
52    /// Create with default param (20)
53    pub fn default_params() -> Self {
54        Self::new(20)
55    }
56
57    /// Set the line color
58    pub fn with_color(mut self, color: Color32) -> Self {
59        self.color = color;
60        self
61    }
62}
63
64/// Construct with the conventional default parameters.
65impl Default for StandardDeviation {
66    fn default() -> Self {
67        Self::new(20)
68    }
69}
70
71impl StandardDeviation {
72    fn compute(&self, data: &[Bar]) -> IndicatorValues {
73        let mut values = IndicatorValues::new();
74
75        if data.is_empty() {
76            return values;
77        }
78
79        for i in 0..data.len() {
80            if i + 1 < self.period {
81                values.push(IndicatorValue::None);
82            } else {
83                let start = i + 1 - self.period;
84
85                // Calculate mean (SMA)
86                let sum: f64 = data[start..=i].iter().map(|b| b.close).sum();
87                let mean = sum / self.period as f64;
88
89                // Calculate variance
90                let variance: f64 = data[start..=i]
91                    .iter()
92                    .map(|b| (b.close - mean).powi(2))
93                    .sum::<f64>()
94                    / self.period as f64;
95
96                // Standard deviation is square root of variance
97                let std_dev = variance.sqrt();
98                values.push(IndicatorValue::Single(std_dev));
99            }
100        }
101        values
102    }
103}
104
105impl Indicator for StandardDeviation {
106    fn source(&self) -> Option<crate::studies::StudySource> {
107        Some(crate::studies::StudySource::Price(
108            crate::model::PriceSource::Close,
109        ))
110    }
111
112    crate::studies::inputs::study_inputs!(self;
113        period: length("Period", 1, usize::MAX)
114    ; {});
115
116    fn value_format(&self) -> crate::scales::StudyFormat {
117        crate::scales::StudyFormat::Price
118    }
119
120    fn name(&self) -> &str {
121        "StdDev"
122    }
123
124    fn desc(&self) -> &str {
125        "Standard Deviation - Measures price volatility"
126    }
127
128    fn calculate(&mut self, data: &[Bar]) {
129        self.update(data, 0);
130    }
131
132    fn update(&mut self, data: &[Bar], from: usize) {
133        let mut values = std::mem::take(&mut self.values);
134        update_window(
135            &mut values,
136            data,
137            from,
138            |_data, from| from.saturating_sub(self.period - 1),
139            |data| self.compute(data),
140        );
141        self.values = values;
142    }
143
144    fn is_incremental(&self) -> bool {
145        true
146    }
147
148    fn values(&self) -> &IndicatorValues {
149        &self.values
150    }
151
152    fn colors(&self) -> Vec<Color32> {
153        vec![self.color]
154    }
155
156    fn set_colors(&mut self, colors: Vec<Color32>) {
157        if !colors.is_empty() {
158            self.color = colors[0];
159        }
160    }
161
162    fn is_overlay(&self) -> bool {
163        false // StdDev is typically in a separate pane
164    }
165
166    fn line_count(&self) -> usize {
167        1
168    }
169
170    fn is_visible(&self) -> bool {
171        self.visible
172    }
173
174    fn set_visible(&mut self, visible: bool) {
175        self.visible = visible;
176    }
177
178    fn clone_box(&self) -> Box<dyn Indicator> {
179        Box::new(self.clone())
180    }
181
182    fn line_names(&self) -> Vec<String> {
183        vec![format!("StdDev({})", self.period)]
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190    use chrono::{Duration, Utc};
191
192    #[test]
193    fn test_std_dev_flat_prices() {
194        // 1234.56 is inexact in binary, so a one-pass variance goes slightly negative here.
195        let start = Utc::now();
196        let bars: Vec<Bar> = (0..30)
197            .map(|i| Bar {
198                time: start + Duration::minutes(i * 5),
199                open: 1234.56,
200                high: 1234.56,
201                low: 1234.56,
202                close: 1234.56,
203                volume: 1000.0,
204                colors: None,
205                payload: None,
206            })
207            .collect();
208
209        let mut std_dev = StandardDeviation::new(20);
210        std_dev.calculate(&bars);
211
212        for value in std_dev.values().to_rows().iter().skip(19) {
213            assert!(
214                matches!(value, IndicatorValue::Single(v) if v.abs() < 1e-9),
215                "a window without deviation reads 0, got {value:?}"
216            );
217        }
218    }
219}