egui_charts/studies/builtin/
vroc.rs1use crate::model::Bar;
2use crate::studies::palette;
3use crate::studies::stepper::update_window;
4use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
5use egui::Color32;
6
7#[derive(Clone)]
11pub struct VolumeRateOfChange {
12 period: usize,
14 values: IndicatorValues,
15 color: Color32,
16 visible: bool,
17}
18
19impl VolumeRateOfChange {
20 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 pub fn with_color(mut self, color: Color32) -> Self {
32 self.color = color;
33 self
34 }
35}
36
37impl 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), make_bar(2200.0), ];
177
178 vroc.calculate(&data);
179
180 assert_eq!(vroc.values.len(), 7);
181
182 for i in 0..5 {
184 assert!(matches!(vroc.values.to_rows()[i], IndicatorValue::None));
185 }
186
187 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}