1use std::collections::VecDeque;
4
5use crate::indicators::rolling_moments::RollingSum;
6use crate::microstructure::{Side, Trade};
7use crate::traits::Indicator;
8use crate::{Error, Result};
9
10#[derive(Debug, Clone)]
47pub struct Vpin {
48 bucket_volume: f64,
49 num_buckets: usize,
50 cur_buy: f64,
51 cur_sell: f64,
52 cur_total: f64,
53 window: VecDeque<f64>,
54 sum_imbalance: RollingSum,
55}
56
57impl Vpin {
58 pub fn new(bucket_volume: f64, num_buckets: usize) -> Result<Self> {
65 if num_buckets == 0 {
66 return Err(Error::PeriodZero);
67 }
68 if num_buckets > crate::error::MAX_PERIOD {
69 return Err(Error::InvalidPeriod {
70 message: crate::error::PERIOD_ABOVE_MAX,
71 });
72 }
73 if !bucket_volume.is_finite() || bucket_volume <= 0.0 {
74 return Err(Error::InvalidParameter {
75 message: "VPIN bucket_volume must be finite and positive",
76 });
77 }
78 Ok(Self {
79 bucket_volume,
80 num_buckets,
81 cur_buy: 0.0,
82 cur_sell: 0.0,
83 cur_total: 0.0,
84 window: VecDeque::with_capacity(num_buckets),
85 sum_imbalance: RollingSum::new(),
86 })
87 }
88
89 pub const fn params(&self) -> (f64, usize) {
91 (self.bucket_volume, self.num_buckets)
92 }
93
94 fn close_bucket(&mut self) {
95 let imbalance = (self.cur_buy - self.cur_sell).abs();
96 if self.window.len() == self.num_buckets {
97 let old = self.window.pop_front().expect("window is non-empty");
98 self.sum_imbalance.evict(old);
99 }
100 self.window.push_back(imbalance);
101 self.sum_imbalance.push(imbalance);
102 if self.sum_imbalance.needs_reseed(self.num_buckets) {
103 self.sum_imbalance.reseed(self.window.iter().copied());
104 }
105 self.cur_buy = 0.0;
106 self.cur_sell = 0.0;
107 self.cur_total = 0.0;
108 }
109}
110
111impl Indicator for Vpin {
112 type Input = Trade;
113 type Output = f64;
114
115 #[inline]
116 fn update(&mut self, trade: Trade) -> Option<f64> {
117 let mut remaining = trade.size;
118 let buy = trade.side == Side::Buy;
119
120 let capacity = self.bucket_volume - self.cur_total;
134 let window_full = self.num_buckets as f64 * self.bucket_volume;
135 if remaining.is_infinite() {
136 remaining = capacity + window_full;
138 } else if remaining > capacity {
139 let beyond = remaining - capacity;
140 if beyond / self.bucket_volume > self.num_buckets as f64 {
141 remaining = capacity + window_full + beyond % self.bucket_volume;
142 }
143 }
144
145 while remaining > 0.0 {
147 let capacity = self.bucket_volume - self.cur_total;
148 let take = remaining.min(capacity);
149 if buy {
150 self.cur_buy += take;
151 } else {
152 self.cur_sell += take;
153 }
154 self.cur_total += take;
155 remaining -= take;
156 if self.cur_total >= self.bucket_volume {
157 self.close_bucket();
158 }
159 }
160 if self.window.len() < self.num_buckets {
161 return None;
162 }
163 Some(self.sum_imbalance.value() / (self.num_buckets as f64 * self.bucket_volume))
164 }
165
166 fn reset(&mut self) {
167 self.cur_buy = 0.0;
168 self.cur_sell = 0.0;
169 self.cur_total = 0.0;
170 self.window.clear();
171 self.sum_imbalance.reset();
172 }
173
174 #[inline]
175 fn warmup_period(&self) -> usize {
176 1
180 }
181
182 #[inline]
183 fn is_ready(&self) -> bool {
184 self.window.len() == self.num_buckets
185 }
186
187 #[inline]
188 fn name(&self) -> &'static str {
189 "Vpin"
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196 use crate::traits::BatchExt;
197 use approx::assert_relative_eq;
198
199 fn trade(size: f64, side: Side) -> Trade {
200 Trade::new(100.0, size, side, 0).unwrap()
201 }
202
203 #[test]
204 fn rejects_bad_params() {
205 assert!(matches!(Vpin::new(10.0, 0), Err(Error::PeriodZero)));
206 assert!(matches!(
207 Vpin::new(0.0, 5),
208 Err(Error::InvalidParameter { .. })
209 ));
210 assert!(matches!(
211 Vpin::new(f64::NAN, 5),
212 Err(Error::InvalidParameter { .. })
213 ));
214 }
215
216 #[test]
217 fn accessors_and_metadata() {
218 let vpin = Vpin::new(10.0, 50).unwrap();
219 assert_eq!(vpin.params(), (10.0, 50));
220 assert_eq!(vpin.warmup_period(), 1);
221 assert_eq!(vpin.name(), "Vpin");
222 assert!(!vpin.is_ready());
223 }
224
225 #[test]
226 fn one_sided_flow_is_one() {
227 let mut vpin = Vpin::new(10.0, 2).unwrap();
229 let mut last = None;
230 for _ in 0..4 {
231 last = vpin.update(trade(5.0, Side::Buy));
232 }
233 assert_relative_eq!(last.unwrap(), 1.0, epsilon = 1e-12);
234 assert!(vpin.is_ready());
235 }
236
237 #[test]
238 fn balanced_flow_is_zero() {
239 let mut vpin = Vpin::new(10.0, 2).unwrap();
241 let mut last = None;
242 for _ in 0..4 {
243 vpin.update(trade(5.0, Side::Buy));
244 last = vpin.update(trade(5.0, Side::Sell));
245 }
246 assert_relative_eq!(last.unwrap(), 0.0, epsilon = 1e-12);
247 }
248
249 #[test]
250 fn large_trade_spans_multiple_buckets() {
251 let mut vpin = Vpin::new(10.0, 2).unwrap();
254 let out = vpin.update(trade(25.0, Side::Buy));
255 assert_relative_eq!(out.unwrap(), 1.0, epsilon = 1e-12);
257 }
258
259 #[test]
260 fn output_within_bounds() {
261 let mut vpin = Vpin::new(7.0, 4).unwrap();
262 for i in 0..200 {
263 let side = if i % 3 == 0 { Side::Sell } else { Side::Buy };
264 if let Some(v) = vpin.update(trade(1.0 + f64::from(i % 5), side)) {
265 assert!((0.0..=1.0).contains(&v), "out of bounds: {v}");
266 }
267 }
268 }
269
270 #[test]
271 fn zero_size_trade_is_noop() {
272 let mut vpin = Vpin::new(10.0, 1).unwrap();
273 assert_eq!(vpin.update(trade(0.0, Side::Buy)), None);
274 let out = vpin.update(trade(10.0, Side::Buy));
276 assert_relative_eq!(out.unwrap(), 1.0, epsilon = 1e-12);
277 }
278
279 #[test]
280 fn reset_clears_state() {
281 let mut vpin = Vpin::new(10.0, 2).unwrap();
282 for _ in 0..4 {
283 vpin.update(trade(5.0, Side::Buy));
284 }
285 assert!(vpin.is_ready());
286 vpin.reset();
287 assert!(!vpin.is_ready());
288 assert_eq!(vpin.update(trade(5.0, Side::Buy)), None);
289 }
290
291 #[test]
292 fn batch_equals_streaming() {
293 let trades: Vec<Trade> = (0..120)
294 .map(|i| {
295 let side = if i % 2 == 0 { Side::Buy } else { Side::Sell };
296 trade(1.0 + f64::from(i % 4), side)
297 })
298 .collect();
299 let batch = Vpin::new(8.0, 5).unwrap().batch(&trades);
300 let mut b = Vpin::new(8.0, 5).unwrap();
301 let streamed: Vec<_> = trades.iter().map(|t| b.update(*t)).collect();
302 assert_eq!(batch, streamed);
303 }
304
305 #[test]
310 fn enormous_size_terminates() {
311 let mut vpin = Vpin::new(8.0, 5).unwrap();
312 let value = vpin.update(trade(1.397_926_697_262_895_6e277, Side::Buy));
313 assert_eq!(
314 value,
315 Some(1.0),
316 "a one-sided flood is maximally imbalanced"
317 );
318 }
319
320 #[test]
326 fn infinite_size_terminates() {
327 let mut vpin = Vpin::new(8.0, 5).unwrap();
328 let flood = Trade::new_unchecked(100.0, f64::INFINITY, Side::Buy, 0);
329 assert_eq!(vpin.update(flood), Some(1.0));
330 }
331
332 #[test]
336 fn bounding_preserves_the_remainder() {
337 let mut bounded = Vpin::new(8.0, 5).unwrap();
338 bounded.update(trade(1000.5, Side::Buy));
339
340 let mut unbounded = Vpin::new(8.0, 5).unwrap();
343 for _ in 0..2001 {
344 unbounded.update(trade(0.5, Side::Buy));
345 }
346 assert_eq!(bounded.cur_total, unbounded.cur_total);
347 assert_eq!(
348 bounded.update(trade(1.0, Side::Sell)),
349 unbounded.update(trade(1.0, Side::Sell))
350 );
351 }
352
353 #[test]
354 fn a_size_below_the_bound_is_untouched() {
355 let mut vpin = Vpin::new(8.0, 5).unwrap();
358 assert_eq!(vpin.update(trade(48.0, Side::Buy)), Some(1.0));
359 assert_eq!(vpin.cur_total, 0.0);
360 }
361}