kestrel_chartkit/indicator/
lsma.rs1use super::{Indicator, IndicatorAlert, IndicatorOutput};
2use crate::model::Bar;
3use crate::stats::linear_regression;
4use std::collections::HashMap;
5use std::collections::VecDeque;
6
7#[derive(Debug, Clone)]
27pub struct LsmaEngine {
28 period: usize,
29 window: VecDeque<f64>,
30}
31
32impl LsmaEngine {
33 pub fn new(period: usize) -> Self {
34 Self {
35 period: period.max(2),
36 window: VecDeque::with_capacity(period),
37 }
38 }
39}
40
41impl Indicator for LsmaEngine {
42 fn name(&self) -> &str {
43 "lsma"
44 }
45
46 fn warmup_period(&self) -> usize {
47 self.period
48 }
49
50 fn reset(&mut self) {
51 self.window.clear();
52 }
53
54 fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
55 self.window.push_back(bar.close);
56 if self.window.len() > self.period {
57 self.window.pop_front();
58 }
59
60 if self.window.len() < self.period {
61 return None;
62 }
63
64 let slice: Vec<f64> = self.window.iter().copied().collect();
65 let fit = linear_regression(&slice)?;
66
67 let lsma_val = fit.slope * (self.period - 1) as f64 + fit.intercept;
69
70 let extra = HashMap::from([
71 ("slope".to_string(), fit.slope),
72 ("intercept".to_string(), fit.intercept),
73 ("r2".to_string(), fit.r2),
74 ]);
75 Some(IndicatorOutput::with_extra(lsma_val, extra))
76 }
77
78 fn alerts(&self) -> Vec<IndicatorAlert> {
79 Vec::new()
80 }
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86
87 #[test]
88 fn test_lsma_linear_trend() {
89 let mut lsma = LsmaEngine::new(5);
90 for i in 0..10 {
91 let b = Bar::new(i, 100.0, 105.0, 95.0, 100.0 + (i as f64 * 2.0), 1000.0);
92 if let Some(out) = lsma.on_bar(&b) {
93 if i == 9 {
94 assert!((out.value - 118.0).abs() < 1e-6);
95 }
96 }
97 }
98 }
99}