Skip to main content

egui_charts/studies/builtin/
vroc.rs

1use crate::model::Bar;
2use crate::studies::palette;
3use crate::studies::stepper::update_window;
4use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
5use egui::Color32;
6
7/// Volume Rate of Change (VROC)
8/// Measures the rate of change in volume over a specified period
9/// Helps identify volume trends and divergences
10#[derive(Clone)]
11pub struct VolumeRateOfChange {
12    /// Lookback period
13    period: usize,
14    values: IndicatorValues,
15    color: Color32,
16    visible: bool,
17}
18
19impl VolumeRateOfChange {
20    /// Create with a lookback of `period` bars, clamped to at least 1.
21    pub fn new(period: usize) -> Self {
22        Self {
23            period: period.max(1),
24            values: IndicatorValues::new(),
25            color: palette::DEEP_PURPLE,
26            visible: true,
27        }
28    }
29
30    /// Set the plot color.
31    pub fn with_color(mut self, color: Color32) -> Self {
32        self.color = color;
33        self
34    }
35}
36
37/// Construct with the conventional default parameters.
38impl Default for VolumeRateOfChange {
39    fn default() -> Self {
40        Self::new(14)
41    }
42}
43
44impl VolumeRateOfChange {
45    fn compute(&self, data: &[Bar]) -> IndicatorValues {
46        let mut values = IndicatorValues::new();
47
48        if data.len() < self.period.saturating_add(1) {
49            for _ in 0..data.len() {
50                values.push(IndicatorValue::None);
51            }
52            return values;
53        }
54
55        for i in 0..data.len() {
56            if i < self.period {
57                values.push(IndicatorValue::None);
58            } else {
59                let curr_volume = data[i].volume;
60                let past_volume = data[i - self.period].volume;
61
62                if past_volume == 0.0 {
63                    values.push(IndicatorValue::None);
64                } else {
65                    let vroc = (curr_volume - past_volume) / past_volume * 100.0;
66                    values.push(IndicatorValue::Single(vroc));
67                }
68            }
69        }
70        values
71    }
72}
73
74impl Indicator for VolumeRateOfChange {
75    crate::studies::inputs::study_inputs!(self;
76        period: length("Period", 1, usize::MAX)
77    ; {});
78
79    fn value_format(&self) -> crate::scales::StudyFormat {
80        crate::scales::StudyFormat::Percent
81    }
82
83    fn name(&self) -> &str {
84        "VROC"
85    }
86
87    fn desc(&self) -> &str {
88        "Volume Rate of Change - Volume momentum indicator"
89    }
90
91    fn calculate(&mut self, data: &[Bar]) {
92        self.update(data, 0);
93    }
94
95    fn update(&mut self, data: &[Bar], from: usize) {
96        let mut values = std::mem::take(&mut self.values);
97        update_window(
98            &mut values,
99            data,
100            from,
101            |_data, from| from.saturating_sub(self.period),
102            |data| self.compute(data),
103        );
104        self.values = values;
105    }
106
107    fn is_incremental(&self) -> bool {
108        true
109    }
110
111    fn values(&self) -> &IndicatorValues {
112        &self.values
113    }
114
115    fn colors(&self) -> Vec<Color32> {
116        vec![self.color]
117    }
118
119    fn set_colors(&mut self, colors: Vec<Color32>) {
120        if !colors.is_empty() {
121            self.color = colors[0];
122        }
123    }
124
125    fn is_overlay(&self) -> bool {
126        false
127    }
128
129    fn is_visible(&self) -> bool {
130        self.visible
131    }
132
133    fn set_visible(&mut self, visible: bool) {
134        self.visible = visible;
135    }
136
137    fn clone_box(&self) -> Box<dyn Indicator> {
138        Box::new(self.clone())
139    }
140
141    fn line_names(&self) -> Vec<String> {
142        vec![format!("VROC({})", self.period)]
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149    use chrono::Utc;
150
151    fn make_bar(volume: f64) -> Bar {
152        Bar {
153            time: Utc::now(),
154            open: 100.0,
155            high: 100.0,
156            low: 100.0,
157            close: 100.0,
158            volume,
159            colors: None,
160            payload: None,
161        }
162    }
163
164    #[test]
165    fn test_vroc_calculation() {
166        let mut vroc = VolumeRateOfChange::new(5);
167
168        let data = vec![
169            make_bar(1000.0),
170            make_bar(1100.0),
171            make_bar(1200.0),
172            make_bar(1300.0),
173            make_bar(1400.0),
174            make_bar(1500.0), // +50% vs 1000
175            make_bar(2200.0), // +100% vs 1100
176        ];
177
178        vroc.calculate(&data);
179
180        assert_eq!(vroc.values.len(), 7);
181
182        // First 5 should be None
183        for i in 0..5 {
184            assert!(matches!(vroc.values.to_rows()[i], IndicatorValue::None));
185        }
186
187        // Check calculation at index 5: (1500 - 1000) / 1000 * 100 = 50%
188        let IndicatorValue::Single(v) = vroc.values.to_rows()[5] else {
189            panic!("no value: {:?}", vroc.values.to_rows()[5]);
190        };
191        assert!((v - 50.0).abs() < 0.01);
192    }
193}