Skip to main content

kestrel_chartkit/indicator/
bollinger.rs

1use std::collections::{HashMap, VecDeque};
2
3use crate::model::Bar;
4
5use super::{Indicator, IndicatorAlert, IndicatorOutput};
6
7#[derive(Debug, Clone)]
8pub struct BollingerBands {
9    len: usize,
10    mult: f64,
11    window: VecDeque<f64>,
12    sum: f64,
13
14    alerts: BollingerAlerts,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Default)]
18pub struct BollingerAlerts {
19    pub lower_touch: bool,
20    pub upper_touch: bool,
21    pub percent_b: f64,
22}
23
24impl BollingerBands {
25    pub fn new(len: usize, mult: f64) -> Self {
26        Self {
27            len,
28            mult,
29            window: VecDeque::with_capacity(len),
30            sum: 0.0,
31            alerts: BollingerAlerts::default(),
32        }
33    }
34
35    pub fn with_defaults() -> Self {
36        Self::new(20, 2.0)
37    }
38}
39
40impl Indicator for BollingerBands {
41    fn name(&self) -> &str {
42        "bollinger"
43    }
44
45    fn warmup_period(&self) -> usize {
46        self.len
47    }
48
49    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
50        self.alerts = BollingerAlerts::default();
51        let close = bar.close;
52
53        self.window.push_back(close);
54        self.sum += close;
55
56        if self.window.len() > self.len {
57            self.sum -= self.window.pop_front().unwrap();
58        }
59
60        if self.window.len() < self.len {
61            return None;
62        }
63
64        let basis = self.sum / self.len as f64;
65        let variance = self
66            .window
67            .iter()
68            .map(|val| {
69                let diff = val - basis;
70                diff * diff
71            })
72            .sum::<f64>()
73            / self.len as f64;
74
75        let std_dev = variance.sqrt();
76        let upper = basis + self.mult * std_dev;
77        let lower = basis - self.mult * std_dev;
78
79        let width = if basis != 0.0 {
80            (upper - lower) / basis
81        } else {
82            0.0
83        };
84
85        let pct_b = if upper != lower {
86            (close - lower) / (upper - lower)
87        } else {
88            0.5
89        };
90
91        self.alerts.lower_touch = close <= lower;
92        self.alerts.upper_touch = close >= upper;
93        self.alerts.percent_b = pct_b;
94
95        let mut extra = HashMap::new();
96        extra.insert("basis".to_string(), basis);
97        extra.insert("upper".to_string(), upper);
98        extra.insert("lower".to_string(), lower);
99        extra.insert("bandwidth".to_string(), width);
100        extra.insert("percent_b".to_string(), pct_b);
101
102        Some(IndicatorOutput::with_extra(basis, extra))
103    }
104
105    fn reset(&mut self) {
106        self.window.clear();
107        self.sum = 0.0;
108        self.alerts = BollingerAlerts::default();
109    }
110
111    fn alerts(&self) -> Vec<IndicatorAlert> {
112        let a = self.alerts;
113        let mut out = Vec::new();
114        if a.lower_touch {
115            out.push(IndicatorAlert {
116                kind: "lower_touch".to_string(),
117                note: "BOLLINGER · TOUCHED LOWER BAND".to_string(),
118                strength: 1.0,
119            });
120        }
121        if a.upper_touch {
122            out.push(IndicatorAlert {
123                kind: "upper_touch".to_string(),
124                note: "BOLLINGER · TOUCHED UPPER BAND".to_string(),
125                strength: 1.0,
126            });
127        }
128        out
129    }
130}