Skip to main content

egui_charts/studies/builtin/
standard_error_bands.rs

1use super::clamp_param;
2use crate::model::Bar;
3use crate::studies::palette;
4use crate::studies::stepper::update_window;
5use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
6use egui::Color32;
7
8/// Standard Error Bands - upper/middle (linear regression)/lower bands
9/// where upper = LinReg + multiplier * StdError, lower = LinReg - multiplier * StdError.
10#[derive(Clone)]
11pub struct StandardErrorBands {
12    period: usize,
13    multiplier: f64,
14    values: IndicatorValues,
15    colors: Vec<Color32>,
16    visible: bool,
17}
18
19impl StandardErrorBands {
20    /// `multiplier` is clamped into 0 to `f64::MAX`, NaN to 0.
21    pub fn new(period: usize, multiplier: f64) -> Self {
22        Self {
23            period: period.max(2),
24            multiplier: clamp_param(multiplier, 0.0, f64::MAX),
25            values: IndicatorValues::new(),
26            colors: vec![palette::LIGHT_BLUE, palette::ORANGE, palette::LIGHT_BLUE],
27            visible: true,
28        }
29    }
30
31    /// Compute linear regression slope, intercept, and standard error.
32    fn linreg_with_se(prices: &[f64]) -> (f64, f64, f64) {
33        let n = prices.len() as f64;
34        let mut sum_x = 0.0;
35        let mut sum_y = 0.0;
36        let mut sum_xy = 0.0;
37        let mut sum_x2 = 0.0;
38
39        for (j, &y) in prices.iter().enumerate() {
40            let x = j as f64;
41            sum_x += x;
42            sum_y += y;
43            sum_xy += x * y;
44            sum_x2 += x * x;
45        }
46
47        let denom = n * sum_x2 - sum_x * sum_x;
48        if denom.abs() < 1e-15 {
49            let mean = sum_y / n;
50            return (0.0, mean, 0.0);
51        }
52
53        let slope = (n * sum_xy - sum_x * sum_y) / denom;
54        let intercept = (sum_y - slope * sum_x) / n;
55
56        let sum_sq_err: f64 = prices
57            .iter()
58            .enumerate()
59            .map(|(j, &y)| {
60                let predicted = intercept + slope * j as f64;
61                (y - predicted).powi(2)
62            })
63            .sum();
64
65        let se = if n > 2.0 {
66            (sum_sq_err / (n - 2.0)).sqrt()
67        } else {
68            0.0
69        };
70
71        (slope, intercept, se)
72    }
73}
74
75impl Default for StandardErrorBands {
76    fn default() -> Self {
77        Self::new(20, 2.0)
78    }
79}
80
81impl StandardErrorBands {
82    fn compute(&self, data: &[Bar]) -> IndicatorValues {
83        let mut values = IndicatorValues::new();
84
85        if data.is_empty() {
86            return values;
87        }
88
89        for i in 0..data.len() {
90            if i + 1 < self.period {
91                values.push(IndicatorValue::None);
92                continue;
93            }
94
95            let start = i + 1 - self.period;
96            let prices: Vec<f64> = data[start..=i].iter().map(|b| b.close).collect();
97            let (slope, intercept, se) = Self::linreg_with_se(&prices);
98
99            // Middle = linear regression value at end of window
100            let middle = intercept + slope * (self.period - 1) as f64;
101            let upper = middle + self.multiplier * se;
102            let lower = middle - self.multiplier * se;
103
104            values.push_lines(&[upper, middle, lower]);
105        }
106        values
107    }
108}
109
110impl Indicator for StandardErrorBands {
111    fn source(&self) -> Option<crate::studies::StudySource> {
112        Some(crate::studies::StudySource::Price(
113            crate::model::PriceSource::Close,
114        ))
115    }
116
117    crate::studies::inputs::study_inputs!(self;
118        period: length("Period", 2, usize::MAX),
119        multiplier: number("Multiplier", 0.0, f64::MAX)
120    ; {});
121
122    fn name(&self) -> &str {
123        "StdErrBands"
124    }
125
126    fn desc(&self) -> &str {
127        "Standard Error Bands - Linear regression with standard error envelope"
128    }
129
130    fn calculate(&mut self, data: &[Bar]) {
131        self.update(data, 0);
132    }
133
134    fn update(&mut self, data: &[Bar], from: usize) {
135        let mut values = std::mem::take(&mut self.values);
136        update_window(
137            &mut values,
138            data,
139            from,
140            |_data, from| from.saturating_sub(self.period - 1),
141            |data| self.compute(data),
142        );
143        self.values = values;
144    }
145
146    fn is_incremental(&self) -> bool {
147        true
148    }
149
150    fn values(&self) -> &IndicatorValues {
151        &self.values
152    }
153
154    fn colors(&self) -> Vec<Color32> {
155        self.colors.clone()
156    }
157
158    fn set_colors(&mut self, colors: Vec<Color32>) {
159        if colors.len() >= 3 {
160            self.colors = colors;
161        } else if !colors.is_empty() {
162            self.colors = vec![colors[0], colors[0], colors[0]];
163        }
164    }
165
166    fn is_overlay(&self) -> bool {
167        true
168    }
169
170    fn line_count(&self) -> usize {
171        3
172    }
173
174    fn is_visible(&self) -> bool {
175        self.visible
176    }
177
178    fn set_visible(&mut self, visible: bool) {
179        self.visible = visible;
180    }
181
182    fn clone_box(&self) -> Box<dyn Indicator> {
183        Box::new(self.clone())
184    }
185
186    fn line_names(&self) -> Vec<String> {
187        vec![
188            format!("SE Upper({}, {})", self.period, self.multiplier),
189            format!("SE Middle({})", self.period),
190            format!("SE Lower({}, {})", self.period, self.multiplier),
191        ]
192    }
193}