egui_charts/studies/builtin/
std_dev.rs1use crate::model::Bar;
24use crate::studies::palette;
25use crate::studies::stepper::update_window;
26use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
27use egui::Color32;
28
29#[derive(Clone)]
31pub struct StandardDeviation {
32 period: usize,
33 values: IndicatorValues,
34 color: Color32,
35 visible: bool,
36}
37
38impl StandardDeviation {
39 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 pub fn default_params() -> Self {
54 Self::new(20)
55 }
56
57 pub fn with_color(mut self, color: Color32) -> Self {
59 self.color = color;
60 self
61 }
62}
63
64impl 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 let sum: f64 = data[start..=i].iter().map(|b| b.close).sum();
87 let mean = sum / self.period as f64;
88
89 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 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 }
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 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}