egui_charts/studies/builtin/
acceleration_bands.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)]
14pub struct AccelerationBands {
15 period: usize,
17 factor: f64,
19 values: IndicatorValues,
20 colors: Vec<Color32>,
22 visible: bool,
23}
24
25impl AccelerationBands {
26 pub fn new(period: usize, factor: f64) -> Self {
28 Self {
29 period: period.max(1),
30 factor: clamp_param(factor, 0.0, f64::MAX),
31 values: IndicatorValues::new(),
32 colors: vec![
33 palette::BLUE, palette::BB_MIDDLE, palette::BLUE, ],
37 visible: true,
38 }
39 }
40
41 pub fn with_colors(mut self, colors: Vec<Color32>) -> Self {
43 self.colors = colors;
44 self
45 }
46}
47
48impl Default for AccelerationBands {
49 fn default() -> Self {
50 Self::new(20, 4.0)
51 }
52}
53
54impl AccelerationBands {
55 fn compute(&self, data: &[Bar]) -> IndicatorValues {
56 let mut values = IndicatorValues::new();
57
58 if data.len() < self.period {
59 for _ in 0..data.len() {
60 values.push(IndicatorValue::None);
61 }
62 return values;
63 }
64
65 for i in 0..data.len() {
66 if i < self.period - 1 {
67 values.push(IndicatorValue::None);
68 } else {
69 let window = &data[i + 1 - self.period..=i];
70
71 let n = self.period as f64;
72 let (mut upper, mut middle, mut lower) = (0.0, 0.0, 0.0);
73 for b in window {
74 let r = if b.high + b.low != 0.0 {
75 (b.high - b.low) / (b.high + b.low)
76 } else {
77 0.0
78 };
79 upper += b.high * (1.0 + self.factor * r);
80 middle += b.close;
81 lower += b.low * (1.0 - self.factor * r);
82 }
83
84 values.push_lines(&[upper / n, middle / n, lower / n]);
85 }
86 }
87 values
88 }
89}
90
91impl Indicator for AccelerationBands {
92 crate::studies::inputs::study_inputs!(self;
93 period: length("Period", 1, usize::MAX),
94 factor: number("Factor", 0.0, f64::MAX)
95 ; {});
96
97 fn name(&self) -> &str {
98 "AccBands"
99 }
100
101 fn desc(&self) -> &str {
102 "Acceleration Bands - Volatility-based price envelope"
103 }
104
105 fn calculate(&mut self, data: &[Bar]) {
106 self.update(data, 0);
107 }
108
109 fn update(&mut self, data: &[Bar], from: usize) {
110 let mut values = std::mem::take(&mut self.values);
111 update_window(
112 &mut values,
113 data,
114 from,
115 |_data, from| from.saturating_sub(self.period - 1),
116 |data| self.compute(data),
117 );
118 self.values = values;
119 }
120
121 fn is_incremental(&self) -> bool {
122 true
123 }
124
125 fn values(&self) -> &IndicatorValues {
126 &self.values
127 }
128
129 fn colors(&self) -> Vec<Color32> {
130 self.colors.clone()
131 }
132
133 fn set_colors(&mut self, colors: Vec<Color32>) {
134 if colors.len() >= 3 {
135 self.colors = colors;
136 }
137 }
138
139 fn is_overlay(&self) -> bool {
140 true
141 }
142
143 fn line_count(&self) -> usize {
144 3
145 }
146
147 fn is_visible(&self) -> bool {
148 self.visible
149 }
150
151 fn set_visible(&mut self, visible: bool) {
152 self.visible = visible;
153 }
154
155 fn clone_box(&self) -> Box<dyn Indicator> {
156 Box::new(self.clone())
157 }
158
159 fn line_names(&self) -> Vec<String> {
160 vec![
161 format!("Upper({})", self.period),
162 "Middle".to_string(),
163 format!("Lower({})", self.period),
164 ]
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use crate::studies::test_support::make_range_bar as make_bar;
172
173 #[test]
178 fn matches_reference_values() {
179 let bars = [
180 (101.0, 100.0, 100.5),
181 (110.0, 98.0, 108.0),
182 (104.0, 102.0, 103.0),
183 (115.0, 105.0, 112.0),
184 (111.0, 109.5, 110.0),
185 (120.0, 108.0, 118.0),
186 ];
187 let expected = [
188 [115.47780019494263, 103.83333333333333, 90.47780019494263],
189 [126.44418041505419, 107.66666666666667, 86.44418041505419],
190 [119.32277800793752, 108.33333333333333, 96.82277800793752],
191 [131.73088565569768, 113.33333333333333, 92.56421898903102],
192 ];
193
194 let mut ab = AccelerationBands::new(3, 4.0);
195 let data: Vec<Bar> = bars.iter().map(|&(h, l, c)| make_bar(h, l, c)).collect();
196 ab.calculate(&data);
197
198 let mut failures = Vec::new();
199 assert_eq!(ab.values.len(), bars.len());
200 assert!(matches!(
201 ab.values.to_rows()[..2],
202 [IndicatorValue::None, IndicatorValue::None]
203 ));
204 for (i, want) in expected.iter().enumerate() {
205 let IndicatorValue::Multiple(got) = &ab.values.to_rows()[i + 2] else {
206 panic!("bar {}: no value", i + 2);
207 };
208 assert_eq!(got.len(), 3, "bar {}", i + 2);
209 for (line, (g, w)) in got.iter().zip(want).enumerate() {
210 if ((g - w) / w).abs() >= 1e-12 {
211 failures.push(format!("bar {} line {line}: got {g}, want {w}", i + 2));
212 }
213 }
214 }
215 assert!(failures.is_empty(), "{}", failures.join("\n"));
216 }
217}