egui_charts/studies/builtin/
cvd.rs1use crate::model::Bar;
8use crate::studies::palette;
9use crate::studies::stepper::Stepper;
10use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
11use egui::Color32;
12
13#[derive(Clone)]
18pub struct CVD {
19 values: IndicatorValues,
21 stepper: Stepper<f64>,
22 color: Color32,
24 visible: bool,
26 mode: CVDMode,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
32pub enum CVDMode {
33 #[default]
35 ClosePosition,
36 BodyRatio,
38 Simple,
40}
41
42impl CVD {
43 pub fn new() -> Self {
45 Self {
46 values: IndicatorValues::new(),
47 stepper: Stepper::default(),
48 color: palette::TEAL, visible: true,
50 mode: CVDMode::default(),
51 }
52 }
53
54 pub fn with_mode(mut self, mode: CVDMode) -> Self {
56 self.mode = mode;
57 self
58 }
59
60 pub fn with_color(mut self, color: Color32) -> Self {
62 self.color = color;
63 self
64 }
65
66 fn estimate_delta(mode: CVDMode, bar: &Bar) -> f64 {
68 let range = bar.high - bar.low;
69 if range <= 0.0 {
70 return 0.0;
72 }
73
74 match mode {
75 CVDMode::ClosePosition => {
76 let close_pos = (bar.close - bar.low) / range;
79 let buy_pct = close_pos;
80 let sell_pct = 1.0 - close_pos;
81 bar.volume * (buy_pct - sell_pct)
82 }
83 CVDMode::BodyRatio => {
84 let body = (bar.close - bar.open).abs();
86 let body_ratio = body / range;
87 let direction = if bar.close >= bar.open { 1.0 } else { -1.0 };
88 bar.volume * body_ratio * direction
89 }
90 CVDMode::Simple => {
91 if bar.close >= bar.open {
93 bar.volume
94 } else {
95 -bar.volume
96 }
97 }
98 }
99 }
100}
101
102impl Default for CVD {
103 fn default() -> Self {
104 Self::new()
105 }
106}
107
108impl Indicator for CVD {
109 crate::studies::inputs::study_inputs!(self;
110 mode: choice("Mode", [CVDMode::ClosePosition, CVDMode::BodyRatio, CVDMode::Simple])
111 ; {});
112
113 fn value_format(&self) -> crate::scales::StudyFormat {
114 crate::scales::StudyFormat::Volume
115 }
116
117 fn name(&self) -> &str {
118 "CVD"
119 }
120
121 fn desc(&self) -> &str {
122 "Cumulative Volume Delta - Running sum of buy minus sell volume, estimated from bars"
123 }
124
125 fn calculate(&mut self, data: &[Bar]) {
126 self.update(data, 0);
127 }
128
129 fn update(&mut self, data: &[Bar], from: usize) {
130 self.stepper
131 .run(data, from, &mut self.values, |state, bar| {
132 if !(self.mode == CVDMode::ClosePosition || bar.open.is_finite())
133 || ![bar.high, bar.low, bar.close, bar.volume]
134 .iter()
135 .all(|x| x.is_finite())
136 {
137 return IndicatorValue::None;
138 }
139 *state += Self::estimate_delta(self.mode, bar);
140 IndicatorValue::Single(*state)
141 });
142 }
143
144 fn is_incremental(&self) -> bool {
145 true
146 }
147
148 fn values(&self) -> &IndicatorValues {
149 &self.values
150 }
151
152 fn colors(&self) -> Vec<Color32> {
153 vec![self.color]
154 }
155
156 fn set_colors(&mut self, colors: Vec<Color32>) {
157 if !colors.is_empty() {
158 self.color = colors[0];
159 }
160 }
161
162 fn is_overlay(&self) -> bool {
163 false }
165
166 fn is_visible(&self) -> bool {
167 self.visible
168 }
169
170 fn set_visible(&mut self, visible: bool) {
171 self.visible = visible;
172 }
173
174 fn clone_box(&self) -> Box<dyn Indicator> {
175 Box::new(self.clone())
176 }
177
178 fn line_names(&self) -> Vec<String> {
179 vec!["CVD".to_string()]
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use chrono::Utc;
187
188 fn create_test_bars() -> Vec<Bar> {
189 let ts = Utc::now();
190 vec![
191 Bar {
192 time: ts,
193 open: 100.0,
194 high: 102.0,
195 low: 99.0,
196 close: 101.5, volume: 1000.0,
198 colors: None,
199 payload: None,
200 },
201 Bar {
202 time: ts,
203 open: 101.5,
204 high: 103.0,
205 low: 100.0,
206 close: 100.5, volume: 1000.0,
208 colors: None,
209 payload: None,
210 },
211 Bar {
212 time: ts,
213 open: 100.5,
214 high: 102.0,
215 low: 99.0,
216 close: 101.8, volume: 1500.0,
218 colors: None,
219 payload: None,
220 },
221 ]
222 }
223
224 #[test]
225 fn test_cvd_close_position_ignores_open() {
226 let mut bars = create_test_bars();
229 bars[1].open = f64::NAN;
230 let mut cvd = CVD::new();
231 cvd.calculate(&bars);
232 for (got, want) in cvd.values.to_rows().iter().zip([2000.0 / 3.0, 0.0, 1300.0]) {
233 let IndicatorValue::Single(got) = got else {
234 panic!("expected a value, got {got:?}");
235 };
236 assert!((got - want).abs() < 1e-9, "{got} != {want}");
237 }
238
239 let mut body = CVD::new().with_mode(CVDMode::BodyRatio);
241 body.calculate(&bars);
242 assert!(matches!(body.values.to_rows()[1], IndicatorValue::None));
243 }
244
245 #[test]
246 fn test_cvd_simple_mode() {
247 let mut cvd = CVD::new().with_mode(CVDMode::Simple);
248 let bars = [Bar::new(Utc::now(), 100.0, 110.0, 99.0, 101.0, 1000.0)];
249
250 cvd.calculate(&bars);
251
252 assert_eq!(cvd.values().value(0, 0), Some(1000.0));
254 }
255}