Skip to content

Commit 139469a

Browse files
author
Ariel Silahian
committed
VPIN Plugin performance and accuracy fixed
1 parent f11af41 commit 139469a

4 files changed

Lines changed: 187 additions & 62 deletions

File tree

VisualHFT.Plugins/Studies.VPIN/Model/PlugInSettings.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ namespace VisualHFT.Studies.VPIN.Model
77
public class PlugInSettings : ISetting
88
{
99
public double BucketVolSize { get; set; }
10+
public int? NumberOfBuckets { get; set; } // Rolling window size (nullable for backward compat)
1011
public string Symbol { get; set; }
1112
public Provider Provider { get; set; }
1213
public AggregationLevel AggregationLevel { get; set; }

VisualHFT.Plugins/Studies.VPIN/UserControls/PluginSettingsView.xaml

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,27 +19,42 @@
1919
SelectedItem="{Binding SelectedSymbol, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}"/>
2020

2121
<StackPanel Orientation="Horizontal" Margin="0,20,0,0">
22-
<Label Content="Bucket Volumen Size" />
22+
<Label Content="Bucket Volume Size" />
2323
<TextBlock Text="ℹ️" Cursor="Hand" Margin="10 5 0 0" >
2424
<TextBlock.ToolTip>
2525
<ToolTip>
2626
<StackPanel>
2727
<TextBlock TextWrapping="Wrap" >
28-
<Run FontWeight="Bold">Bucket Size</Run>
28+
<Run FontWeight="Bold">Bucket Volume Size (V)</Run>
2929
<LineBreak />
30-
<Run>The "Bucket Size" represents a predefined quantity or volume of data points or trades. </Run>
30+
<Run>The volume threshold that must accumulate before a bucket completes. Trades are split at bucket boundaries so each bucket contains exactly this volume.</Run>
3131
<LineBreak />
32-
<Run>When analyzing data, it's often grouped into "buckets" to make it more manageable and to identify patterns or trends over specific intervals.</Run>
3332
<LineBreak />
33+
<Run FontWeight="Bold">Paper Recommendation (ELO 2012)</Run>
3434
<LineBreak />
35-
<Run FontStyle="Italic">For example, if you're analyzing trade volumes and choose a bucket size of 100, the system will group trades in sets of 100 and then analyze each set as a single unit.</Run>
35+
<Run>V = ADV / n, where ADV = Average Daily Volume and n = Number of Buckets.</Run>
3636
<LineBreak />
37+
<Run>This calibrates each bucket to represent 1/n of a trading day.</Run>
3738
<LineBreak />
38-
<Run FontWeight="Bold">How to Use it</Run>
3939
<LineBreak />
40-
<Run>1. Choose a smaller bucket size for more granular analysis. This can help in identifying short-term patterns.</Run>
40+
<Run FontWeight="Bold">How to Estimate</Run>
4141
<LineBreak />
42-
<Run>2. Choose a larger bucket size for a broader overview, which can be useful for spotting long-term trends.</Run>
42+
<Run>1. Observe your instrument's daily traded volume (in base units, e.g., BTC, shares, contracts).</Run>
43+
<LineBreak />
44+
<Run>2. Divide by Number of Buckets (default 50).</Run>
45+
<LineBreak />
46+
<Run FontStyle="Italic">Example: If BTC/USD trades ~10,000 BTC/day with 50 buckets, set V = 200.</Run>
47+
<LineBreak />
48+
<Run FontStyle="Italic">Example: If a stock trades ~5M shares/day with 50 buckets, set V = 100,000.</Run>
49+
<LineBreak />
50+
<LineBreak />
51+
<Run FontWeight="Bold">Sizing Guidelines</Run>
52+
<LineBreak />
53+
<Run>- Too small: buckets fill in 1-2 trades, VPIN saturates at 1.0 (no mixing of buys/sells).</Run>
54+
<LineBreak />
55+
<Run>- Too large: buckets take too long to fill, VPIN updates infrequently.</Run>
56+
<LineBreak />
57+
<Run>- Aim for each bucket to contain at least 20-50 trades for meaningful buy/sell classification.</Run>
4358
<LineBreak />
4459
</TextBlock>
4560
</StackPanel>
@@ -49,7 +64,39 @@
4964
</StackPanel>
5065
<TextBox Text="{Binding BucketVolumeSize, ValidatesOnDataErrors=True}" Margin="0,5" />
5166

52-
67+
<StackPanel Orientation="Horizontal" Margin="0,20,0,0">
68+
<Label Content="Number of Buckets" />
69+
<TextBlock Text="&#x2139;&#xFE0F;" Cursor="Hand" Margin="10 5 0 0" >
70+
<TextBlock.ToolTip>
71+
<ToolTip>
72+
<StackPanel>
73+
<TextBlock TextWrapping="Wrap" >
74+
<Run FontWeight="Bold">Rolling Window Size (n)</Run>
75+
<LineBreak />
76+
<Run>The number of completed volume buckets used to compute VPIN.</Run>
77+
<LineBreak />
78+
<Run>VPIN = (1/n) x SUM |V_buy - V_sell| / V_bucket, averaged over the last n buckets.</Run>
79+
<LineBreak />
80+
<LineBreak />
81+
<Run FontWeight="Bold">Paper Recommendation (ELO 2012)</Run>
82+
<LineBreak />
83+
<Run>n = 50 (default). With V = ADV/50, 50 buckets spans approximately one trading day.</Run>
84+
<LineBreak />
85+
<LineBreak />
86+
<Run FontWeight="Bold">Tuning</Run>
87+
<LineBreak />
88+
<Run>- Fewer buckets (10-30): faster reaction to toxic flow, but noisier signal.</Run>
89+
<LineBreak />
90+
<Run>- More buckets (50-100): smoother signal, better for detecting sustained toxicity.</Run>
91+
<LineBreak />
92+
<Run>- The window and bucket size work together: n x V should represent the lookback volume you care about.</Run>
93+
</TextBlock>
94+
</StackPanel>
95+
</ToolTip>
96+
</TextBlock.ToolTip>
97+
</TextBlock>
98+
</StackPanel>
99+
<TextBox Text="{Binding NumberOfBuckets, ValidatesOnDataErrors=True}" Margin="0,5" />
53100

54101
<Label Content="Aggregation" Visibility="Hidden"/>
55102
<ComboBox ItemsSource="{Binding AggregationLevels}" Margin="0,5"

VisualHFT.Plugins/Studies.VPIN/VPINStudy.cs

Lines changed: 112 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -15,29 +15,37 @@
1515
namespace VisualHFT.Studies
1616
{
1717
/// <summary>
18-
/// The VPIN (Volume-Synchronized Probability of Informed Trading) value is a measure of the imbalance between buy and sell volumes in a given bucket. It's calculated as the absolute difference between buy and sell volumes divided by the total volume (buy + sell) for that bucket.
19-
///
20-
/// Given this definition, the range of VPIN values is between 0 and 1:
21-
/// 0: This indicates a perfect balance between buy and sell volumes in the bucket. In other words, the number of buy trades is equal to the number of sell trades.
22-
/// 1: This indicates a complete imbalance, meaning all the trades in the bucket are either all buys or all sells.
23-
/// Most of the time, the VPIN value will be somewhere between these two extremes, indicating some level of imbalance between buy and sell trades. The closer the VPIN value is to 1, the greater the imbalance, and vice versa.
18+
/// VPIN (Volume-Synchronized Probability of Informed Trading) measures order flow toxicity
19+
/// using volume-synchronized buckets per Easley, Lopez de Prado & O'Hara (2012).
20+
///
21+
/// Formula: VPIN = (1/n) * SUM |V_buy_i - V_sell_i| / V_bucket, over n completed buckets.
22+
///
23+
/// Range [0, 1]: 0 = balanced flow, 1 = fully toxic (all buys or all sells).
2424
/// </summary>
2525
public class VPINStudy : BasePluginStudy
2626
{
27-
private const string ValueFormat = "N1";
27+
private const string ValueFormat = "N2";
2828
private const string colorGreen = "Green";
2929
private const string colorWhite = "White";
30+
private const int DEFAULT_NUMBER_OF_BUCKETS = 50;
3031

3132
private bool _disposed = false; // to track whether the object has been disposed
3233
private PlugInSettings _settings;
34+
private readonly object _lockBucket = new object();
3335

3436
//variables for calculation
3537
private decimal _bucketVolumeSize; // The volume size of each bucket
36-
private decimal _currentBucketVolume; // The volume size of each bucket
38+
private decimal _currentBucketVolume; // Running accumulated volume in current bucket
3739
private decimal _lastMarketMidPrice = 0; //keep track of market price
3840
private decimal _currentBuyVolume = 0;
3941
private decimal _currentSellVolume = 0;
4042

43+
// Rolling window of completed bucket imbalances: |V_buy - V_sell| / V_bucket
44+
private decimal[] _bucketImbalances;
45+
private int _bufferIndex = 0;
46+
private int _bufferCount = 0;
47+
private decimal _rollingSum = 0; // Running sum for O(1) average calculation
48+
4149

4250
// Event declaration
4351
public override event EventHandler<decimal> OnAlertTriggered;
@@ -60,6 +68,7 @@ public class VPINStudy : BasePluginStudy
6068

6169
public VPINStudy()
6270
{
71+
_bucketImbalances = new decimal[DEFAULT_NUMBER_OF_BUCKETS];
6372
}
6473
~VPINStudy()
6574
{
@@ -106,43 +115,58 @@ private void TRADES_OnDataReceived(Trade e)
106115
return;
107116
if (_settings.Provider.ProviderID != e.ProviderId || _settings.Symbol != e.Symbol)
108117
return;
109-
if (!e.IsBuy.HasValue) //we do not know what it is
110-
return;
111-
if (_bucketVolumeSize == 0)
112-
_bucketVolumeSize = (decimal)_settings.BucketVolSize;
113-
114-
decimal bucketOverflow = 0;
115118

116-
117-
_currentBucketVolume += e.Size;
118-
if (_currentBucketVolume > _bucketVolumeSize) //We have overflow
119-
{
120-
bucketOverflow = _currentBucketVolume - _bucketVolumeSize;
121-
_currentBucketVolume = _bucketVolumeSize; //Cap it
122-
if (e.IsBuy.Value)
123-
_currentBuyVolume += e.Size - bucketOverflow;
124-
else
125-
_currentSellVolume += e.Size - bucketOverflow;
126-
}
127-
else //NO OVERFLOW
119+
lock (_lockBucket)
128120
{
129-
if (e.IsBuy.Value)
130-
_currentBuyVolume += e.Size;
121+
if (_bucketVolumeSize == 0)
122+
_bucketVolumeSize = (decimal)_settings.BucketVolSize;
123+
124+
// Tick rule: classify using mid-price from the order book
125+
// Price >= mid → buy (aggressor lifting the ask)
126+
// Price < mid → sell (aggressor hitting the bid)
127+
// Fallback to provider's IsBuy if no mid-price yet
128+
bool isBuy;
129+
if (_lastMarketMidPrice > 0)
130+
isBuy = e.Price >= _lastMarketMidPrice;
131+
else if (e.IsBuy.HasValue)
132+
isBuy = e.IsBuy.Value;
131133
else
132-
_currentSellVolume += e.Size;
133-
}
134-
135-
DoCalculation(bucketOverflow > 0); // will update vpin
134+
return; // No classification possible
136135

136+
decimal remainingSize = e.Size;
137137

138-
//assign overfowed volume to its proper variable.
139-
if (bucketOverflow > 0)
140-
{
141-
if (e.IsBuy.Value)
142-
_currentBuyVolume = bucketOverflow;
138+
// Assign entire trade to buy or sell for the current bucket portion
139+
if (isBuy)
140+
_currentBuyVolume += remainingSize;
143141
else
144-
_currentSellVolume = bucketOverflow;
145-
_currentBucketVolume = bucketOverflow;
142+
_currentSellVolume += remainingSize;
143+
_currentBucketVolume += remainingSize;
144+
145+
// Complete as many buckets as this trade fills
146+
while (_currentBucketVolume >= _bucketVolumeSize && _bucketVolumeSize > 0)
147+
{
148+
decimal bucketOverflow = _currentBucketVolume - _bucketVolumeSize;
149+
150+
// Trim the overflow from whichever side received it
151+
if (isBuy)
152+
_currentBuyVolume -= bucketOverflow;
153+
else
154+
_currentSellVolume -= bucketOverflow;
155+
_currentBucketVolume = _bucketVolumeSize;
156+
157+
DoCalculation(true); // Bucket completed
158+
159+
// Start new bucket with the overflow
160+
_currentBuyVolume = 0;
161+
_currentSellVolume = 0;
162+
if (isBuy)
163+
_currentBuyVolume = bucketOverflow;
164+
else
165+
_currentSellVolume = bucketOverflow;
166+
_currentBucketVolume = bucketOverflow;
167+
}
168+
169+
DoCalculation(false); // Interim update with current state
146170
}
147171
}
148172
private void LIMITORDERBOOK_OnDataReceived(OrderBook e)
@@ -152,7 +176,7 @@ private void LIMITORDERBOOK_OnDataReceived(OrderBook e)
152176
* TRANSFORM the incoming object (decouple it)
153177
* DO NOT hold this call back, since other components depends on the speed of this specific call back.
154178
* DO NOT BLOCK
155-
* IDEALLY, USE QUEUES TO DECOUPLE
179+
* IDEALLY, USE QUEUES TO DECOUPLE
156180
* ***************************************************************************************************
157181
*/
158182

@@ -161,34 +185,65 @@ private void LIMITORDERBOOK_OnDataReceived(OrderBook e)
161185
if (_settings.Provider.ProviderID != e.ProviderID || _settings.Symbol != e.Symbol)
162186
return;
163187

164-
_lastMarketMidPrice = (decimal)e.MidPrice;
165-
DoCalculation(false); //Interim update -> Just to send update.
188+
lock (_lockBucket)
189+
{
190+
_lastMarketMidPrice = (decimal)e.MidPrice;
191+
DoCalculation(false); //Interim update -> Just to send update.
192+
}
166193
}
167194
private void DoCalculation(bool isNewBucket)
168195
{
196+
// Caller must hold _lockBucket
169197
if (Status != VisualHFT.PluginManager.ePluginStatus.STARTED) return;
170198
string valueColor = isNewBucket ? colorGreen : colorWhite;
171199

200+
if (isNewBucket && _bucketVolumeSize > 0)
201+
{
202+
// Completed bucket: push imbalance into rolling window
203+
decimal bucketImbalance = Math.Abs(_currentBuyVolume - _currentSellVolume) / _bucketVolumeSize;
204+
205+
// Subtract the value being evicted (if buffer is full)
206+
if (_bufferCount == _bucketImbalances.Length)
207+
_rollingSum -= _bucketImbalances[_bufferIndex];
208+
else
209+
_bufferCount++;
210+
211+
_bucketImbalances[_bufferIndex] = bucketImbalance;
212+
_rollingSum += bucketImbalance;
213+
_bufferIndex = (_bufferIndex + 1) % _bucketImbalances.Length;
214+
}
215+
216+
// VPIN = average of completed bucket imbalances in the rolling window
172217
decimal vpin = 0;
173-
if ((_currentBuyVolume + _currentSellVolume) > 0)
174-
vpin = Math.Abs(_currentBuyVolume - _currentSellVolume) / (_currentBuyVolume + _currentSellVolume);
218+
if (_bufferCount > 0)
219+
vpin = _rollingSum / _bufferCount;
175220

176-
// Add to rolling window and remove oldest if size exceeded
177221
var newItem = new BaseStudyModel();
178222
newItem.Value = vpin;
179223
newItem.Format = ValueFormat;
180224
newItem.Timestamp = HelperTimeProvider.Now;
181225
newItem.MarketMidPrice = _lastMarketMidPrice;
182226
newItem.ValueColor = valueColor;
183227
newItem.AddItemSkippingAggregation = isNewBucket;
228+
184229
AddCalculation(newItem);
185230
}
186231
private void ResetBucket()
187232
{
188-
_bucketVolumeSize = 0;
189-
_currentSellVolume = 0;
190-
_currentBuyVolume = 0;
191-
_currentBucketVolume = 0;
233+
lock (_lockBucket)
234+
{
235+
_bucketVolumeSize = 0;
236+
_currentSellVolume = 0;
237+
_currentBuyVolume = 0;
238+
_currentBucketVolume = 0;
239+
240+
int n = _settings?.NumberOfBuckets ?? DEFAULT_NUMBER_OF_BUCKETS;
241+
if (n <= 0) n = DEFAULT_NUMBER_OF_BUCKETS;
242+
_bucketImbalances = new decimal[n];
243+
_bufferIndex = 0;
244+
_bufferCount = 0;
245+
_rollingSum = 0;
246+
}
192247
}
193248
/// <summary>
194249
/// This method defines how the internal AggregatedCollection should aggregate incoming items.
@@ -238,6 +293,10 @@ protected override void LoadSettings()
238293
{
239294
_settings.Provider = new Provider();
240295
}
296+
if (!_settings.NumberOfBuckets.HasValue || _settings.NumberOfBuckets.Value <= 0)
297+
{
298+
_settings.NumberOfBuckets = DEFAULT_NUMBER_OF_BUCKETS;
299+
}
241300
_settings.AggregationLevel = AggregationLevel.S1; //force to 1 second
242301
}
243302

@@ -251,6 +310,7 @@ protected override void InitializeDefaultSettings()
251310
_settings = new PlugInSettings()
252311
{
253312
BucketVolSize = 1,
313+
NumberOfBuckets = DEFAULT_NUMBER_OF_BUCKETS,
254314
Symbol = "",
255315
Provider = new ViewModel.Model.Provider(),
256316
AggregationLevel = AggregationLevel.S1
@@ -262,21 +322,22 @@ public override object GetUISettings()
262322
PluginSettingsView view = new PluginSettingsView();
263323
PluginSettingsViewModel viewModel = new PluginSettingsViewModel(CloseSettingWindow);
264324
viewModel.BucketVolumeSize = _settings.BucketVolSize;
325+
viewModel.NumberOfBuckets = _settings.NumberOfBuckets ?? DEFAULT_NUMBER_OF_BUCKETS;
265326
viewModel.SelectedSymbol = _settings.Symbol;
266327
viewModel.SelectedProviderID = _settings.Provider.ProviderID;
267328
viewModel.AggregationLevelSelection = _settings.AggregationLevel;
268329

269330
viewModel.UpdateSettingsFromUI = () =>
270331
{
271332
_settings.BucketVolSize = viewModel.BucketVolumeSize;
333+
_settings.NumberOfBuckets = viewModel.NumberOfBuckets;
272334
_settings.Symbol = viewModel.SelectedSymbol;
273335
_settings.Provider = viewModel.SelectedProvider;
274336
_settings.AggregationLevel = viewModel.AggregationLevelSelection;
275337
_bucketVolumeSize = (decimal)_settings.BucketVolSize;
276338
SaveSettings();
277339

278-
// Start the Reconnection
279-
// It will allow to reload with the new values
340+
// Reload with the new values
280341
Task.Run(() =>
281342
{
282343
ResetBucket();

0 commit comments

Comments
 (0)