Skip to main content

mantis_ta/indicators/
obv.rs

1use crate::indicators::Indicator;
2use crate::types::Candle;
3
4/// On-Balance Volume.
5///
6/// # Examples
7/// ```rust
8/// use mantis_ta::indicators::{Indicator, OBV};
9/// use mantis_ta::types::Candle;
10///
11/// let candles: Vec<Candle> = vec![
12///     (10.0, 100.0), // first bar, no prev -> OBV = 0
13///     (12.0, 150.0), // close > prev -> +150
14///     (9.0, 80.0),   // close < prev -> -80
15///     (9.0, 50.0),   // equal -> unchanged
16/// ]
17/// .into_iter()
18/// .enumerate()
19/// .map(|(i, (c, v))| Candle {
20///     timestamp: i as i64,
21///     open: 0.0,
22///     high: 0.0,
23///     low: 0.0,
24///     close: c,
25///     volume: v,
26/// })
27/// .collect();
28///
29/// let out = OBV::new().calculate(&candles);
30/// assert_eq!(out[0], Some(0.0));
31/// assert_eq!(out[1], Some(150.0));
32/// assert_eq!(out[2], Some(70.0));
33/// assert_eq!(out[3], Some(70.0));
34/// ```
35#[derive(Debug, Clone)]
36pub struct OBV {
37    current: f64,
38    prev_close: Option<f64>,
39}
40
41impl OBV {
42    pub fn new() -> Self {
43        Self {
44            current: 0.0,
45            prev_close: None,
46        }
47    }
48}
49
50impl Default for OBV {
51    fn default() -> Self {
52        Self::new()
53    }
54}
55
56impl Indicator for OBV {
57    type Output = f64;
58
59    fn next(&mut self, candle: &Candle) -> Option<Self::Output> {
60        if let Some(prev) = self.prev_close {
61            match candle.close.partial_cmp(&prev) {
62                Some(std::cmp::Ordering::Greater) => self.current += candle.volume,
63                Some(std::cmp::Ordering::Less) => self.current -= candle.volume,
64                _ => {}
65            }
66        }
67        self.prev_close = Some(candle.close);
68        Some(self.current)
69    }
70
71    fn reset(&mut self) {
72        self.current = 0.0;
73        self.prev_close = None;
74    }
75
76    fn warmup_period(&self) -> usize {
77        0
78    }
79
80    fn clone_boxed(&self) -> Box<dyn Indicator<Output = Self::Output>> {
81        Box::new(self.clone())
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn obv_moves_with_direction() {
91        let mut obv = OBV::new();
92        // close values: 10, 12, 9, 9
93        let candles: Vec<Candle> = vec![
94            (10.0, 100.0), // first bar, no prev -> OBV = 0
95            (12.0, 150.0), // close > prev close -> +150
96            (9.0, 80.0),   // close < prev close -> -80
97            (9.0, 50.0),   // close == prev close -> unchanged
98        ]
99        .into_iter()
100        .map(|(c, v)| Candle {
101            timestamp: 0,
102            open: 0.0,
103            high: 0.0,
104            low: 0.0,
105            close: c,
106            volume: v,
107        })
108        .collect();
109
110        let outputs: Vec<_> = candles.iter().map(|c| obv.next(c)).collect();
111        assert_eq!(outputs[0], Some(0.0)); // no previous close
112        assert_eq!(outputs[1], Some(150.0)); // 0 + 150
113        assert_eq!(outputs[2], Some(70.0)); // 150 - 80
114        assert_eq!(outputs[3], Some(70.0)); // unchanged
115    }
116}