quantwave_core/indicators/
vpn.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::smoothing::SMA;
3use crate::indicators::ultimate_smoother::UltimateSmoother;
4use crate::indicators::volatility::ATR;
5use crate::traits::Next;
6
7#[derive(Debug, Clone)]
27pub struct VPNIndicator {
28 period: usize,
29 atr: ATR,
30 vp_sma: SMA,
31 vn_sma: SMA,
32 vol_sma: SMA,
33 smoother: UltimateSmoother,
34 prev_tp: Option<f64>,
35}
36
37impl VPNIndicator {
38 pub fn new(period: usize, smooth_period: usize) -> Self {
39 Self {
40 period,
41 atr: ATR::new(period),
42 vp_sma: SMA::new(period),
43 vn_sma: SMA::new(period),
44 vol_sma: SMA::new(period),
45 smoother: UltimateSmoother::new(smooth_period),
46 prev_tp: None,
47 }
48 }
49}
50
51impl Next<(f64, f64, f64, f64)> for VPNIndicator {
52 type Output = f64;
53
54 fn next(&mut self, (high, low, close, volume): (f64, f64, f64, f64)) -> Self::Output {
55 let tp = (high + low + close) / 3.0;
56 let atr = self.atr.next((high, low, close));
57 let mc = 0.1 * atr;
58
59 let (vmp, vmn) = match self.prev_tp {
60 Some(ptp) => {
61 let mf = tp - ptp;
62 if mf > mc {
63 (volume, 0.0)
64 } else if mf < -mc {
65 (0.0, volume)
66 } else {
67 (0.0, 0.0)
68 }
69 }
70 None => (0.0, 0.0),
71 };
72
73 self.prev_tp = Some(tp);
74
75 let vp_avg = self.vp_sma.next(vmp);
76 let vn_avg = self.vn_sma.next(vmn);
77 let vol_avg = self.vol_sma.next(volume);
78
79 let mav = if vol_avg <= 0.0 { 1.0 } else { vol_avg };
80
81 let vpn = (vp_avg - vn_avg) / mav * 100.0;
85
86 self.smoother.next(vpn)
87 }
88}
89
90pub const VPN_METADATA: IndicatorMetadata = IndicatorMetadata {
91 name: "Volume Positive Negative",
92 description: "Detects high-volume breakouts by comparing volume on up days vs down days, normalized between -100 and 100.",
93 usage: "Use to confirm breakouts. A VPN value crossing above a critical threshold (e.g., 10) signals a high-volume positive breakout.",
94 keywords: &["volume", "breakout", "katsanos", "vpn", "momentum"],
95 ehlers_summary: "While originally using EMA for smoothing, this implementation employs the UltimateSmoother to further reduce lag in detecting volume-driven trend shifts, aligning with modern DSP standards for technical indicators.",
96 params: &[
97 ParamDef {
98 name: "period",
99 default: "30",
100 description: "Calculation period for volume sums and ATR",
101 },
102 ParamDef {
103 name: "smooth_period",
104 default: "3",
105 description: "Smoothing period for the final VPN value",
106 },
107 ],
108 formula_source: "https://www.traders.com/Documentation/FEEDbk_docs/2021/04/TradersTips.html",
109 formula_latex: r#"
110\[
111TP = \frac{High + Low + Close}{3}
112\]
113\[
114MF = TP - TP_{t-1}
115\]
116\[
117MC = 0.1 \times ATR(Period)
118\]
119\[
120VP = \sum_{i=0}^{Period-1} (\text{if } MF_{t-i} > MC_{t-i} \text{ then } Volume_{t-i} \text{ else } 0)
121\]
122\[
123VN = \sum_{i=0}^{Period-1} (\text{if } MF_{t-i} < -MC_{t-i} \text{ then } Volume_{t-i} \text{ else } 0)
124\]
125\[
126MAV = \text{Average}(Volume, Period)
127\]
128\[
129VPN = \frac{VP - VN}{MAV \times Period} \times 100
130\]
131"#,
132 gold_standard_file: "vpn.json",
133 category: "Volume",
134};
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use crate::traits::Next;
140 use proptest::prelude::*;
141
142 #[test]
143 fn test_vpn_basic() {
144 let mut vpn = VPNIndicator::new(30, 3);
145 for i in 0..40 {
147 let val = 100.0 + i as f64;
148 let res = vpn.next((val + 1.0, val - 1.0, val, 1000.0));
149 if i > 35 {
150 assert!(res > 0.0);
151 }
152 }
153 }
154
155 proptest! {
156 #[test]
157 fn test_vpn_parity(
158 inputs in prop::collection::vec((10.0..20.0, 5.0..10.0, 7.0..15.0, 1000.0..5000.0), 50..100),
159 ) {
160 let period = 30;
161 let smooth_period = 3;
162 let mut vpn_ind = VPNIndicator::new(period, smooth_period);
163
164 let mut streaming_results = Vec::with_capacity(inputs.len());
165 for &val in &inputs {
166 streaming_results.push(vpn_ind.next(val));
167 }
168
169 let mut atr = ATR::new(period);
171 let mut vp_sma = SMA::new(period);
172 let mut vn_sma = SMA::new(period);
173 let mut vol_sma = SMA::new(period);
174 let mut smoother = UltimateSmoother::new(smooth_period);
175 let mut prev_tp = None;
176 let mut batch_results = Vec::with_capacity(inputs.len());
177
178 for &(h, l, c, v) in &inputs {
179 let tp = (h + l + c) / 3.0;
180 let cur_atr = atr.next((h, l, c));
181 let mc = 0.1 * cur_atr;
182
183 let (vmp, vmn) = match prev_tp {
184 Some(ptp) => {
185 let mf = tp - ptp;
186 if mf > mc {
187 (v, 0.0)
188 } else if mf < -mc {
189 (0.0, v)
190 } else {
191 (0.0, 0.0)
192 }
193 }
194 None => (0.0, 0.0),
195 };
196 prev_tp = Some(tp);
197
198 let vp_avg = vp_sma.next(vmp);
199 let vn_avg = vn_sma.next(vmn);
200 let vol_avg = vol_sma.next(v);
201
202 let mav = if vol_avg <= 0.0 { 1.0 } else { vol_avg };
203 let vpn = (vp_avg - vn_avg) / mav * 100.0;
204 batch_results.push(smoother.next(vpn));
205 }
206
207 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
208 approx::assert_relative_eq!(s, b, epsilon = 1e-10);
209 }
210 }
211 }
212}