Skip to main content

egui_charts/studies/builtin/
bbw.rs

1use 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/// Bollinger Bands Width - measures how wide the Bollinger Bands are.
9/// Formula: (upper_band - lower_band) / middle_band * 100
10/// where middle = SMA(close, period), upper = middle + std_dev * stdev,
11/// lower = middle - std_dev * stdev.
12///
13/// Outputs are the width, the highest width of the last `expansion` bars (highest
14/// expansion) and the lowest width of the last `contraction` bars (lowest contraction),
15/// each window ending at the current bar. The width starts at bar `period - 1` and both
16/// extrema start with it. A bar with a non-finite close is skipped as if absent: it has no
17/// value and is in no window, so later values equal those of the series without it.
18#[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    /// `std_dev` is clamped into 0 to `f64::MAX`, NaN to 0.
31    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    /// Set the highest expansion and lowest contraction lookbacks, each at least 1 bar.
48    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}