wickra_core/indicators/
obv.rs1use crate::ohlcv::Candle;
4use crate::traits::Indicator;
5
6#[derive(Debug, Clone, Default)]
28pub struct Obv {
29 prev_close: Option<f64>,
30 total: f64,
31 has_emitted: bool,
32}
33
34impl Obv {
35 pub const fn new() -> Self {
37 Self {
38 prev_close: None,
39 total: 0.0,
40 has_emitted: false,
41 }
42 }
43
44 pub const fn value(&self) -> Option<f64> {
46 if self.has_emitted {
47 Some(self.total)
48 } else {
49 None
50 }
51 }
52}
53
54impl Indicator for Obv {
55 type Input = Candle;
56 type Output = f64;
57
58 fn update(&mut self, candle: Candle) -> Option<f64> {
59 if let Some(prev) = self.prev_close {
62 if candle.close > prev {
63 self.total += candle.volume;
64 } else if candle.close < prev {
65 self.total -= candle.volume;
66 }
67 }
68 self.prev_close = Some(candle.close);
69 self.has_emitted = true;
70 Some(self.total)
71 }
72
73 fn reset(&mut self) {
74 self.prev_close = None;
75 self.total = 0.0;
76 self.has_emitted = false;
77 }
78
79 fn warmup_period(&self) -> usize {
80 1
81 }
82
83 fn is_ready(&self) -> bool {
84 self.has_emitted
85 }
86
87 fn name(&self) -> &'static str {
88 "OBV"
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95 use crate::traits::BatchExt;
96 use approx::assert_relative_eq;
97
98 fn c(close: f64, volume: f64) -> Candle {
99 Candle::new(close, close, close, close, volume, 0).unwrap()
100 }
101
102 #[test]
107 fn accessors_and_metadata() {
108 let mut obv = Obv::new();
109 assert_eq!(obv.warmup_period(), 1);
110 assert_eq!(obv.name(), "OBV");
111 assert_eq!(obv.value(), None);
112 obv.update(c(10.0, 100.0));
113 assert_eq!(obv.value(), Some(0.0));
115 }
116
117 #[test]
118 fn first_candle_baseline_zero() {
119 let mut obv = Obv::new();
120 assert_relative_eq!(obv.update(c(10.0, 100.0)).unwrap(), 0.0, epsilon = 1e-12);
121 }
122
123 #[test]
124 fn up_close_adds_volume() {
125 let mut obv = Obv::new();
126 obv.update(c(10.0, 100.0)); let v = obv.update(c(11.0, 50.0)).unwrap();
128 assert_relative_eq!(v, 50.0, epsilon = 1e-12);
129 }
130
131 #[test]
132 fn down_close_subtracts_volume() {
133 let mut obv = Obv::new();
134 obv.update(c(10.0, 100.0));
135 let v = obv.update(c(9.0, 50.0)).unwrap();
136 assert_relative_eq!(v, -50.0, epsilon = 1e-12);
137 }
138
139 #[test]
140 fn equal_close_does_nothing() {
141 let mut obv = Obv::new();
142 obv.update(c(10.0, 100.0));
143 let v = obv.update(c(10.0, 50.0)).unwrap();
144 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
145 }
146
147 #[test]
148 fn cumulative_sequence() {
149 let candles = vec![
150 c(10.0, 100.0), c(11.0, 20.0), c(10.5, 30.0), c(10.5, 40.0), c(12.0, 10.0), ];
156 let mut obv = Obv::new();
157 let out = obv.batch(&candles);
158 assert_relative_eq!(out[0].unwrap(), 0.0, epsilon = 1e-12);
159 assert_relative_eq!(out[1].unwrap(), 20.0, epsilon = 1e-12);
160 assert_relative_eq!(out[2].unwrap(), -10.0, epsilon = 1e-12);
161 assert_relative_eq!(out[3].unwrap(), -10.0, epsilon = 1e-12);
162 assert_relative_eq!(out[4].unwrap(), 0.0, epsilon = 1e-12);
163 }
164
165 #[test]
166 fn batch_equals_streaming() {
167 let candles: Vec<Candle> = (0..20)
168 .map(|i| {
169 let cl = 10.0 + (f64::from(i) * 0.5).sin();
170 c(cl, 1.0)
171 })
172 .collect();
173 let mut a = Obv::new();
174 let mut b = Obv::new();
175 assert_eq!(
176 a.batch(&candles),
177 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
178 );
179 }
180
181 #[test]
182 fn reset_clears_state() {
183 let mut obv = Obv::new();
184 obv.batch(&[c(10.0, 50.0), c(11.0, 30.0)]);
185 assert!(obv.is_ready());
186 obv.reset();
187 assert!(!obv.is_ready());
188 assert_eq!(obv.value(), None);
189 }
190}