egui_charts/studies/builtin/
bbtrend.rs1use 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#[derive(Clone)]
38pub struct BBTrend {
39 short_length: usize,
40 long_length: usize,
41 multiplier: f64,
42 values: IndicatorValues,
43 colors: [Color32; 4],
45 visible: bool,
46}
47
48impl BBTrend {
49 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 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 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 #[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}