Skip to main content

quantwave_core/indicators/
obvm.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::smoothing::EMA;
3use crate::traits::Next;
4
5pub const METADATA: IndicatorMetadata = IndicatorMetadata {
6    name: "OBVM",
7    description: "On-Balance Volume Modified - a smoothed version of OBV with an additional signal line.",
8    usage: "Used to identify divergences between price and volume flow, and to generate signals via crossovers with its signal line. Values typically follow the trend of buying and selling pressure.",
9    keywords: &["volume", "obv", "momentum", "smoothing", "apirine"],
10    ehlers_summary: "While originally developed by Joe Granville, this modified version by Vitali Apirine applies exponential smoothing to the OBV values to filter out noise and adds a signal line for better trend identification and crossover signals. It provides a clearer picture of volume-price relationships by reducing high-frequency fluctuations. — TASC April 2020",
11    params: &[
12        ParamDef {
13            name: "obvm_period",
14            default: "7",
15            description: "EMA period for smoothing OBV",
16        },
17        ParamDef {
18            name: "signal_period",
19            default: "10",
20            description: "EMA period for the signal line",
21        },
22    ],
23    formula_source: "https://www.traders.com/Documentation/FEEDbk_docs/2020/04/TradersTips.html",
24    formula_latex: r#"
25\begin{aligned}
26TP &= \frac{High + Low + Close}{3} \\
27OBV_t &= OBV_{t-1} + \begin{cases} Volume, & \text{if } TP_t > TP_{t-1} \\ -Volume, & \text{if } TP_t < TP_{t-1} \\ 0, & \text{otherwise} \end{cases} \\
28OBVM &= EMA(OBV, Period_1) \\
29Signal &= EMA(OBVM, Period_2)
30\end{aligned}
31"#,
32    gold_standard_file: "obvm.json",
33    category: "Volume Indicators",
34};
35
36/// On-Balance Volume Modified (OBVM)
37///
38/// Modified OBV using typical price and smoothing.
39/// Based on Vitali Apirine's article in TASC April 2020.
40#[derive(Debug, Clone)]
41pub struct Obvm {
42    obv: f64,
43    prev_price: Option<f64>,
44    ema_obv: EMA,
45    ema_signal: EMA,
46}
47
48impl Obvm {
49    pub fn new(obvm_period: usize, signal_period: usize) -> Self {
50        Self {
51            obv: 0.0,
52            prev_price: None,
53            ema_obv: EMA::new(obvm_period),
54            ema_signal: EMA::new(signal_period),
55        }
56    }
57}
58
59impl Next<(f64, f64, f64, f64)> for Obvm {
60    type Output = (f64, f64);
61
62    fn next(&mut self, (high, low, close, volume): (f64, f64, f64, f64)) -> Self::Output {
63        let tp = (high + low + close) / 3.0;
64
65        match self.prev_price {
66            Some(prev) => {
67                if tp > prev {
68                    self.obv += volume;
69                } else if tp < prev {
70                    self.obv -= volume;
71                }
72            }
73            None => {
74                // First bar: some start with 0, others with current volume.
75                // Article implementations vary; starting with 0 is common.
76            }
77        }
78
79        self.prev_price = Some(tp);
80
81        let obvm_val = self.ema_obv.next(self.obv);
82        let signal_val = self.ema_signal.next(obvm_val);
83
84        (obvm_val, signal_val)
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use proptest::prelude::*;
92
93    #[test]
94    fn test_obvm_basic() {
95        let mut obvm = Obvm::new(7, 10);
96        // (H, L, C, V)
97        let inputs = vec![
98            (10.0, 10.0, 10.0, 1000.0), // TP = 10, OBV = 0
99            (11.0, 11.0, 11.0, 1000.0), // TP = 11, OBV = 1000
100            (12.0, 12.0, 12.0, 1000.0), // TP = 12, OBV = 2000
101            (11.0, 11.0, 11.0, 1000.0), // TP = 11, OBV = 1000
102        ];
103
104        let results: Vec<(f64, f64)> = inputs.into_iter().map(|x| obvm.next(x)).collect();
105        
106        // Check that values are being computed and are finite
107        for (obvm_val, signal_val) in results {
108            assert!(!obvm_val.is_nan());
109            assert!(!signal_val.is_nan());
110        }
111    }
112
113    proptest! {
114        #[test]
115        fn test_obvm_parity(
116            inputs in prop::collection::vec((1.0..100.0, 1.0..100.0, 1.0..100.0, 1.0..1000.0), 10..100),
117        ) {
118            let mut obvm = Obvm::new(7, 10);
119            
120            let mut obv = 0.0;
121            let mut prev_tp: Option<f64> = None;
122            let mut ema_obv = EMA::new(7);
123            let mut ema_signal = EMA::new(10);
124            
125            for (h, l, c, v) in inputs {
126                let h: f64 = h;
127                let l: f64 = l;
128                let c: f64 = c;
129                let high = h.max(l).max(c);
130                let low = h.min(l).min(c);
131                let tp = (high + low + c) / 3.0;
132                
133                if let Some(prev) = prev_tp {
134                    if tp > prev {
135                        obv += v;
136                    } else if tp < prev {
137                        obv -= v;
138                    }
139                }
140                prev_tp = Some(tp);
141                
142                let expected_obvm = ema_obv.next(obv);
143                let expected_signal = ema_signal.next(expected_obvm);
144                
145                let (actual_obvm, actual_signal) = obvm.next((high, low, c, v));
146                
147                approx::assert_relative_eq!(actual_obvm, expected_obvm, epsilon = 1e-10);
148                approx::assert_relative_eq!(actual_signal, expected_signal, epsilon = 1e-10);
149            }
150        }
151    }
152}