Skip to main content

egui_charts/studies/builtin/
acceleration_bands.rs

1use 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/// Acceleration Bands (Price Headley), a price envelope that widens on wide bars.
9///
10/// With `r = (high - low) / (high + low)` per bar (0 when `high + low` is 0):
11/// upper = SMA(high * (1 + factor * r), period), middle = SMA(close, period),
12/// lower = SMA(low * (1 - factor * r), period).
13#[derive(Clone)]
14pub struct AccelerationBands {
15    /// Period for calculation
16    period: usize,
17    /// Band width factor (typically 4.0)
18    factor: f64,
19    values: IndicatorValues,
20    /// Colors: [Upper, Middle, Lower]
21    colors: Vec<Color32>,
22    visible: bool,
23}
24
25impl AccelerationBands {
26    /// `factor` is clamped into 0 to `f64::MAX`, NaN to 0.
27    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,      // Upper
34                palette::BB_MIDDLE, // Middle
35                palette::BLUE,      // Lower
36            ],
37            visible: true,
38        }
39    }
40
41    /// Set plot colors in the order returned by [`Indicator::line_names`].
42    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    /// Reference values from Price Headley's definition ("Big Trends in Trading", 2002), computed
174    /// independently: upper = SMA(H * (1 + f * (H - L) / (H + L)), n), middle = SMA(C, n),
175    /// lower = SMA(L * (1 - f * (H - L) / (H + L)), n). The bar ranges vary from 1 to 12, so
176    /// averaging each bar's widened high differs from widening the average high.
177    #[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}