Skip to content

VPIN: add a corrected form (off by default); fix Binance trade aggressor side - #78

Merged
silahian merged 1 commit into
masterfrom
port/vpin-corrected-form-and-binance-aggressor-side
Sep 20, 2026
Merged

silahian merged 1 commit into
masterfrom
port/vpin-corrected-form-and-binance-aggressor-side

Conversation

@silahian

Copy link
Copy Markdown
Collaborator

VPIN: an optional corrected form, off by default

The VPIN tile ships with a bucket volume size of 1. On any instrument that trades in whole lots, every bucket then holds exactly one unit from exactly one trade, so every bucket's imbalance is |1 - 0| / 1 = 1 and the mean of 1s is 1. The tile reads 1.00 permanently, from the first trade onward — the top of its own scale, labelled "high imbalance". That is arithmetic, not a bug to be debugged, and the settings validator only rejects sizes at or below zero, so nothing catches it.

More generally, small buckets read high on random flow by arithmetic alone. With B equally sized trades per bucket, the expected imbalance on coin-flip sides is 1.000 at B = 1, 0.500 at B = 2 and 0.375 at B = 4, falling toward zero only as B grows. Bucket volume decides the number.

This adds a corrected form alongside the existing one, selected by a new UseCorrectedForm setting. It defaults to false, so a settings file written before this option existed keeps the behaviour it already had.

The corrected form:

  • Classifies by the tick rule — a trade above the previous trade is a buy, below is a sell, an unchanged price repeats the previous side — instead of comparing the trade price against the order-book mid. The existing code comments call the mid comparison a "tick rule"; it is not one, since the tick rule compares against the previous trade price.
  • Publishes nothing until a full window of buckets has closed. Easley, López de Prado and O'Hara define the metric only over a full window; averaging over 1, then 2, then 3 completed buckets is a different quantity, and emitting 0 before any bucket closes reports the glyph that means "perfectly balanced" when nothing has been measured.
  • Publishes nothing while the bucket volume is below 20 times the median trade size, measured over the first 100 classifiable trades. Below that floor the reading describes the bucket size rather than the order flow.
  • No longer republishes on every order-book update. The value can only change when a bucket closes, so re-emitting between closes changed only the timestamp and the colour. The order book is still read for the displayed mid price.

The bucket arithmetic is unchanged: trades are still split at bucket boundaries with the remainder carried into the next bucket, and the window is still the mean of |V_buy - V_sell| / V over n completed buckets. Both are the paper's own definitions.

Why the tick rule rather than the paper's own classifier

The paper splits each bucket's volume between buy and sell in bulk, from the bucket's price change scaled by the volatility of price changes. Andersen and Bondarenko (2014, 2015) showed that this classifier is itself a function of volatility, so a metric built on it tracks volatility mechanically, and concluded it is unsuitable for signalling market turbulence. Chakrabarty, Pascual and Shkilko (2015) found the tick rule produces more accurate estimates of the same quantity.

Because a whole trade is assigned to one side rather than split fractionally, expect the corrected form to read higher than a bulk-classified figure on the same tape. That follows from the two constructions and has not been measured here. Compare ranks and percentiles rather than published absolute thresholds.

Why the exchange's own aggressor flag is not used

Trade.IsBuy has no documented convention and the connectors do not agree on what it means, so a tile built on it would read backwards on some venues. The tick rule needs nothing but the trade stream.

Text

Tooltips and descriptions are corrected to describe what the tile computes: it is not a probability, it is not a warning signal, and its bucket volume decides what it reads.

Name, Author, Version and Description are left byte for byte as they were, with a comment saying why: BasePluginStudy.GetPluginUniqueID() hashes all four together with the assembly name, and that hash keys both the plugin's saved settings and its registered metric. Editing any of them makes an existing installation fall back to defaults with an empty symbol and provider — the tile goes dead rather than merely resetting — and silently unhooks every alert rule built on the study. The user-visible text is in TileToolTip, which is not hashed.

Binance: the trade aggressor side was inverted

Trade.IsBuy means the aggressor bought. The Binance trade stream reports the opposite fact: its m flag is true when the buyer was the resting maker, which makes the seller the aggressor. The connector published that flag unnegated:

trade.IsBuy = eventData.BuyerIsMaker;

So every aggressive sell on this venue was reported as a buy and every aggressive buy as a sell. The other connectors report the taker's own side and are correct, which means any study reading trade side was comparing this venue against its own mirror image.

Confirmed against the client library's own documentation: BinanceStreamTrade.BuyerIsMaker is documented as ["m"] Whether the buyer was the maker.

The trade handler and the local order-book dictionary become internal so the behaviour can be covered by a test, matching how the Coinbase connector is already arranged.

Notes

  • Every expected value in the new tests is computed by hand from the definition, including an enumeration of all 16 sign patterns a four-trade bucket can take, which averages to exactly 0.375.
  • A trade large enough to fill many buckets is now counted rather than walked, so a bucket volume far below the trade size cannot spin the callback thread.

Adds an optional corrected form of VPIN alongside the existing one,
selected via a new UseCorrectedForm setting (default false, so a saved
settings file keeps the behaviour it already had). The corrected form
classifies trades by the tick rule instead of against the order-book
mid, publishes nothing until a full window of buckets has closed, and
publishes nothing while the bucket volume is too small to hold 20
trades - below which the reading is an artefact of the bucket size
rather than a property of the order flow. The bucket arithmetic and the
window formula are unchanged.

Tooltips and descriptions are corrected to describe what the tile
computes: the metric is not a probability and not a warning signal, and
bucket volume decides what it reads.

Also fixes the Binance connector reporting the trade aggressor side
backwards. Trade.IsBuy means the aggressor bought, but the stream's "m"
flag is true when the buyer was the resting maker, which makes the
seller the aggressor. The flag was published unnegated, so every
aggressive sell on this venue was reported as a buy, and every study
that reads trade side read this venue backwards.
Copilot AI lite review requested due to automatic review settings September 20, 2026 04:31

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Address bucket-volume validation, startup trade handling, and missing calculation tests.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity

Open (1)
What changed in this PR

Adds an opt-in corrected VPIN calculation and fixes Binance aggressor-side reporting.

Changes:

  • Adds tick-rule classification, full-window and trade-size gating.
  • Adds corrected-form settings and UI guidance.
  • Negates Binance’s BuyerIsMaker flag when populating Trade.IsBuy.
File Description
VisualHFT.Plugins/​Studies.VPIN/​VPINStudy.cs Integrates legacy and corrected VPIN forms.
VisualHFT.Plugins/​Studies.VPIN/​ViewModel/​PluginSettingsViewModel.cs Exposes corrected-form selection.
VisualHFT.Plugins/​Studies.VPIN/​UserControls/​PluginSettingsView.xaml Adds corrected-form controls and guidance.
VisualHFT.Plugins/​Studies.VPIN/​Model/​VpinBucketEngine.cs Implements corrected bucket calculations.
VisualHFT.Plugins/​Studies.VPIN/​Model/​PlugInSettings.cs Persists the new setting.
VisualHFT.Plugins/​MarketConnectors.Binance/​BinancePlugin.cs Corrects Binance aggressor-side mapping.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +367 to +368
if (_settings!.BucketVolSize > 0)
_engine = new VpinBucketEngine((decimal)_settings.BucketVolSize, n);
@silahian
silahian merged commit 6bb8f31 into master Sep 20, 2026
1 check passed
@silahian
silahian deleted the port/vpin-corrected-form-and-binance-aggressor-side branch September 20, 2026 16:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants