quantwave_core/indicators/
obvm.rs1use 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#[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 }
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 let inputs = vec![
98 (10.0, 10.0, 10.0, 1000.0), (11.0, 11.0, 11.0, 1000.0), (12.0, 12.0, 12.0, 1000.0), (11.0, 11.0, 11.0, 1000.0), ];
103
104 let results: Vec<(f64, f64)> = inputs.into_iter().map(|x| obvm.next(x)).collect();
105
106 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}