Skip to main content

egui_charts/studies/builtin/
volume.rs

1//! Volume columns with an optional moving average on an independent scale.
2
3use crate::model::Bar;
4use crate::studies::palette;
5use crate::studies::stepper::Stepper;
6use crate::studies::{Indicator, IndicatorValue, IndicatorValues, PlotStyle, PlotType};
7use egui::Color32;
8use std::collections::VecDeque;
9
10/// Comparison used to colour volume columns. An unchanged close is always up.
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum VolumeColorRule {
13    /// Compare this bar's close with its open.
14    Open,
15    /// Compare with the previous usable close; the first bar uses its open.
16    PreviousClose,
17}
18
19/// Moving average formula for volume.
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub enum VolumeAverage {
22    /// Arithmetic mean of a complete window.
23    Simple,
24    /// Alpha 2/(length+1), seeded by the first usable volume.
25    Exponential,
26}
27
28/// Volume columns and a moving average, in the price pane or a separate pane.
29///
30/// Outputs are up volume, down volume and the average. Exactly one volume
31/// column is defined per usable bar. Equal prices count as up under both rules;
32/// a bar with non-finite volume, open or close leaves all outputs missing.
33#[derive(Clone)]
34pub struct Volume {
35    period: usize,
36    average: VolumeAverage,
37    show_average: bool,
38    color_rule: VolumeColorRule,
39    overlay: bool,
40    colors: [Color32; 3],
41    visible: bool,
42    values: IndicatorValues,
43    stepper: Stepper<State>,
44}
45
46#[derive(Clone, Default)]
47struct State {
48    previous: Option<f64>,
49    window: VecDeque<f64>,
50    ema: Option<f64>,
51}
52
53impl Default for Volume {
54    fn default() -> Self {
55        Self::new(9)
56    }
57}
58
59impl Volume {
60    /// Volume with an SMA of `period` bars (zero becomes one) that is off until enabled,
61    /// overlaid by default.
62    pub fn new(period: usize) -> Self {
63        Self {
64            period: period.max(1),
65            average: VolumeAverage::Simple,
66            show_average: false,
67            color_rule: VolumeColorRule::Open,
68            overlay: true,
69            colors: [
70                palette::HISTOGRAM_TEAL.gamma_multiply_u8(128),
71                Color32::from_rgba_unmultiplied(239, 83, 80, 128),
72                palette::LIGHT_BLUE,
73            ],
74            visible: true,
75            values: IndicatorValues::new(),
76            stepper: Stepper::default(),
77        }
78    }
79
80    /// Choose a price comparison for column colours.
81    pub fn with_color_rule(mut self, rule: VolumeColorRule) -> Self {
82        self.color_rule = rule;
83        self
84    }
85
86    /// Choose SMA, EMA, or no moving average.
87    pub fn with_average(mut self, average: Option<VolumeAverage>) -> Self {
88        self.show_average = average.is_some();
89        if let Some(average) = average {
90            self.average = average;
91        }
92        self
93    }
94
95    /// Draw in the price pane (`true`) or in a separate pane (`false`).
96    pub fn with_overlay(mut self, overlay: bool) -> Self {
97        self.overlay = overlay;
98        self
99    }
100}
101
102impl Indicator for Volume {
103    crate::studies::inputs::study_inputs!(self;
104        period: length("MA length", 1, usize::MAX),
105        average: choice("MA type", [VolumeAverage::Simple, VolumeAverage::Exponential]),
106        show_average: toggle("Moving average"),
107        color_rule: choice("Colour by", [VolumeColorRule::Open, VolumeColorRule::PreviousClose]),
108        overlay: toggle("Overlay price pane")
109    ; {});
110
111    fn name(&self) -> &str {
112        "Volume"
113    }
114    fn desc(&self) -> &str {
115        "Volume with an optional SMA or EMA; unchanged prices count as up"
116    }
117    fn calculate(&mut self, data: &[Bar]) {
118        self.update(data, 0);
119    }
120    fn update(&mut self, data: &[Bar], from: usize) {
121        let (period, average, show_average, rule) = (
122            self.period,
123            self.average,
124            self.show_average,
125            self.color_rule,
126        );
127        self.stepper
128            .run(data, from, &mut self.values, |state, bar| {
129                if !bar.volume.is_finite() || !bar.open.is_finite() || !bar.close.is_finite() {
130                    return IndicatorValue::None;
131                }
132                let reference = match rule {
133                    VolumeColorRule::Open => bar.open,
134                    VolumeColorRule::PreviousClose => state.previous.unwrap_or(bar.open),
135                };
136                state.previous = Some(bar.close);
137                state.window.push_back(bar.volume);
138                if state.window.len() > period {
139                    state.window.pop_front();
140                }
141                let ema = state.ema.map_or(bar.volume, |previous| {
142                    previous + (bar.volume - previous) * (2.0 / (period as f64 + 1.0))
143                });
144                state.ema = Some(ema);
145                let mean = if !show_average {
146                    f64::NAN
147                } else {
148                    match average {
149                        VolumeAverage::Simple if state.window.len() == period => {
150                            state.window.iter().map(|v| v / period as f64).sum()
151                        }
152                        VolumeAverage::Simple => f64::NAN,
153                        VolumeAverage::Exponential => ema,
154                    }
155                };
156                let mut outputs = vec![f64::NAN, f64::NAN, mean];
157                outputs[usize::from(bar.close < reference)] = bar.volume;
158                IndicatorValue::Multiple(outputs)
159            });
160    }
161    fn is_incremental(&self) -> bool {
162        true
163    }
164    fn values(&self) -> &IndicatorValues {
165        &self.values
166    }
167    fn colors(&self) -> Vec<Color32> {
168        self.colors.to_vec()
169    }
170    fn set_colors(&mut self, colors: Vec<Color32>) {
171        for (slot, color) in self.colors.iter_mut().zip(colors) {
172            *slot = color;
173        }
174    }
175    fn plot_style(&self, line: usize) -> PlotStyle {
176        PlotStyle {
177            color: self.colors.get(line).copied().unwrap_or(Color32::WHITE),
178            width: 1.0,
179            pattern: crate::model::LineStyle::Solid,
180            plot_type: if line < 2 {
181                PlotType::Columns
182            } else {
183                PlotType::Line
184            },
185            visible: line < 2 || self.show_average,
186        }
187    }
188    fn is_overlay(&self) -> bool {
189        self.overlay
190    }
191    fn is_volume(&self) -> bool {
192        true
193    }
194    fn is_visible(&self) -> bool {
195        self.visible
196    }
197    fn set_visible(&mut self, visible: bool) {
198        self.visible = visible;
199    }
200    fn clone_box(&self) -> Box<dyn Indicator> {
201        Box::new(self.clone())
202    }
203    fn line_names(&self) -> Vec<String> {
204        vec![
205            "Up volume".into(),
206            "Down volume".into(),
207            format!(
208                "{} {}",
209                match self.average {
210                    VolumeAverage::Simple => "SMA",
211                    VolumeAverage::Exponential => "EMA",
212                },
213                self.period
214            ),
215        ]
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222    use chrono::{TimeZone, Utc};
223
224    #[test]
225    fn comparisons_and_averages_match_hand_worked_bars() {
226        let bars: Vec<_> = [
227            (10.0, 12.0, 10.0),
228            (13.0, 12.5, 20.0),
229            (11.0, 11.5, 30.0),
230            (11.5, 11.5, 40.0),
231        ]
232        .iter()
233        .enumerate()
234        .map(|(i, &(open, close, volume))| {
235            Bar::new(
236                Utc.timestamp_opt(i as i64 * 60, 0).unwrap(),
237                open,
238                14.0,
239                9.0,
240                close,
241                volume,
242            )
243        })
244        .collect();
245        for (rule, up) in [
246            (0, [true, false, true, true]),
247            (1, [true, true, false, true]),
248        ] {
249            for (average, expected) in [
250                (Some(0), [None, Some(15.0), Some(25.0), Some(35.0)]),
251                (
252                    Some(1),
253                    [
254                        Some(10.0),
255                        Some(50.0 / 3.0),
256                        Some(230.0 / 9.0),
257                        Some(950.0 / 27.0),
258                    ],
259                ),
260                (None, [None; 4]),
261            ] {
262                let mut study = crate::studies::IndicatorFactory::new()
263                    .create("Volume")
264                    .expect("registered Volume study");
265                study
266                    .set_input("period", crate::studies::InputValue::Length(2))
267                    .unwrap();
268                study
269                    .set_input("color_rule", crate::studies::InputValue::Choice(rule))
270                    .unwrap();
271                study
272                    .set_input(
273                        "show_average",
274                        crate::studies::InputValue::Toggle(average.is_some()),
275                    )
276                    .unwrap();
277                if let Some(average) = average {
278                    study
279                        .set_input("average", crate::studies::InputValue::Choice(average))
280                        .unwrap();
281                }
282                study.calculate(&bars);
283                for (i, is_up) in up.iter().enumerate() {
284                    let line = usize::from(!is_up);
285                    assert_eq!(study.values().value(line, i), Some(bars[i].volume));
286                    assert_eq!(study.values().value(1 - line, i), None);
287                    assert_eq!(
288                        study.colors()[line],
289                        if *is_up {
290                            Color32::from_rgba_unmultiplied(38, 166, 154, 128)
291                        } else {
292                            Color32::from_rgba_unmultiplied(239, 83, 80, 128)
293                        }
294                    );
295                    match expected[i] {
296                        Some(value) => {
297                            assert!((study.values().value(2, i).unwrap() - value).abs() < 1e-10)
298                        }
299                        None => assert_eq!(study.values().value(2, i), None),
300                    }
301                }
302                let mut changed = bars.clone();
303                changed[3].volume = 60.0;
304                study.update(&changed, 3);
305                let mut fresh = study.clone_box();
306                fresh.calculate(&changed);
307                for line in 0..3 {
308                    for i in 0..4 {
309                        assert_eq!(study.values().value(line, i), fresh.values().value(line, i));
310                    }
311                }
312            }
313        }
314        assert!(
315            !Volume::default().plot_style(2).visible,
316            "the average starts off"
317        );
318        let mut study =
319            crate::studies::ConfiguredStudy::new(Box::new(Volume::new(2).with_average(None)));
320        for enabled in [false, true, false, true] {
321            study
322                .set_input("show_average", crate::studies::InputValue::Toggle(enabled))
323                .unwrap();
324            study.calculate(&bars);
325            assert_eq!(study.plot_style(2).visible, enabled);
326            assert_eq!(study.values().value(2, 3), enabled.then_some(35.0));
327        }
328        let mut settings = study.settings();
329        settings.plots[2].visible = false;
330        study.apply_settings(&[], settings).unwrap();
331        assert!(
332            !study.plot_style(2).visible,
333            "style visibility still applies"
334        );
335    }
336}