vike
Order flow

VPIN

Volume-synchronized probability of informed trading — fixed-volume buckets, the per-bucket imbalance, the rolling window, and the aggressor-free bulk-volume-classification twin with its side-bias read.

crates/vike-orderflow/src/vpin.rs carries two estimators of the same quantity. Vpin is the aggressor-classified form: it reads each trade's side through the classification module, "the crate's one load-bearing semantic". BvcVpin is the canonical Easley, López de Prado and O'Hara form, which needs no aggressor flag at all — price and volume only. Both are streaming, pure, and gated streaming-against-batch like every builder in the crate.

The mechanism

The trade stream is chopped into fixed-volume buckets of bucket_volume. A trade that overflows the open bucket spills its remainder into the next — the same splitting convention as bars::VolumeBarBuilder on the flow bars page. Each completed bucket scores an order-flow toxicity imbalance:

|Vbuy − Vsell| / Vbucket ∈ [0, 1]

VPIN is the rolling mean of the last window completed buckets — DEFAULT_VPIN_WINDOW is 50 — "maintained as an O(1) rolling sum (add-newest/subtract-evicted — never a window re-scan on the hot path)".

The module's hand-computed case, pinned in its tests: with a bucket volume of 4, two buys of 2 fill the first bucket at an imbalance of 1.0; a sell of 1, a buy of 1 and a sell of 2 fill the second at |1 − 3| / 4 = 0.5; VPIN reads (1.0 + 0.5) / 2 = 0.75.

Vpin — the aggressor-classified form

impl Vpin {
    pub fn new(bucket_volume: f64) -> Self;            // aggressor-classified, window 50
    pub fn with_params(bucket_volume: f64, window: usize, has_aggressor: bool) -> Self;
    pub fn push(&mut self, t: &TradeTick, mid: Option<f64>) -> Vec<f64>;
    pub fn committed(&self) -> Option<f64>;
    pub fn forming_bucket(&self) -> Option<VpinBucket>;
    pub fn interim(&self) -> Option<f64>;
    pub fn completed_buckets(&self) -> usize;
    pub fn bucket_imbalances(trades: &[TradeTick], bucket_volume: f64) -> Vec<f64>;
    pub fn from_trades(trades: &[TradeTick], bucket_volume: f64, window: usize) -> Option<f64>;
}

Construction asserts a finite, positive bucket_volume and a window of at least 1. push folds one trade and "returns the imbalances of every bucket this trade COMPLETED (usually empty; ≥ 2 when one big trade spans several buckets)"; a trade of non-positive size is skipped.

Classification. With has_aggressor — the default — each trade routes by classify. A feed without a real aggressor flag constructs with has_aggressor = false and passes the prevailing mid to push: price >= mid is a buy, else a sell — the quote-midpoint test. "A missing mid falls back to the aggressor flag so no trade is ever dropped."

Forming versus committed. The convention mirrors bars: committed() reads completed buckets only and is None until the first bucket completes. forming_bucket() and interim() are pure reads that fold the open bucket's partial imbalance, |b − s| / filled, in "as the NEWEST window sample (evicting the oldest when the window is full), without mutating state". interim() falls back to committed() when nothing is forming, and is None only before any volume has arrived.

Batch twins. bucket_imbalances is the full completed-bucket series with the trailing partial bucket dropped; from_trades is the last ≤ window entries of that series under "a naive-fold mean — a fresh sum, NOT the streaming rolling sum". The parity tests state what the two forms owe each other: on exact-dyadic sizes streaming and batch are bit-identical even past eviction; on non-dyadic sizes the per-bucket imbalances stay bit-identical (same fold order) and the rolling-sum mean may differ from the fresh fold only at rounding noise.

BvcVpin — the aggressor-free form

The header cites the paper: Easley, López de Prado & O'Hara (2012), "Flow Toxicity and Liquidity in a High-Frequency World", Review of Financial Studies 25(5):1457–1493 — "the paper that defines VPIN and Bulk Volume Classification (BVC)".

  • Buckets. Equal-volume buckets of bucket_volume (VBS), with the same spill convention. Each bucket's close price is the price of the trade that filled it.
  • BVC. A bucket's directional split comes from its price change, not from trade-level tags: buy_frac = Φ((P_i − P_{i−1}) / σ_ΔP), where Φ is the standard-normal CDF (norm_cdf, a libm erf mirroring vike-options) and σ_ΔP is the population standard deviation of bucket price changes over the last window buckets. Then V_buy = VBS·buy_frac, V_sell = VBS·(1 − buy_frac), and the bucket imbalance is |2·buy_frac − 1|.
  • The value. value() is the rolling mean of the last window imbalances — and None until window of them exist. The first bucket close only sets the P_{i−1} reference: "a price CHANGE needs two closes, so N imbalances need N+1 closes". The tests pin the boundary — a window of 2 is ready on the third close.
  • The tick-rule option. VolumeClassifier::TickRule classifies each bucket wholly by the sign of ΔP: up → all buy, down → all sell, flat → balanced. It is "the degenerate limit BVC itself falls back to when σ_ΔP is undefined".
  • Degenerate guards. A non-positive or non-finite bucket_volume and a window below 1 panic at construction; a non-positive-size trade is skipped; σ_ΔP ≤ 0, or fewer than two ΔP samples (the warm-up), falls back per bucket to the tick rule — "so a flat tape yields 0.0 (no NaN from Φ(0/0)) and a one-way tape yields 1.0".

The worked series from the tests: closes 100, 101, 100. The first is the reference; the second gives ΔP = +1 with one sample in the σ window, so the tick rule fires and the imbalance is 1.0; the third gives ΔP = −1 against σ = 1, buy_frac = Φ(−1), and an imbalance of |2·Φ(−1) − 1| = erf(1/√2) = 0.6826894921370859.

A side as well as a magnitude

value() is deliberately side-less. directional_bias() is the signed rolling mean of (2·buy_frac − 1) ∈ [−1, 1]: "> 0 buy-toxic → ask side, < 0 sell-toxic → bid side — so a caller can route the magnitude onto a per-side gate". It shares value()'s readiness gate.

Feeding it

on_trade(price, size) buckets internally and returns the imbalance of every bucket the trade completed (empty for the very first bucket); on_trade_tick is the TradeTick convenience, with the aggressor flag "deliberately unused — BVC needs none". A caller that has already formed equal-volume buckets feeds their close prices to on_bucket(close) instead — the two must not be interleaved on one instance. The batch twins are imbalances_from_closes ("a genuinely separate slice computation (NOT on_bucket in disguise)") and from_bucket_closes; the σ fold order is shared verbatim between the streaming and batch paths, so the per-bucket imbalance series is bit-identical across them.

In the chart

The chart engine's study registry (crates/vike-chart/src/studies.rs) builds its vpin entry — "VPIN (order-flow toxicity)" — on the aggressor-classified Vpin: bucket volume 1000 by default, window 50 (2 to 1000), an aggressor-flag switch whose 0 selects quote-midpoint classification, and reference bands at 0.2, 0.5 and 0.8. It is the one study that needs trades only and is never book-gated. What that registry can reach in the shipped app is on the in the GUI page; a walkthrough over a recorded tape is the VPIN tutorial.

A level, not an alarm

Neither estimator emits a signal. A threshold that adapts to its own history is what the crate's streaming quantile exists for — its header names "a VPIN alarm level tracking its own p95" as the intended use.

On this page