egui_charts/studies/builtin/
percent_b.rs1use super::clamp_param;
2use crate::model::Bar;
3use crate::studies::palette;
4use crate::studies::stepper::update_window;
5use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
6use egui::Color32;
7
8#[derive(Clone)]
12pub struct PercentB {
13 period: usize,
15 multiplier: f64,
17 values: IndicatorValues,
18 color: Color32,
19 visible: bool,
20}
21
22impl PercentB {
23 pub fn new(period: usize, multiplier: f64) -> Self {
25 Self {
26 period: period.max(1),
27 multiplier: clamp_param(multiplier, 0.0, f64::MAX),
28 values: IndicatorValues::new(),
29 color: palette::BLUE,
30 visible: true,
31 }
32 }
33
34 pub fn with_color(mut self, color: Color32) -> Self {
36 self.color = color;
37 self
38 }
39}
40
41impl Default for PercentB {
42 fn default() -> Self {
43 Self::new(20, 2.0)
44 }
45}
46
47impl PercentB {
48 fn compute(&self, data: &[Bar]) -> IndicatorValues {
49 let mut values = IndicatorValues::new();
50
51 if data.len() < self.period {
52 for _ in 0..data.len() {
53 values.push(IndicatorValue::None);
54 }
55 return values;
56 }
57
58 for i in 0..data.len() {
59 if i < self.period - 1 {
60 values.push(IndicatorValue::None);
61 } else {
62 let window = &data[i + 1 - self.period..=i];
63
64 let sma: f64 = window.iter().map(|b| b.close).sum::<f64>() / self.period as f64;
66
67 let variance: f64 = window.iter().map(|b| (b.close - sma).powi(2)).sum::<f64>()
69 / self.period as f64;
70 let std_dev = variance.sqrt();
71
72 let upper = sma + self.multiplier * std_dev;
74 let lower = sma - self.multiplier * std_dev;
75
76 let band_width = upper - lower;
78 if band_width == 0.0 {
79 values.push(IndicatorValue::Single(0.5));
80 } else {
81 let percent_b = (data[i].close - lower) / band_width;
82 values.push(IndicatorValue::Single(percent_b));
83 }
84 }
85 }
86 values
87 }
88}
89
90impl Indicator for PercentB {
91 fn source(&self) -> Option<crate::studies::StudySource> {
92 Some(crate::studies::StudySource::Price(
93 crate::model::PriceSource::Close,
94 ))
95 }
96
97 crate::studies::inputs::study_inputs!(self;
98 period: length("Period", 1, usize::MAX),
99 multiplier: number("Multiplier", 0.0, f64::MAX)
100 ; {});
101
102 fn name(&self) -> &str {
103 "%B"
104 }
105
106 fn desc(&self) -> &str {
107 "Percent B - Price position within Bollinger Bands"
108 }
109
110 fn calculate(&mut self, data: &[Bar]) {
111 self.update(data, 0);
112 }
113
114 fn update(&mut self, data: &[Bar], from: usize) {
115 let mut values = std::mem::take(&mut self.values);
116 update_window(
117 &mut values,
118 data,
119 from,
120 |_data, from| from.saturating_sub(self.period - 1),
121 |data| self.compute(data),
122 );
123 self.values = values;
124 }
125
126 fn is_incremental(&self) -> bool {
127 true
128 }
129
130 fn values(&self) -> &IndicatorValues {
131 &self.values
132 }
133
134 fn colors(&self) -> Vec<Color32> {
135 vec![self.color]
136 }
137
138 fn set_colors(&mut self, colors: Vec<Color32>) {
139 if !colors.is_empty() {
140 self.color = colors[0];
141 }
142 }
143
144 fn is_overlay(&self) -> bool {
145 false
146 }
147
148 fn is_visible(&self) -> bool {
149 self.visible
150 }
151
152 fn set_visible(&mut self, visible: bool) {
153 self.visible = visible;
154 }
155
156 fn clone_box(&self) -> Box<dyn Indicator> {
157 Box::new(self.clone())
158 }
159
160 fn line_names(&self) -> Vec<String> {
161 vec![format!("%B({},{})", self.period, self.multiplier)]
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168 use crate::studies::test_support::make_bar;
169
170 #[test]
171 fn test_percent_b() {
172 let mut pb = PercentB::new(5, 2.0);
173
174 let data: Vec<Bar> = (0..10).map(|_| make_bar(100.0)).collect();
176
177 pb.calculate(&data);
178
179 let rows = pb.values.to_rows();
180 let IndicatorValue::Single(v) = rows.last().unwrap() else {
181 panic!("no value: {:?}", pb.values.to_rows().last().unwrap());
182 };
183 assert_eq!(*v, 0.5);
185 }
186}