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