1use 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum VolumeColorRule {
13 Open,
15 PreviousClose,
17}
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub enum VolumeAverage {
22 Simple,
24 Exponential,
26}
27
28#[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 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 pub fn with_color_rule(mut self, rule: VolumeColorRule) -> Self {
82 self.color_rule = rule;
83 self
84 }
85
86 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 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}