kestrel_chartkit/indicator/
lsma.rs1use super::{Indicator, IndicatorAlert, IndicatorOutput};
2use crate::model::Bar;
3use crate::stats::linear_regression;
4use std::collections::VecDeque;
5
6#[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 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}