kestrel_chartkit/indicator/
bollinger.rs1use 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}