Skip to main content

egui_charts/studies/builtin/
linear_regression.rs

1//! Linear Regression Indicators
2//! Includes Linear Regression Line, Slope, and R-Squared
3//! Popular for trend analysis
4
5use crate::model::Bar;
6use crate::studies::palette;
7use crate::studies::stepper::update_window;
8use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
9use egui::Color32;
10
11/// Linear regression fitted to closing prices over a rolling window.
12#[derive(Clone)]
13pub struct LinearRegression {
14    /// Lookback period (typically 14)
15    period: usize,
16    values: IndicatorValues,
17    color: Color32,
18    visible: bool,
19}
20
21impl LinearRegression {
22    /// Create with a lookback of `period` bars, clamped to at least 1.
23    pub fn new(period: usize) -> Self {
24        Self {
25            period: period.max(1),
26            values: IndicatorValues::new(),
27            color: palette::LIGHT_BLUE,
28            visible: true,
29        }
30    }
31
32    /// Set the plot color.
33    pub fn with_color(mut self, color: Color32) -> Self {
34        self.color = color;
35        self
36    }
37}
38
39impl Default for LinearRegression {
40    fn default() -> Self {
41        Self::new(14)
42    }
43}
44
45impl LinearRegression {
46    fn compute(&self, data: &[Bar]) -> IndicatorValues {
47        let mut values = IndicatorValues::new();
48
49        if data.len() < self.period {
50            for _ in 0..data.len() {
51                values.push(IndicatorValue::None);
52            }
53            return values;
54        }
55
56        for i in 0..data.len() {
57            if i < self.period - 1 {
58                values.push(IndicatorValue::None);
59            } else {
60                let window = &data[i + 1 - self.period..=i];
61                let n = self.period as f64;
62
63                let mut sum_x = 0.0;
64                let mut sum_y = 0.0;
65                let mut sum_xy = 0.0;
66                let mut sum_x2 = 0.0;
67
68                for (j, bar) in window.iter().enumerate() {
69                    let x = j as f64;
70                    let y = bar.close;
71                    sum_x += x;
72                    sum_y += y;
73                    sum_xy += x * y;
74                    sum_x2 += x * x;
75                }
76
77                let slope = (n * sum_xy - sum_x * sum_y) / (n * sum_x2 - sum_x * sum_x);
78                let intercept = (sum_y - slope * sum_x) / n;
79
80                // Value at the end of the regression line (current bar)
81                let value = intercept + slope * (self.period - 1) as f64;
82                values.push(IndicatorValue::Single(value));
83            }
84        }
85        values
86    }
87}
88
89impl Indicator for LinearRegression {
90    fn source(&self) -> Option<crate::studies::StudySource> {
91        Some(crate::studies::StudySource::Price(
92            crate::model::PriceSource::Close,
93        ))
94    }
95
96    crate::studies::inputs::study_inputs!(self;
97        period: length("Period", 1, usize::MAX)
98    ; {});
99
100    fn name(&self) -> &str {
101        "LinReg"
102    }
103
104    fn desc(&self) -> &str {
105        "Linear Regression Line - Best fit line value"
106    }
107
108    fn calculate(&mut self, data: &[Bar]) {
109        self.update(data, 0);
110    }
111
112    fn update(&mut self, data: &[Bar], from: usize) {
113        let mut values = std::mem::take(&mut self.values);
114        update_window(
115            &mut values,
116            data,
117            from,
118            |_data, from| from.saturating_sub(self.period - 1),
119            |data| self.compute(data),
120        );
121        self.values = values;
122    }
123
124    fn is_incremental(&self) -> bool {
125        true
126    }
127
128    fn values(&self) -> &IndicatorValues {
129        &self.values
130    }
131
132    fn colors(&self) -> Vec<Color32> {
133        vec![self.color]
134    }
135
136    fn set_colors(&mut self, colors: Vec<Color32>) {
137        if !colors.is_empty() {
138            self.color = colors[0];
139        }
140    }
141
142    fn is_overlay(&self) -> bool {
143        true // Overlay on price chart
144    }
145
146    fn is_visible(&self) -> bool {
147        self.visible
148    }
149
150    fn set_visible(&mut self, visible: bool) {
151        self.visible = visible;
152    }
153
154    fn clone_box(&self) -> Box<dyn Indicator> {
155        Box::new(self.clone())
156    }
157
158    fn line_names(&self) -> Vec<String> {
159        vec![format!("LinReg({})", self.period)]
160    }
161}
162
163/// Linear Regression Slope
164#[derive(Clone)]
165pub struct LinearRegressionSlope {
166    period: usize,
167    values: IndicatorValues,
168    color: Color32,
169    visible: bool,
170}
171
172impl LinearRegressionSlope {
173    /// Create with a lookback of `period` bars, clamped to at least 1.
174    pub fn new(period: usize) -> Self {
175        Self {
176            period: period.max(1),
177            values: IndicatorValues::new(),
178            color: Color32::from_rgb(255, 82, 82),
179            visible: true,
180        }
181    }
182
183    /// Set the plot color.
184    pub fn with_color(mut self, color: Color32) -> Self {
185        self.color = color;
186        self
187    }
188}
189
190impl Default for LinearRegressionSlope {
191    fn default() -> Self {
192        Self::new(14)
193    }
194}
195
196impl LinearRegressionSlope {
197    fn compute(&self, data: &[Bar]) -> IndicatorValues {
198        let mut values = IndicatorValues::new();
199
200        if data.len() < self.period {
201            for _ in 0..data.len() {
202                values.push(IndicatorValue::None);
203            }
204            return values;
205        }
206
207        for i in 0..data.len() {
208            if i < self.period - 1 {
209                values.push(IndicatorValue::None);
210            } else {
211                let window = &data[i + 1 - self.period..=i];
212                let n = self.period as f64;
213
214                let mut sum_x = 0.0;
215                let mut sum_y = 0.0;
216                let mut sum_xy = 0.0;
217                let mut sum_x2 = 0.0;
218
219                for (j, bar) in window.iter().enumerate() {
220                    let x = j as f64;
221                    let y = bar.close;
222                    sum_x += x;
223                    sum_y += y;
224                    sum_xy += x * y;
225                    sum_x2 += x * x;
226                }
227
228                let slope = (n * sum_xy - sum_x * sum_y) / (n * sum_x2 - sum_x * sum_x);
229                values.push(IndicatorValue::Single(slope));
230            }
231        }
232        values
233    }
234}
235
236impl Indicator for LinearRegressionSlope {
237    fn source(&self) -> Option<crate::studies::StudySource> {
238        Some(crate::studies::StudySource::Price(
239            crate::model::PriceSource::Close,
240        ))
241    }
242
243    crate::studies::inputs::study_inputs!(self;
244        period: length("Period", 1, usize::MAX)
245    ; {});
246
247    fn name(&self) -> &str {
248        "LinReg Slope"
249    }
250
251    fn desc(&self) -> &str {
252        "Linear Regression Slope - Rate of change"
253    }
254
255    fn calculate(&mut self, data: &[Bar]) {
256        self.update(data, 0);
257    }
258
259    fn update(&mut self, data: &[Bar], from: usize) {
260        let mut values = std::mem::take(&mut self.values);
261        update_window(
262            &mut values,
263            data,
264            from,
265            |_data, from| from.saturating_sub(self.period - 1),
266            |data| self.compute(data),
267        );
268        self.values = values;
269    }
270
271    fn is_incremental(&self) -> bool {
272        true
273    }
274
275    fn values(&self) -> &IndicatorValues {
276        &self.values
277    }
278
279    fn colors(&self) -> Vec<Color32> {
280        vec![self.color]
281    }
282
283    fn set_colors(&mut self, colors: Vec<Color32>) {
284        if !colors.is_empty() {
285            self.color = colors[0];
286        }
287    }
288
289    fn is_overlay(&self) -> bool {
290        false
291    }
292
293    fn is_visible(&self) -> bool {
294        self.visible
295    }
296
297    fn set_visible(&mut self, visible: bool) {
298        self.visible = visible;
299    }
300
301    fn clone_box(&self) -> Box<dyn Indicator> {
302        Box::new(self.clone())
303    }
304
305    fn line_names(&self) -> Vec<String> {
306        vec![format!("Slope({})", self.period)]
307    }
308}