Skip to main content

egui_charts/studies/builtin/
volume_oscillator.rs

1use super::smoothing::Smoothing;
2use crate::model::Bar;
3use crate::studies::palette;
4use crate::studies::stepper::Stepper;
5use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
6use egui::Color32;
7
8/// Volume Oscillator
9/// Measures the difference between two volume moving avgs
10/// Similar to MACD but for volume
11#[derive(Clone)]
12pub struct VolumeOscillator {
13    /// Short EMA period (typically 5)
14    short_period: usize,
15    /// Long EMA period (typically 10)
16    long_period: usize,
17    values: IndicatorValues,
18    stepper: Stepper<[Smoothing; 2]>,
19    color: Color32,
20    visible: bool,
21}
22
23impl VolumeOscillator {
24    /// Create with `short_period`, `long_period` in bars, each clamped to at least one.
25    pub fn new(short_period: usize, long_period: usize) -> Self {
26        Self {
27            short_period: short_period.max(1),
28            long_period: long_period.max(1),
29            values: IndicatorValues::new(),
30            stepper: Stepper::default(),
31            color: palette::BLUE, // Blue
32            visible: true,
33        }
34    }
35
36    /// Set the plot color.
37    pub fn with_color(mut self, color: Color32) -> Self {
38        self.color = color;
39        self
40    }
41}
42
43/// Construct with the conventional default parameters.
44impl Default for VolumeOscillator {
45    fn default() -> Self {
46        Self::new(5, 10)
47    }
48}
49
50impl Indicator for VolumeOscillator {
51    crate::studies::inputs::study_inputs!(self;
52        short_period: length("Short period", 1, usize::MAX),
53        long_period: length("Long period", 1, usize::MAX)
54    ; {});
55
56    fn value_format(&self) -> crate::scales::StudyFormat {
57        crate::scales::StudyFormat::Percent
58    }
59
60    fn name(&self) -> &str {
61        "Volume Osc"
62    }
63
64    fn desc(&self) -> &str {
65        "Volume Oscillator - Difference between volume EMAs"
66    }
67
68    fn calculate(&mut self, data: &[Bar]) {
69        self.update(data, 0);
70    }
71
72    fn update(&mut self, data: &[Bar], from: usize) {
73        self.stepper
74            .run(data, from, &mut self.values, |state, bar| {
75                if !bar.volume.is_finite() {
76                    return IndicatorValue::None;
77                }
78                let short = state[0].next(
79                    bar.volume,
80                    self.short_period,
81                    2.0 / (self.short_period as f64 + 1.0),
82                );
83                let long = state[1].next(
84                    bar.volume,
85                    self.long_period,
86                    2.0 / (self.long_period as f64 + 1.0),
87                );
88                if short.is_nan() || long.is_nan() || long == 0.0 {
89                    IndicatorValue::None
90                } else {
91                    IndicatorValue::Single((short - long) / long * 100.0)
92                }
93            });
94    }
95
96    fn is_incremental(&self) -> bool {
97        true
98    }
99
100    fn values(&self) -> &IndicatorValues {
101        &self.values
102    }
103
104    fn colors(&self) -> Vec<Color32> {
105        vec![self.color]
106    }
107
108    fn set_colors(&mut self, colors: Vec<Color32>) {
109        if !colors.is_empty() {
110            self.color = colors[0];
111        }
112    }
113
114    fn is_overlay(&self) -> bool {
115        false
116    }
117
118    fn is_visible(&self) -> bool {
119        self.visible
120    }
121
122    fn set_visible(&mut self, visible: bool) {
123        self.visible = visible;
124    }
125
126    fn clone_box(&self) -> Box<dyn Indicator> {
127        Box::new(self.clone())
128    }
129
130    fn line_names(&self) -> Vec<String> {
131        vec![format!(
132            "VolOsc({},{})",
133            self.short_period, self.long_period
134        )]
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use chrono::Utc;
142
143    fn make_bar(volume: f64) -> Bar {
144        Bar {
145            time: Utc::now(),
146            open: 100.0,
147            high: 100.0,
148            low: 100.0,
149            close: 100.0,
150            volume,
151            colors: None,
152            payload: None,
153        }
154    }
155
156    #[test]
157    fn test_volume_oscillator_rising() {
158        let mut vo = VolumeOscillator::new(2, 4);
159
160        // Rising volume - short EMA should be > long EMA = positive oscillator
161        let data = vec![
162            make_bar(100.0),
163            make_bar(200.0),
164            make_bar(300.0),
165            make_bar(400.0),
166            make_bar(500.0),
167            make_bar(600.0),
168        ];
169
170        vo.calculate(&data);
171
172        // Last value should be positive (rising volume)
173        let rows = vo.values.to_rows();
174        let IndicatorValue::Single(v) = rows.last().unwrap() else {
175            panic!("no value: {:?}", vo.values.to_rows().last().unwrap());
176        };
177        assert!(*v > 0.0, "Oscillator should be positive for rising volume");
178    }
179}