Skip to main content

egui_charts/studies/builtin/
bbtrend.rs

1//! BBTrend
2//!
3//! John Bollinger's BBTrend compares a short and a long set of Bollinger Bands on the close to
4//! measure the direction and strength of a trend: positive when the lower bands spread further
5//! apart than the upper bands (an uptrend), negative in the opposite case.
6//!
7//! # Formula
8//! ```text
9//! lower, middle, upper = Bollinger Bands(close, length, multiplier), population deviation
10//! BBTrend = (|lowerShort - lowerLong| - |upperShort - upperLong|) / middleShort * 100
11//! ```
12//! with conventional lengths 20 and 50 and multiplier 2. The first value is on the bar where the
13//! longer band is complete.
14//!
15//! # Presentation
16//! Columns: above zero, strong green while BBTrend rises and light green otherwise; at or
17//! below zero, strong red while it falls and light red otherwise.
18//!
19//! # Example
20//! ```
21//! # let bars: Vec<egui_charts::model::Bar> = (0..80).map(|i| { let p = 100.0 + (i as f64 * 0.3).sin() * 5.0; egui_charts::model::Bar::new(chrono::Utc::now(), p, p + 1.0, p - 1.0, p + 0.5, 1_000.0) }).collect();
22//! use egui_charts::studies::{BBTrend, Indicator};
23//!
24//! let mut trend = BBTrend::new(20, 50, 2.0);
25//! trend.calculate(&bars);
26//! assert!(trend.values().value(0, 49).is_some());
27//! ```
28
29use super::{BollingerBands, clamp_param, over_finite_bars};
30use crate::model::Bar;
31use crate::studies::palette;
32use crate::studies::stepper::{update_window, usable_behind};
33use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
34use egui::Color32;
35
36/// Trend strength from a short and a long Bollinger Band.
37#[derive(Clone)]
38pub struct BBTrend {
39    short_length: usize,
40    long_length: usize,
41    multiplier: f64,
42    values: IndicatorValues,
43    /// Rising above zero, easing above zero, falling at or below zero, easing at or below zero.
44    colors: [Color32; 4],
45    visible: bool,
46}
47
48impl BBTrend {
49    /// Create a BBTrend from the lengths of its two bands and their deviation multiplier.
50    /// The multiplier is clamped into 0 to `f64::MAX`, NaN to 0.
51    pub fn new(short_length: usize, long_length: usize, multiplier: f64) -> Self {
52        let (up, down) = (palette::TEAL, palette::RED);
53        Self {
54            short_length: short_length.max(1),
55            long_length: long_length.max(1),
56            multiplier: clamp_param(multiplier, 0.0, f64::MAX),
57            values: IndicatorValues::new(),
58            colors: [
59                up.gamma_multiply_u8(191),
60                up.gamma_multiply_u8(128),
61                down.gamma_multiply_u8(191),
62                down.gamma_multiply_u8(128),
63            ],
64            visible: true,
65        }
66    }
67
68    /// The values over `data`, every bar of which has a finite close.
69    fn compute(&self, data: &[Bar]) -> IndicatorValues {
70        let bands = |length| {
71            let mut bands = BollingerBands::new(length, self.multiplier);
72            bands.calculate(data);
73            bands
74        };
75        let (short, long) = (bands(self.short_length), bands(self.long_length));
76        let (short, long) = (short.values(), long.values());
77        (0..data.len())
78            .map(|bar| {
79                let line =
80                    |values: &IndicatorValues, line| values.value(line, bar).unwrap_or(f64::NAN);
81                IndicatorValue::Single(
82                    ((line(short, 2) - line(long, 2)).abs()
83                        - (line(short, 0) - line(long, 0)).abs())
84                        / line(short, 1)
85                        * 100.0,
86                )
87            })
88            .collect()
89    }
90}
91
92impl Default for BBTrend {
93    fn default() -> Self {
94        Self::new(20, 50, 2.0)
95    }
96}
97
98impl Indicator for BBTrend {
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        short_length: length("Short BB Length", 1, usize::MAX),
107        long_length: length("Long BB Length", 1, usize::MAX),
108        multiplier: number("StdDev", 0.0, f64::MAX)
109    ; {});
110
111    fn plot_style(&self, _line: usize) -> crate::studies::PlotStyle {
112        crate::studies::PlotStyle {
113            color: self.colors[0],
114            width: 1.0,
115            pattern: crate::model::LineStyle::Solid,
116            plot_type: crate::studies::PlotType::Columns,
117            visible: true,
118        }
119    }
120
121    fn plot_color(&self, _line: usize, bar: usize) -> Option<Color32> {
122        let value = self.values.value(0, bar)?;
123        // Bars without a value, skipped as non-finite, do not count as the previous one.
124        let previous = (0..bar).rev().find_map(|bar| self.values.value(0, bar));
125        Some(if value == 0.0 {
126            self.colors[1]
127        } else if value > 0.0 {
128            self.colors[usize::from(!previous.is_some_and(|p| value >= p))]
129        } else {
130            self.colors[2 + usize::from(!previous.is_some_and(|p| value <= p))]
131        })
132    }
133
134    fn name(&self) -> &str {
135        "BBTrend"
136    }
137
138    fn desc(&self) -> &str {
139        "BBTrend - Trend strength from a short and a long Bollinger Band"
140    }
141
142    fn calculate(&mut self, data: &[Bar]) {
143        self.update(data, 0);
144    }
145
146    fn update(&mut self, data: &[Bar], from: usize) {
147        let mut values = std::mem::take(&mut self.values);
148        update_window(
149            &mut values,
150            data,
151            from,
152            usable_behind(self.short_length.max(self.long_length) - 1, |b| {
153                b.close.is_finite()
154            }),
155            |data| over_finite_bars(data, |b| b.close.is_finite(), |data| self.compute(data)),
156        );
157        self.values = values;
158    }
159
160    fn is_incremental(&self) -> bool {
161        true
162    }
163
164    fn values(&self) -> &IndicatorValues {
165        &self.values
166    }
167
168    fn colors(&self) -> Vec<Color32> {
169        self.colors.to_vec()
170    }
171
172    fn set_colors(&mut self, colors: Vec<Color32>) {
173        for (slot, color) in self.colors.iter_mut().zip(colors) {
174            *slot = color;
175        }
176    }
177
178    fn is_overlay(&self) -> bool {
179        false
180    }
181
182    fn line_count(&self) -> usize {
183        1
184    }
185
186    fn is_visible(&self) -> bool {
187        self.visible
188    }
189
190    fn set_visible(&mut self, visible: bool) {
191        self.visible = visible;
192    }
193
194    fn clone_box(&self) -> Box<dyn Indicator> {
195        Box::new(self.clone())
196    }
197
198    fn line_names(&self) -> Vec<String> {
199        vec![format!(
200            "BBTrend({}, {}, {})",
201            self.short_length, self.long_length, self.multiplier
202        )]
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    /// Above zero the column is strong while BBTrend rises and light otherwise; at or below zero
211    /// it is strong while BBTrend falls and light otherwise, and light without a previous value.
212    #[test]
213    fn columns_are_coloured_by_sign_and_direction() {
214        let study = BBTrend {
215            values: [1.0, 2.0, 1.5, -1.0, -2.0, -1.5, 0.0]
216                .into_iter()
217                .map(IndicatorValue::Single)
218                .collect(),
219            ..BBTrend::default()
220        };
221        let colors = study.colors();
222        let slots: Vec<usize> = (0..7)
223            .map(|bar| {
224                let color = study.plot_color(0, bar).unwrap();
225                colors.iter().position(|&c| c == color).unwrap()
226            })
227            .collect();
228        assert_eq!(slots, [1, 0, 1, 2, 2, 3, 1]);
229    }
230}