Flow toxicity

VPIN, OFI, PIN, and Hawkes branching ratio. Cross-validated signals for informed and self-exciting flow.

Flow-toxicity measures operate on trade and quote streams. Most do NOT require counterparty attribution, so they work on Kalshi, equities, and options just as well as on Polymarket. Cross-referencing multiple measures is the only defensible use. A single measure in isolation, especially VPIN, is contested in the literature.

VPIN

Volume-synchronized Probability of Informed Trading. Easley, López de Prado, O’Hara (2012). Implemented in horizon.flow.toxicity.VPINEstimator.

Method

Volume buckets, not time buckets. Each bucket accumulates a target USD notional; when full, the entire bucket is classified buy / sell using the bulk-volume classification (BVC):

text
V_buy(tau) = V(tau) * Phi( dP(tau) / sigma_dP )
V_sell(tau) = V(tau) - V_buy(tau)

where Phi is the standard-normal CDF, dP(tau) is the price change across the bucket (end minus start), and sigma_dP is the std of price changes across the rolling window of previous buckets. VPIN over the last N buckets:

text
VPIN = average over tau in window of | V_buy(tau) - V_sell(tau) | / V(tau)

Values near 0 mean balanced flow; values near 1 mean one-sided flow, which Easley et al. interpret as evidence of informed trading.

Usage

python
from horizon.flow.config import VPINConfig
from horizon.flow.toxicity import VPINEstimator
from datetime import datetime, timedelta, timezone

v = VPINEstimator(VPINConfig(
 volume_bucket_usd=50_000.0,
 volume_bucket_usd_alt=250_000.0,
 window_buckets=50,
))
t = datetime.now(timezone.utc)
for trade in trades:
 v.add_trade(price=trade.price, size=trade.size, timestamp=trade.ts)

primary = v.vpin() # the reported headline number
alt = v.vpin_alt() # same stream, larger bucket
divergence = v.sensitivity_divergence() # abs(primary - alt)

Andersen-Bondarenko (2014) and why we ship two bucket sizes

Andersen-Bondarenko argued VPIN is sensitive to bucket-size choice and can produce spurious signals under standard tick regimes. We ship two bucket sizes side-by-side and expose sensitivity_divergence() precisely so the reader can see whether a VPIN number is a real signal or a bucket artifact. Divergence near zero = both sizes agree = trust the signal. Large divergence = bucket artifact, disregard.

This is also why no detector in horizon.flow uses VPIN alone as a binary classifier. Manipulation detectors cross-reference VPIN with OFI and the Hawkes branching ratio before emitting a finding.

OFI

Order Flow Imbalance. Cont, Kukanov, Stoikov (2014). Implemented in horizon.flow.toxicity.OrderFlowImbalance.

Method

Top-of-book snapshot deltas produce signed contributions:

text
# Bid-side contribution
if P_bid(n) > P_bid(n-1): e += +q_bid(n)
if P_bid(n) = P_bid(n-1): e += (q_bid(n) - q_bid(n-1))
if P_bid(n) lt P_bid(n-1): e += -q_bid(n-1)

# Ask-side contribution
if P_ask(n) lt P_ask(n-1): e += -q_ask(n)
if P_ask(n) = P_ask(n-1): e += (q_ask(n-1) - q_ask(n))
if P_ask(n) > P_ask(n-1): e += +q_ask(n-1)

OFI(t) = sum(e_n) over n in the lookback window ending at t

Positive OFI = net buy pressure at the top of book. Cont et al. showed OFI is a cleaner short-horizon predictor of the next price move than VPIN on equity LOBs.

Usage

python
from horizon.flow.config import OFIConfig
from horizon.flow.toxicity import OrderFlowImbalance

ofi = OrderFlowImbalance(OFIConfig(lookback_s=30.0))
for q in quotes:
 ofi.on_quote(
 market_id=q.market_id,
 bid=q.bid, bid_size=q.bid_size,
 ask=q.ask, ask_size=q.ask_size,
 timestamp=q.ts,
 )

val = ofi.value("AAPL", now=datetime.now(timezone.utc))

The rolling window prunes automatically. Contributions older than lookback_s drop off.

PIN

Probability of Informed Trading. Easley, Kiefer, O’Hara, Paperman (1996). Batch-only, structural view to complement VPIN’s dynamic one.

Method

EKOP mixture model: three daily states (no news, good news, bad news) with Poisson buy + sell arrivals. Maximum-likelihood fit on a history of per-day (buys, sells) counts yields four parameters (alpha, delta, mu, epsilon), then:

text
PIN = (alpha * mu) / (alpha * mu + 2 * epsilon)

Usage

python
from horizon.flow.toxicity import PINEstimator

estimator = PINEstimator()
result = estimator.estimate(daily_counts=[(120, 115), (130, 90), (105, 145), ...])
# PINResult(pin=0.17, alpha=0.35, delta=0.48, mu=22.1, epsilon=98.3, ...)

Uses scipy.optimize.minimize when available (falls back to a crude coordinate-descent search without scipy). Slow: run nightly rather than inline. Best used as a retrospective benchmark against which VPIN’s faster signal is compared.

Hawkes branching ratio

The cleanest fingerprint of algorithmic self-excitation. Bacry, Jaimungal, Muzy (2015). Implemented in horizon.flow.toxicity.HawkesFingerprint.

Why this matters

A Hawkes process has intensity lambda(t) = mu + sum over past events j of phi(t - t_j). Every past event raises the probability of future events. The branching ratio n = integral phi(u) du (in [0, 1)) quantifies reflexivity. As n approaches 1, every event triggers on average one descendant. A pattern you see in cascading bot activity, not in humans reacting to exogenous signals.

Method

Two estimators, tried in order:

  1. MLE for exponential kernel phi(u) = alpha * exp(-beta * u) via scipy.optimize.minimize. Returns (mu, alpha, beta) and n = alpha / beta. Runs when scipy is installed.
  2. Moments estimator (Hardiman-Bouchaud 2014): bin events, compute Fano factor F = Var/Mean, then n ≈ 1 - 1/F. Runs without any dependency.
python
from horizon.flow.config import HawkesConfig
from horizon.flow.toxicity import HawkesFingerprint

h = HawkesFingerprint(HawkesConfig(window_s=300.0, kernel_decay_s=1.0))
for ts in actor_event_timestamps:
 h.observe(ts)

result = h.estimate()
# HawkesResult(branching_ratio=0.72, base_intensity=0.8, alpha=0.9, beta=1.25, method='mle', n_events=210)

Interpretation

  • n ≲ 0.2: Poisson-ish, unrelated arrivals. Human or fundamental-driven actor.
  • 0.2 to 0.5: Moderate self-excitation. Mixed fundamental / reactive behavior.
  • n > 0.5: Substantially self-exciting. Bot fingerprint. Especially when paired with low inter-arrival variance.
  • n → 1: Near-critical reflexive regime. Consistent with cascading HFT activity; also what Filimonov-Sornette (2012) flag as flash-crash risk.

Used in combination with the ActorFeatures.hawkes_branching_ratio cheap proxy: the proxy updates on every event (CV-based); the full MLE runs on demand when compliance wants the defensible number.

Cross-validation

Any one measure is not enough. The flow module treats a “toxic market” assertion as requiring at least two concordant signals. Example compliance rule:

  • VPIN above 0.4 AND sensitivity divergence below 0.15 (VPIN is not an artifact)
  • OR OFI persistently one-sided over multi-minute window
  • AND Hawkes branching ratio > 0.5 for at least one actor in the market

Detector-level code does this composition implicitly: the spoofing detector uses book imbalance plus fast-cancel timing; the wash-trade detector uses round-number bias plus Benford chi-squared plus same-origin on-chain links. No detector emits on a single axis.

Citations

  • Easley, D., López de Prado, M. M., O’Hara, M. (2012). “Flow Toxicity and Liquidity in a High-frequency World.” Review of Financial Studies, 25(5), 1457–1493.
  • Andersen, T. G., Bondarenko, O. (2014). “VPIN and the Flash Crash.” Journal of Financial Markets, 17, 1–46.
  • Cont, R., Kukanov, A., Stoikov, S. (2014). “The Price Impact of Order Book Events.” Journal of Financial Econometrics, 12(1), 47–88.
  • Easley, D., Kiefer, N. M., O’Hara, M., Paperman, J. B. (1996). “Liquidity, Information, and Infrequently Traded Stocks.” Journal of Finance, 51(4), 1405–1436.
  • Bacry, E., Jaimungal, S., Muzy, J.-F. (2015). “Hawkes processes in finance.” Market Microstructure and Liquidity.
  • Filimonov, V., Sornette, D. (2012). “Quantifying reflexivity in financial markets.” Physical Review E, 85(5), 056108.
  • Hardiman, S. J., Bouchaud, J.-P. (2014). “Branching-ratio approximation for the self-exciting Hawkes process.”