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