VPIN: add a corrected form (off by default); fix Binance trade aggressor side - #78
Merged
silahian merged 1 commit intoSep 20, 2026
Conversation
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.
There was a problem hiding this comment.
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
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
BuyerIsMakerflag when populatingTrade.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
deleted the
port/vpin-corrected-form-and-binance-aggressor-side
branch
September 20, 2026 16:30
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

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 = 1and 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
UseCorrectedFormsetting. It defaults tofalse, so a settings file written before this option existed keeps the behaviour it already had.The corrected form:
0before any bucket closes reports the glyph that means "perfectly balanced" when nothing has been measured.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| / Vover 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.IsBuyhas 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,VersionandDescriptionare 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 inTileToolTip, which is not hashed.Binance: the trade aggressor side was inverted
Trade.IsBuymeans the aggressor bought. The Binance trade stream reports the opposite fact: itsmflag is true when the buyer was the resting maker, which makes the seller the aggressor. The connector published that flag unnegated: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.BuyerIsMakeris documented as["m"] Whether the buyer was the maker.The trade handler and the local order-book dictionary become
internalso the behaviour can be covered by a test, matching how the Coinbase connector is already arranged.Notes