egui_charts/studies/builtin/
lsma.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)]
34pub struct LSMA {
35 period: usize,
36 offset: i32, values: IndicatorValues,
38 color: Color32,
39 visible: bool,
40}
41
42impl LSMA {
43 pub fn new(period: usize) -> Self {
48 Self {
49 period: period.max(1),
50 offset: 0,
51 values: IndicatorValues::new(),
52 color: palette::BLUE,
53 visible: true,
54 }
55 }
56
57 pub fn with_offset(mut self, offset: i32) -> Self {
59 self.offset = offset;
60 self
61 }
62
63 pub fn with_color(mut self, color: Color32) -> Self {
65 self.color = color;
66 self
67 }
68
69 fn linear_regression(prices: &[f64], offset: i32) -> f64 {
72 let n = prices.len() as f64;
73 if n == 0.0 {
74 return 0.0;
75 }
76
77 let mut sum_x = 0.0;
80 let mut sum_y = 0.0;
81 let mut sum_xy = 0.0;
82 let mut sum_xx = 0.0;
83
84 for (i, price) in prices.iter().enumerate() {
85 let x = i as f64;
86 sum_x += x;
87 sum_y += price;
88 sum_xy += x * price;
89 sum_xx += x * x;
90 }
91
92 let denominator = n * sum_xx - sum_x * sum_x;
94 if denominator.abs() < 1e-10 {
95 return sum_y / n; }
97
98 let m = (n * sum_xy - sum_x * sum_y) / denominator;
99 let b = (sum_y - m * sum_x) / n;
100
101 let x_target = (prices.len() - 1) as f64 + offset as f64;
103 m * x_target + b
104 }
105}
106
107impl Default for LSMA {
109 fn default() -> Self {
110 Self::new(25)
111 }
112}
113
114impl LSMA {
115 fn compute(&self, data: &[Bar]) -> IndicatorValues {
116 let mut values = IndicatorValues::new();
117
118 for i in 0..data.len() {
119 if i < self.period - 1 {
120 values.push(IndicatorValue::None);
121 } else {
122 let prices: Vec<f64> = data[i + 1 - self.period..=i]
123 .iter()
124 .map(|bar| bar.close)
125 .collect();
126
127 let lsma = Self::linear_regression(&prices, self.offset);
128 values.push(IndicatorValue::Single(lsma));
129 }
130 }
131 values
132 }
133}
134
135impl Indicator for LSMA {
136 fn source(&self) -> Option<crate::studies::StudySource> {
137 Some(crate::studies::StudySource::Price(
138 crate::model::PriceSource::Close,
139 ))
140 }
141
142 crate::studies::inputs::study_inputs!(self;
143 period: length("Period", 1, usize::MAX),
144 offset: integer("Offset", i32::MIN, i32::MAX)
145 ; {});
146
147 fn name(&self) -> &str {
148 "LSMA"
149 }
150
151 fn desc(&self) -> &str {
152 "Least Squares Moving Avg - Linear regression line"
153 }
154
155 fn calculate(&mut self, data: &[Bar]) {
156 self.update(data, 0);
157 }
158
159 fn update(&mut self, data: &[Bar], from: usize) {
160 let mut values = std::mem::take(&mut self.values);
161 update_window(
162 &mut values,
163 data,
164 from,
165 |_data, from| from.saturating_sub(self.period - 1),
166 |data| self.compute(data),
167 );
168 self.values = values;
169 }
170
171 fn is_incremental(&self) -> bool {
172 true
173 }
174
175 fn values(&self) -> &IndicatorValues {
176 &self.values
177 }
178
179 fn colors(&self) -> Vec<Color32> {
180 vec![self.color]
181 }
182
183 fn set_colors(&mut self, colors: Vec<Color32>) {
184 if !colors.is_empty() {
185 self.color = colors[0];
186 }
187 }
188
189 fn is_overlay(&self) -> bool {
190 true
191 }
192
193 fn is_visible(&self) -> bool {
194 self.visible
195 }
196
197 fn set_visible(&mut self, visible: bool) {
198 self.visible = visible;
199 }
200
201 fn clone_box(&self) -> Box<dyn Indicator> {
202 Box::new(self.clone())
203 }
204
205 fn line_names(&self) -> Vec<String> {
206 if self.offset != 0 {
207 vec![format!("LSMA({}, {})", self.period, self.offset)]
208 } else {
209 vec![format!("LSMA({})", self.period)]
210 }
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217 use crate::studies::test_support::make_bar;
218
219 #[test]
220 fn regression_endpoint_and_offset_match_hand_values() {
221 for (closes, offset, expected) in [
222 ([100.0; 5], 0, 100.0),
223 ([100.0, 101.0, 102.0, 103.0, 104.0], 0, 104.0),
224 ([100.0, 101.0, 102.0, 103.0, 104.0], 1, 105.0),
225 ] {
226 let data: Vec<Bar> = closes.into_iter().map(make_bar).collect();
227 let mut lsma = LSMA::new(data.len()).with_offset(offset);
228 lsma.calculate(&data);
229 let got = lsma.values().value(0, data.len() - 1).unwrap();
230 assert!((got - expected).abs() < 1e-9, "{closes:?}, offset {offset}");
231 }
232 }
233
234 #[test]
235 fn test_lsma_calculation() {
236 let mut lsma = LSMA::new(3);
237
238 let data = vec![
239 make_bar(100.0),
240 make_bar(101.0),
241 make_bar(102.0),
242 make_bar(103.0),
243 ];
244
245 lsma.calculate(&data);
246
247 assert_eq!(lsma.values.len(), 4);
248 assert!(matches!(lsma.values.to_rows()[0], IndicatorValue::None));
249 assert!(matches!(lsma.values.to_rows()[1], IndicatorValue::None));
250
251 let IndicatorValue::Single(v) = lsma.values.to_rows()[2] else {
253 panic!("no value: {:?}", lsma.values.to_rows()[2]);
254 };
255 assert!((v - 102.0).abs() < 0.01);
256 }
257}