Skip to main content

finance_query/indicators/
obv.rs

1//! On-Balance Volume (OBV) indicator.
2
3use super::{IndicatorError, Result};
4
5/// Calculate On-Balance Volume (OBV).
6///
7/// OBV is a cumulative indicator that adds volume on up days and subtracts volume on down days.
8/// It measures buying and selling pressure.
9///
10/// # Arguments
11///
12/// * `closes` - Close prices
13/// * `volumes` - Trading volumes
14///
15/// # Returns
16///
17/// Vector of cumulative OBV values.
18///
19/// # Example
20///
21/// ```
22/// use finance_query::indicators::obv;
23///
24/// let closes = vec![100.0, 102.0, 101.0, 103.0, 105.0];
25/// let volumes = vec![1000.0, 1200.0, 900.0, 1500.0, 2000.0];
26///
27/// let result = obv(&closes, &volumes).unwrap();
28/// assert_eq!(result.len(), 5);
29/// ```
30pub fn obv(closes: &[f64], volumes: &[f64]) -> Result<Vec<Option<f64>>> {
31    if closes.len() < 2 {
32        return Err(IndicatorError::InsufficientData {
33            need: 2,
34            got: closes.len(),
35        });
36    }
37
38    if closes.len() != volumes.len() {
39        return Err(IndicatorError::InvalidPeriod(
40            "Closes and volumes must have the same length".to_string(),
41        ));
42    }
43
44    let mut result = Vec::with_capacity(closes.len());
45    let mut obv_value = 0.0;
46
47    result.push(Some(obv_value)); // First value is 0
48
49    for i in 1..closes.len() {
50        if closes[i] > closes[i - 1] {
51            obv_value += volumes[i];
52        } else if closes[i] < closes[i - 1] {
53            obv_value -= volumes[i];
54        }
55        // If close == previous close, OBV unchanged
56        result.push(Some(obv_value));
57    }
58
59    Ok(result)
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    #[test]
67    fn test_obv_basic() {
68        let closes = vec![100.0, 102.0, 101.0, 103.0, 105.0];
69        let volumes = vec![1000.0, 1200.0, 900.0, 1500.0, 2000.0];
70
71        let result = obv(&closes, &volumes).unwrap();
72
73        assert_eq!(result.len(), 5);
74        assert_eq!(result[0], Some(0.0)); // Start at 0
75        assert_eq!(result[1], Some(1200.0)); // Price up, add volume
76        assert_eq!(result[2], Some(300.0)); // Price down, subtract volume
77        assert_eq!(result[3], Some(1800.0)); // Price up, add volume
78        assert_eq!(result[4], Some(3800.0)); // Price up, add volume
79    }
80
81    #[test]
82    fn test_obv_insufficient_data() {
83        let closes = vec![100.0];
84        let volumes = vec![1000.0];
85
86        let result = obv(&closes, &volumes);
87        assert!(result.is_err());
88    }
89
90    #[test]
91    fn test_obv_mismatched_lengths() {
92        let closes = vec![100.0, 102.0, 101.0];
93        let volumes = vec![1000.0, 1200.0];
94
95        let result = obv(&closes, &volumes);
96        assert!(result.is_err());
97    }
98}