egui_charts/studies/builtin/
volume_oscillator.rs1use 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#[derive(Clone)]
12pub struct VolumeOscillator {
13 short_period: usize,
15 long_period: usize,
17 values: IndicatorValues,
18 stepper: Stepper<[Smoothing; 2]>,
19 color: Color32,
20 visible: bool,
21}
22
23impl VolumeOscillator {
24 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, visible: true,
33 }
34 }
35
36 pub fn with_color(mut self, color: Color32) -> Self {
38 self.color = color;
39 self
40 }
41}
42
43impl 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 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 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}