egui_charts/studies/builtin/
bbw.rs1use super::{clamp_param, over_finite_bars};
2use crate::model::Bar;
3use crate::studies::stepper::{update_window, usable_behind};
4use crate::studies::{Indicator, IndicatorValues};
5use crate::studies::{helpers, palette};
6use egui::Color32;
7
8#[derive(Clone)]
19pub struct BollingerBandsWidth {
20 period: usize,
21 std_dev: f64,
22 expansion: usize,
23 contraction: usize,
24 values: IndicatorValues,
25 colors: Vec<Color32>,
26 visible: bool,
27}
28
29impl BollingerBandsWidth {
30 pub fn new(period: usize, std_dev: f64) -> Self {
32 Self {
33 period: period.max(1),
34 std_dev: clamp_param(std_dev, 0.0, f64::MAX),
35 expansion: 125,
36 contraction: 125,
37 values: IndicatorValues::new(),
38 colors: vec![
39 palette::BLUE,
40 palette::RED.gamma_multiply_u8(128),
41 palette::TEAL.gamma_multiply_u8(128),
42 ],
43 visible: true,
44 }
45 }
46
47 pub fn with_lookbacks(mut self, expansion: usize, contraction: usize) -> Self {
49 self.expansion = expansion.max(1);
50 self.contraction = contraction.max(1);
51 self
52 }
53}
54
55impl Default for BollingerBandsWidth {
56 fn default() -> Self {
57 Self::new(20, 2.0)
58 }
59}
60
61impl BollingerBandsWidth {
62 fn compute(&self, data: &[Bar]) -> IndicatorValues {
63 let mut widths = Vec::with_capacity(data.len());
64
65 for i in 0..data.len() {
66 if i + 1 < self.period {
67 widths.push(f64::NAN);
68 continue;
69 }
70
71 let start = i + 1 - self.period;
72 let prices: Vec<f64> = data[start..=i].iter().map(|b| b.close).collect();
73
74 let sma = prices.iter().sum::<f64>() / self.period as f64;
75 let variance =
76 prices.iter().map(|p| (p - sma).powi(2)).sum::<f64>() / self.period as f64;
77 let stdev = variance.sqrt();
78
79 let upper = sma + self.std_dev * stdev;
80 let lower = sma - self.std_dev * stdev;
81
82 widths.push(if sma == 0.0 {
83 0.0
84 } else {
85 (upper - lower) / sma * 100.0
86 });
87 }
88 let highest = helpers::highest(widths.iter().copied(), self.expansion);
89 let lowest = helpers::lowest(widths.iter().copied(), self.contraction);
90 let mut values = IndicatorValues::new();
91 for ((width, highest), lowest) in widths.into_iter().zip(highest).zip(lowest) {
92 values.push_lines(&[width, highest, lowest]);
93 }
94 values
95 }
96}
97
98impl Indicator for BollingerBandsWidth {
99 fn source(&self) -> Option<crate::studies::StudySource> {
100 Some(crate::studies::StudySource::Price(
101 crate::model::PriceSource::Close,
102 ))
103 }
104
105 crate::studies::inputs::study_inputs!(self;
106 period: length("Period", 1, usize::MAX),
107 std_dev: number("Std dev", 0.0, f64::MAX),
108 expansion: length("Highest expansion lookback", 1, usize::MAX),
109 contraction: length("Lowest contraction lookback", 1, usize::MAX)
110 ; {});
111
112 fn value_format(&self) -> crate::scales::StudyFormat {
113 crate::scales::StudyFormat::Percent
114 }
115
116 fn name(&self) -> &str {
117 "BBW"
118 }
119
120 fn desc(&self) -> &str {
121 "Bollinger Bands Width - Band width as percentage of middle band"
122 }
123
124 fn calculate(&mut self, data: &[Bar]) {
125 self.update(data, 0);
126 }
127
128 fn update(&mut self, data: &[Bar], from: usize) {
129 let mut values = std::mem::take(&mut self.values);
130 update_window(
131 &mut values,
132 data,
133 from,
134 usable_behind(
135 (self.period - 1).saturating_add(self.expansion.max(self.contraction) - 1),
136 |b| b.close.is_finite(),
137 ),
138 |data| over_finite_bars(data, |b| b.close.is_finite(), |data| self.compute(data)),
139 );
140 self.values = values;
141 }
142
143 fn is_incremental(&self) -> bool {
144 true
145 }
146
147 fn values(&self) -> &IndicatorValues {
148 &self.values
149 }
150
151 fn colors(&self) -> Vec<Color32> {
152 self.colors.clone()
153 }
154
155 fn set_colors(&mut self, colors: Vec<Color32>) {
156 for (slot, color) in self.colors.iter_mut().zip(colors) {
157 *slot = color;
158 }
159 }
160
161 fn is_overlay(&self) -> bool {
162 false
163 }
164
165 fn is_visible(&self) -> bool {
166 self.visible
167 }
168
169 fn set_visible(&mut self, visible: bool) {
170 self.visible = visible;
171 }
172
173 fn clone_box(&self) -> Box<dyn Indicator> {
174 Box::new(self.clone())
175 }
176
177 fn line_names(&self) -> Vec<String> {
178 vec![
179 format!("BBW({}, {})", self.period, self.std_dev),
180 format!("Highest Expansion({})", self.expansion),
181 format!("Lowest Contraction({})", self.contraction),
182 ]
183 }
184}