quantwave_core/indicators/
vfi.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::smoothing::{EMA, SMA};
3use crate::indicators::statistics::StandardDeviation;
4use crate::traits::Next;
5use std::collections::VecDeque;
6
7pub const METADATA: IndicatorMetadata = IndicatorMetadata {
8 name: "VFI",
9 description: "Volume Flow Indicator - a volume-based indicator that uses price and volume relative to a cutoff to measure money flow.",
10 usage: "Used to identify trend direction and potential reversals. Values above 0 are bullish, below 0 are bearish. Extreme readings and divergences are also significant.",
11 keywords: &["volume", "vfi", "money-flow", "katsanos", "oscillator"],
12 ehlers_summary: "Katsanos' Volume Flow Indicator (VFI) is based on the popular On Balance Volume (OBV) but with three main modifications: it is bounded, it filters out small price changes, and it caps volume extremes. It provides a more balanced view of buying and selling pressure by accounting for price volatility and volume outliers. — TASC June 2004",
13 params: &[
14 ParamDef {
15 name: "period",
16 default: "130",
17 description: "Lookback period for Vave and Summation",
18 },
19 ParamDef {
20 name: "coef",
21 default: "0.2",
22 description: "Coefficient for minimal price cut-off",
23 },
24 ParamDef {
25 name: "vcoef",
26 default: "2.5",
27 description: "Coefficient for volume cut-off",
28 },
29 ParamDef {
30 name: "smoothing_period",
31 default: "3",
32 description: "EMA period for final smoothing",
33 },
34 ],
35 formula_source: "https://www.traders.com/Documentation/FEEDbk_docs/2022/04/TradersTips.html",
36 formula_latex: r#"
37\begin{aligned}
38TP &= \frac{H+L+C}{3} \\
39Inter &= \ln(TP) - \ln(TP_{t-1}) \\
40VInter &= StdDev(Inter, 30) \\
41Cutoff &= Coef \cdot VInter \cdot Close \\
42Vave &= SMA(Volume, Period)_{t-1} \\
43Vmax &= Vave \cdot Vcoef \\
44VC &= \min(Volume, Vmax) \\
45MF &= TP - TP_{t-1} \\
46VCP &= \begin{cases} VC, & \text{if } MF > Cutoff \\ -VC, & \text{if } MF < -Cutoff \\ 0, & \text{otherwise} \end{cases} \\
47VFI_{raw} &= \frac{\sum_{i=0}^{Period-1} VCP_{t-i}}{Vave} \\
48VFI &= EMA(VFI_{raw}, 3)
49\end{aligned}
50"#,
51 gold_standard_file: "vfi.json",
52 category: "Volume Indicators",
53};
54
55#[derive(Debug, Clone)]
59pub struct Vfi {
60 period: usize,
61 coef: f64,
62 vcoef: f64,
63 prev_tp: Option<f64>,
64 stddev: StandardDeviation,
65 v_sma: SMA,
66 prev_v_ave: f64,
67 dir_vol_window: VecDeque<f64>,
68 dir_vol_sum: f64,
69 ema: EMA,
70}
71
72impl Vfi {
73 pub fn new(period: usize, coef: f64, vcoef: f64, smoothing_period: usize) -> Self {
74 Self {
75 period,
76 coef,
77 vcoef,
78 prev_tp: None,
79 stddev: StandardDeviation::new(30),
80 v_sma: SMA::new(period),
81 prev_v_ave: 0.0,
82 dir_vol_window: VecDeque::with_capacity(period),
83 dir_vol_sum: 0.0,
84 ema: EMA::new(smoothing_period),
85 }
86 }
87}
88
89impl Next<(f64, f64, f64, f64)> for Vfi {
90 type Output = f64;
91
92 fn next(&mut self, (high, low, close, volume): (f64, f64, f64, f64)) -> Self::Output {
93 let tp = (high + low + close) / 3.0;
94
95 let inter = match self.prev_tp {
96 Some(prev) if tp > 0.0 && prev > 0.0 => tp.ln() - prev.ln(),
97 _ => 0.0,
98 };
99
100 let v_inter = self.stddev.next(inter);
101 let cutoff = self.coef * v_inter * close;
102
103 let v_ave = if self.prev_v_ave == 0.0 {
105 self.v_sma.next(volume)
108 } else {
109 self.prev_v_ave
110 };
111 self.prev_v_ave = self.v_sma.next(volume);
113
114 let v_max = v_ave * self.vcoef;
115 let vc = volume.min(v_max);
116
117 let mf = match self.prev_tp {
118 Some(prev) => tp - prev,
119 None => 0.0,
120 };
121
122 let dir_vol = if mf > cutoff {
123 vc
124 } else if mf < -cutoff {
125 -vc
126 } else {
127 0.0
128 };
129
130 self.dir_vol_window.push_back(dir_vol);
132 self.dir_vol_sum += dir_vol;
133 if self.dir_vol_window.len() > self.period {
134 if let Some(oldest) = self.dir_vol_window.pop_front() {
135 self.dir_vol_sum -= oldest;
136 }
137 }
138
139 let vfi_raw = if v_ave != 0.0 {
140 self.dir_vol_sum / v_ave
141 } else {
142 0.0
143 };
144
145 self.prev_tp = Some(tp);
146
147 self.ema.next(vfi_raw)
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use proptest::prelude::*;
155
156 #[test]
157 fn test_vfi_basic() {
158 let mut vfi = Vfi::new(130, 0.2, 2.5, 3);
159 let inputs = vec![
160 (10.0, 10.0, 10.0, 1000.0),
161 (11.0, 11.0, 11.0, 1000.0),
162 (12.0, 12.0, 12.0, 1000.0),
163 (11.0, 11.0, 11.0, 1000.0),
164 ];
165
166 for input in inputs {
167 let res = vfi.next(input);
168 assert!(!res.is_nan());
169 }
170 }
171
172 proptest! {
173 #[test]
174 fn test_vfi_parity(
175 inputs in prop::collection::vec((1.0..100.0, 1.0..100.0, 1.0..100.0, 1.0..1000.0), 10..100),
176 ) {
177 let period = 130;
178 let coef = 0.2;
179 let vcoef = 2.5;
180 let smoothing = 3;
181 let mut vfi = Vfi::new(period, coef, vcoef, smoothing);
182
183 let mut prev_tp: Option<f64> = None;
185 let mut stddev = StandardDeviation::new(30);
186 let mut v_sma = SMA::new(period);
187 let mut prev_v_ave = 0.0;
188 let mut dir_vol_window = VecDeque::new();
189 let mut dir_vol_sum = 0.0;
190 let mut ema = EMA::new(smoothing);
191
192 for (h, l, c, v) in inputs {
193 let h: f64 = h;
194 let l: f64 = l;
195 let c: f64 = c;
196 let high = h.max(l).max(c);
197 let low = h.min(l).min(c);
198 let tp = (high + low + c) / 3.0;
199
200 let inter = match prev_tp {
201 Some(p) if tp > 0.0 && p > 0.0 => tp.ln() - p.ln(),
202 _ => 0.0,
203 };
204 let v_inter = stddev.next(inter);
205 let cutoff = coef * v_inter * c;
206
207 let v_ave = if prev_v_ave == 0.0 {
208 v_sma.next(v)
209 } else {
210 prev_v_ave
211 };
212 prev_v_ave = v_sma.next(v);
213
214 let v_max = v_ave * vcoef;
215 let vc = v.min(v_max);
216
217 let mf = match prev_tp {
218 Some(p) => tp - p,
219 None => 0.0,
220 };
221
222 let dir_vol = if mf > cutoff {
223 vc
224 } else if mf < -cutoff {
225 -vc
226 } else {
227 0.0
228 };
229
230 dir_vol_window.push_back(dir_vol);
231 dir_vol_sum += dir_vol;
232 if dir_vol_window.len() > period {
233 dir_vol_sum -= dir_vol_window.pop_front().unwrap();
234 }
235
236 let vfi_raw = if v_ave != 0.0 {
237 dir_vol_sum / v_ave
238 } else {
239 0.0
240 };
241
242 let expected_vfi = ema.next(vfi_raw);
243 let actual_vfi = vfi.next((high, low, c, v));
244
245 approx::assert_relative_eq!(actual_vfi, expected_vfi, epsilon = 1e-10);
246 prev_tp = Some(tp);
247 }
248 }
249 }
250}