Skip to main content

kestrel_chartkit/indicator/
lsma.rs

1use super::{Indicator, IndicatorAlert, IndicatorOutput};
2use crate::model::Bar;
3use crate::stats::linear_regression;
4use std::collections::VecDeque;
5
6/// Least Squares Moving Average (LSMA) Engine / Linear Regression Endpoint.
7#[derive(Debug, Clone)]
8pub struct LsmaEngine {
9    period: usize,
10    window: VecDeque<f64>,
11}
12
13impl LsmaEngine {
14    pub fn new(period: usize) -> Self {
15        Self {
16            period: period.max(2),
17            window: VecDeque::with_capacity(period),
18        }
19    }
20}
21
22impl Indicator for LsmaEngine {
23    fn name(&self) -> &str {
24        "lsma"
25    }
26
27    fn warmup_period(&self) -> usize {
28        self.period
29    }
30
31    fn reset(&mut self) {
32        self.window.clear();
33    }
34
35    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
36        self.window.push_back(bar.close);
37        if self.window.len() > self.period {
38            self.window.pop_front();
39        }
40
41        if self.window.len() < self.period {
42            return None;
43        }
44
45        let slice: Vec<f64> = self.window.iter().copied().collect();
46        let fit = linear_regression(&slice)?;
47
48        // Endpoint prediction at x = N - 1
49        let lsma_val = fit.slope * (self.period - 1) as f64 + fit.intercept;
50        Some(IndicatorOutput::new(lsma_val))
51    }
52
53    fn alerts(&self) -> Vec<IndicatorAlert> {
54        Vec::new()
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61
62    #[test]
63    fn test_lsma_linear_trend() {
64        let mut lsma = LsmaEngine::new(5);
65        for i in 0..10 {
66            let b = Bar::new(i, 100.0, 105.0, 95.0, 100.0 + (i as f64 * 2.0), 1000.0);
67            if let Some(out) = lsma.on_bar(&b) {
68                if i == 9 {
69                    assert!((out.value - 118.0).abs() < 1e-6);
70                }
71            }
72        }
73    }
74}