Skip to main content

kestrel_chartkit/indicator/
envelope.rs

1use super::smoothing::Sma;
2use super::{Indicator, IndicatorAlert, IndicatorOutput};
3use crate::model::Bar;
4use std::collections::HashMap;
5
6/// Moving Average Envelopes Engine (% upper and lower bands around SMA).
7#[derive(Debug, Clone)]
8pub struct EnvelopeEngine {
9    period: usize,
10    percent: f64,
11    sma: Sma,
12}
13
14impl EnvelopeEngine {
15    pub fn new(period: usize, percent: f64) -> Self {
16        Self {
17            period: period.max(1),
18            percent: percent.max(0.001),
19            sma: Sma::new(period),
20        }
21    }
22}
23
24impl Indicator for EnvelopeEngine {
25    fn name(&self) -> &str {
26        "envelope"
27    }
28
29    fn warmup_period(&self) -> usize {
30        self.period
31    }
32
33    fn reset(&mut self) {
34        self.sma.reset();
35    }
36
37    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
38        let basis = self.sma.update(bar.close)?;
39        let band_margin = basis * (self.percent / 100.0);
40
41        let upper = basis + band_margin;
42        let lower = basis - band_margin;
43
44        let mut extra = HashMap::new();
45        extra.insert("basis".to_string(), basis);
46        extra.insert("upper".to_string(), upper);
47        extra.insert("lower".to_string(), lower);
48
49        Some(IndicatorOutput::with_extra(basis, extra))
50    }
51
52    fn alerts(&self) -> Vec<IndicatorAlert> {
53        Vec::new()
54    }
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    #[test]
62    fn test_envelope_bands() {
63        let mut env = EnvelopeEngine::new(5, 5.0);
64        let mut out = None;
65        for i in 0..10 {
66            let b = Bar::new(i, 100.0, 105.0, 95.0, 100.0, 1000.0);
67            out = env.on_bar(&b);
68        }
69        let o = out.unwrap();
70        assert_eq!(o.extra["basis"], 100.0);
71        assert_eq!(o.extra["upper"], 105.0);
72        assert_eq!(o.extra["lower"], 95.0);
73    }
74}