VPIN Verification Report: One Tape, Two Classifiers, Two Defects Fixed
The implementation reproduces the defining property of VPIN — a rising reading under informed flow — on a tape where the informed episode is known in advance: 0.3292 in the balanced segment against 0.6376 in the informed segment, a ratio of 1.94×. It did so only after two defects in its degenerate-case handling were found and fixed: one on 16 August 2026, one on 15 September 2026.
- Verification record
- v1.1 · first run 16 August 2026 · published as a report 4 September 2026 · second defect added 15 September 2026
- Implementation under test
quantmedia-research/vpin-order-flow-toxicity/vpin.py, in the public repository- Environment
- Python 3.11.9; numpy 2.4.6; pandas 3.0.5; scipy 1.17.1 — all newer than the package pins, so this doubles as a forward-compatibility check
- Verifier
- Cemil Ertürk
- Status
- Independent. Not peer reviewed. Synthetic data only; no claim about any real security.
On the same synthetic tape, mean VPIN is 0.4410 with bulk volume classification and 0.1779 with the tick rule. The classifier changes the reading substantially, so a VPIN threshold needs more context than a number.
- Reproduction record
- Checked 4 September 2026. Seed:
20260808. - Source
- vpin-order-flow-toxicity at revision
70ae45c. - Environment used
- Python 3.11.9, NumPy 2.4.6, pandas 3.0.5, SciPy 1.17.1.
- Data
- Synthetic. These examples test the implementation and illustrate the method; they do not measure trading performance on real securities.
Set up the experiment
The generator creates 20,000 trades with total volume 5,344,047. A segment with one-sided flow is inserted into an otherwise balanced tape. Both classifiers receive the same trades, bucket size of 21,376 and rolling window of 50 buckets.
Compare the output
| Measure | Bulk volume classification | Tick rule |
|---|---|---|
| Complete buckets | 250 | 250 |
| Mean VPIN | 0.4410 | 0.1779 |
| Minimum | 0.2805 | 0.1247 |
| Maximum | 0.8257 | 0.2918 |
For bulk volume classification, the mean in the balanced segment is 0.3292, compared with 0.6376 in the planted one-sided segment: a ratio of 1.94. This describes the two segments in this tape, not a forecast accuracy or trading return.
The buckets account for 5,344,000 volume units. The remaining 47 units are in an incomplete final bucket and are excluded by design. The console rounds this coverage to 100.0%; it is not exactly 100%.
What to inspect in the code
Bulk volume classification estimates buy and sell volume from standardized bucket price changes. The tick rule uses trade-to-trade price direction. Neither method observes the true identity or intent of a trader. Their difference here shows why changing the classification rule can change an apparent toxicity signal.
The tests also cover a special case: constant positive price changes have zero dispersion. The implementation handles that case by price-change sign, so a uniformly rising tape does not incorrectly receive a balanced 50/50 split. Flat changes receive an even split.
Limits and a useful next experiment
This is one seed and one constructed tape. It does not verify that VPIN predicts volatility in a real market. Try a different bucket size or window, keeping the trade tape fixed, to separate estimator sensitivity from changes in the underlying inputs. That experiment has now been run on this tape: see VPIN: why BVC and the tick rule disagree. Its numbers are not folded into this report.
Download the expected bucket output or inspect the tests at the checked revision.
Defects found during verification
Bulk volume classification standardises each bucket’s price change by the dispersion of price changes. On a monotone tape — every trade at a higher price than the last — that dispersion is zero, and the original branch handled the division by splitting volume 50/50. So a perfectly one-sided tape, the most toxic input imaginable, returned VPIN = 0. Exactly backwards.
Detection: a test constructing a monotone tape and asserting
VPIN > 0.9. Fix: in the degenerate branch, classify by the
sign of the price change (up → buy, down → sell, flat → 50/50).
Guard: the test is one of 15 in tests/test_vpin.py.
The tick rule signs each trade by its price change and lets an unchanged price inherit the previous sign, which is standard. The implementation also treated the first trade as buyer-initiated. On a tape whose price never moves there is no change to inherit from, so that opening guess propagated through every trade: 100% buy volume in every bucket, VPIN = 1, on an input that carries no information at all.
Detection: the degenerate-input
suite, which runs each method on inputs whose answer is obvious; a flat tape
was expected to read 0 under both classifiers and read 0 under BVC, 1 under the
tick rule. Fix: trades before the first price change carry no
sign and split evenly. Guard: tests/test_vpin.py
now asserts the flat tape reads 0; the published numbers on this page are
unchanged to four decimals (the first trade of the synthetic tape is the only
one affected) and verify_examples.py still matches the committed
CSV to nine decimals.
What this verification does not establish
- It proves the estimator responds to imbalance as designed. It proves nothing about whether VPIN forecasts volatility in real markets; that is contested (Andersen & Bondarenko, 2014) and outside what synthetic data can settle.
- The absolute level is method-dependent: BVC and the tick rule give 0.4410 and 0.1779 on identical data. No threshold such as “toxic above 0.4” means anything without stating classifier, bucket size and window.
- One seed, one tape. The direction of the result is robust to the seed; the magnitudes are not claimed to be.
Reproduce
python quantmedia-research/verify_examples.py re-runs this
example in a clean directory and asserts the shipped CSV matches to nine
decimal places. Then python tests/test_vpin.py from
quantmedia-research/: expected 15 passed.