egui_charts/studies/builtin/
obv.rs1use crate::model::Bar;
27use crate::studies::palette;
28use crate::studies::stepper::Stepper;
29use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
30use egui::Color32;
31
32#[derive(Clone)]
34pub struct OnBalanceVolume {
35 values: IndicatorValues,
36 stepper: Stepper<State>,
37 color: Color32,
38 visible: bool,
39}
40
41#[derive(Clone, Default)]
42struct State {
43 previous: Option<f64>,
44 total: f64,
45}
46
47impl OnBalanceVolume {
48 pub fn new() -> Self {
50 Self {
51 values: IndicatorValues::new(),
52 stepper: Stepper::default(),
53 color: palette::BLUE,
54 visible: true,
55 }
56 }
57
58 pub fn with_color(mut self, color: Color32) -> Self {
60 self.color = color;
61 self
62 }
63}
64
65impl Default for OnBalanceVolume {
66 fn default() -> Self {
67 Self::new()
68 }
69}
70
71impl Indicator for OnBalanceVolume {
72 fn source(&self) -> Option<crate::studies::StudySource> {
73 Some(crate::studies::StudySource::Price(
74 crate::model::PriceSource::Close,
75 ))
76 }
77
78 fn value_format(&self) -> crate::scales::StudyFormat {
79 crate::scales::StudyFormat::Volume
80 }
81
82 fn name(&self) -> &str {
83 "OBV"
84 }
85
86 fn desc(&self) -> &str {
87 "On Balance Volume - Cumulative volume indicator"
88 }
89
90 fn calculate(&mut self, data: &[Bar]) {
91 self.update(data, 0);
92 }
93
94 fn update(&mut self, data: &[Bar], from: usize) {
95 self.stepper
96 .run(data, from, &mut self.values, |state, bar| {
97 if !bar.close.is_finite() || !bar.volume.is_finite() {
98 return IndicatorValue::None;
99 }
100 if let Some(previous) = state.previous {
101 if bar.close > previous {
102 state.total += bar.volume;
103 } else if bar.close < previous {
104 state.total -= bar.volume;
105 }
106 }
107 state.previous = Some(bar.close);
108 IndicatorValue::Single(state.total)
109 });
110 }
111
112 fn is_incremental(&self) -> bool {
113 true
114 }
115
116 fn values(&self) -> &IndicatorValues {
117 &self.values
118 }
119
120 fn colors(&self) -> Vec<Color32> {
121 vec![self.color]
122 }
123
124 fn set_colors(&mut self, colors: Vec<Color32>) {
125 if !colors.is_empty() {
126 self.color = colors[0];
127 }
128 }
129
130 fn is_overlay(&self) -> bool {
131 false }
133
134 fn is_visible(&self) -> bool {
135 self.visible
136 }
137
138 fn set_visible(&mut self, visible: bool) {
139 self.visible = visible;
140 }
141
142 fn clone_box(&self) -> Box<dyn Indicator> {
143 Box::new(self.clone())
144 }
145
146 fn line_names(&self) -> Vec<String> {
147 vec!["OBV".to_string()]
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use chrono::Utc;
155
156 fn bars(closes: &[f64], volumes: &[f64]) -> Vec<Bar> {
157 closes
158 .iter()
159 .zip(volumes)
160 .map(|(&c, &v)| Bar::new(Utc::now(), c, c + 1.0, c - 1.0, c, v))
161 .collect()
162 }
163
164 #[test]
167 fn test_obv_matches_reference() {
168 type Case = (&'static [f64], &'static [f64], &'static [Option<f64>]);
170 let cases: &[Case] = &[
171 (
173 &[10.0, 11.0, 10.5, 10.5, 12.0, 11.0],
174 &[1000.0, 1500.0, 700.0, 900.0, 0.0, 400.0],
175 &[
176 Some(0.0),
177 Some(1500.0),
178 Some(800.0),
179 Some(800.0),
180 Some(800.0),
181 Some(400.0),
182 ],
183 ),
184 (
186 &[10.0, 10.0, 10.0],
187 &[1000.0, 2000.0, 3000.0],
188 &[Some(0.0), Some(0.0), Some(0.0)],
189 ),
190 (&[10.0], &[5000.0], &[Some(0.0)]),
192 (
194 &[10.0, 11.0, 12.0, 13.0, 14.0],
195 &[100.0, 200.0, f64::NAN, 400.0, 500.0],
196 &[Some(0.0), Some(200.0), None, Some(600.0), Some(1100.0)],
197 ),
198 ];
199 for (closes, volumes, expected) in cases {
200 let mut obv = OnBalanceVolume::new();
201 obv.calculate(&bars(closes, volumes));
202 let got: Vec<Option<f64>> = obv
203 .values()
204 .to_rows()
205 .iter()
206 .map(|v| match v {
207 IndicatorValue::Single(x) => Some(*x),
208 IndicatorValue::None => None,
209 other => panic!("unexpected value {other:?}"),
210 })
211 .collect();
212 assert_eq!(got, *expected, "closes {closes:?}");
213 }
214 }
215}