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 #[inline]
59 fn update(&mut self, candle: Candle) -> Option<f64> {
60 if let Some(prev) = self.prev_close {
63 if candle.close > prev {
64 self.total += candle.volume;
65 } else if candle.close < prev {
66 self.total -= candle.volume;
67 }
68 }
69 self.prev_close = Some(candle.close);
70 self.has_emitted = true;
71 Some(self.total)
72 }
73
74 fn reset(&mut self) {
75 self.prev_close = None;
76 self.total = 0.0;
77 self.has_emitted = false;
78 }
79
80 #[inline]
81 fn warmup_period(&self) -> usize {
82 1
83 }
84
85 #[inline]
86 fn is_ready(&self) -> bool {
87 self.has_emitted
88 }
89
90 #[inline]
91 fn name(&self) -> &'static str {
92 "OBV"
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99 use crate::traits::BatchExt;
100 use approx::assert_relative_eq;
101
102 fn c(close: f64, volume: f64) -> Candle {
103 Candle::new(close, close, close, close, volume, 0).unwrap()
104 }
105
106 #[test]
111 fn accessors_and_metadata() {
112 let mut obv = Obv::new();
113 assert_eq!(obv.warmup_period(), 1);
114 assert_eq!(obv.name(), "OBV");
115 assert_eq!(obv.value(), None);
116 obv.update(c(10.0, 100.0));
117 assert_eq!(obv.value(), Some(0.0));
119 }
120
121 #[test]
122 fn first_candle_baseline_zero() {
123 let mut obv = Obv::new();
124 assert_relative_eq!(obv.update(c(10.0, 100.0)).unwrap(), 0.0, epsilon = 1e-12);
125 }
126
127 #[test]
128 fn up_close_adds_volume() {
129 let mut obv = Obv::new();
130 obv.update(c(10.0, 100.0)); let v = obv.update(c(11.0, 50.0)).unwrap();
132 assert_relative_eq!(v, 50.0, epsilon = 1e-12);
133 }
134
135 #[test]
136 fn down_close_subtracts_volume() {
137 let mut obv = Obv::new();
138 obv.update(c(10.0, 100.0));
139 let v = obv.update(c(9.0, 50.0)).unwrap();
140 assert_relative_eq!(v, -50.0, epsilon = 1e-12);
141 }
142
143 #[test]
144 fn equal_close_does_nothing() {
145 let mut obv = Obv::new();
146 obv.update(c(10.0, 100.0));
147 let v = obv.update(c(10.0, 50.0)).unwrap();
148 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
149 }
150
151 #[test]
152 fn cumulative_sequence() {
153 let candles = vec![
154 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), ];
160 let mut obv = Obv::new();
161 let out = obv.batch(&candles);
162 assert_relative_eq!(out[0].unwrap(), 0.0, epsilon = 1e-12);
163 assert_relative_eq!(out[1].unwrap(), 20.0, epsilon = 1e-12);
164 assert_relative_eq!(out[2].unwrap(), -10.0, epsilon = 1e-12);
165 assert_relative_eq!(out[3].unwrap(), -10.0, epsilon = 1e-12);
166 assert_relative_eq!(out[4].unwrap(), 0.0, epsilon = 1e-12);
167 }
168
169 #[test]
170 fn batch_equals_streaming() {
171 let candles: Vec<Candle> = (0..20)
172 .map(|i| {
173 let cl = 10.0 + (f64::from(i) * 0.5).sin();
174 c(cl, 1.0)
175 })
176 .collect();
177 let mut a = Obv::new();
178 let mut b = Obv::new();
179 assert_eq!(
180 a.batch(&candles),
181 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
182 );
183 }
184
185 #[test]
186 fn reset_clears_state() {
187 let mut obv = Obv::new();
188 obv.batch(&[c(10.0, 50.0), c(11.0, 30.0)]);
189 assert!(obv.is_ready());
190 obv.reset();
191 assert!(!obv.is_ready());
192 assert_eq!(obv.value(), None);
193 }
194}