BulkVolumeClassifier#
Description#
BulkVolumeClassifier implements the Bulk Volume Classification (BVC) model of
Easley, Lopez de Prado, and O'Hara (2012). It estimates the buy-initiated
fraction of a bar's volume without tick-level data, using only the bar's return
and its trailing volatility.
At each step the operator computes the standardized return
z = return_ / sigma_t, where sigma_t is the rolling standard deviation of
return_ over the most recent window_size observations, and evaluates the
standard normal CDF Phi(z) = 0.5 * (1 + erf(z / sqrt(2))). The result is a
fraction in [0, 1]: values near 1 indicate a predominantly buy-driven bar,
values near 0 indicate a sell-driven bar, and 0.5 indicates a neutral bar.
The rolling standard deviation is tracked with two running sums, so each step
costs O(1). The operator is causal and honors nan_policy: ignore; a
zero-variance window leaves the classification undefined and returns NaN.
A common pipeline is:
Compute bar log-returns with
LogReturn.Feed the return series to
BulkVolumeClassifierto obtain a per-bar buy-fraction estimate.Multiply by the bar's total volume to recover an estimated buy volume.
Parameters:
window_size(int, >= 2): number of observations in the trailing standard deviation window.start_policy(str, default"strict"): controls the warmup period beforewindow_sizeobservations have been seen."strict"emitsNaN."expanding"uses all available observations."zero"fills with zero.
Return value: the buy fraction at each time step, in [0, 1]. NaN during
warmup (under strict) or when the input return is NaN.
Reference: Easley, D., Lopez de Prado, M. M., & O'Hara, M. (2012). "Bulk classification of trading activity." Working Paper, Cornell University.
Examples#
Usage plot#
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from screamer import BulkVolumeClassifier
rng = np.random.default_rng(3)
n = 300
drift = np.concatenate([np.full(n // 2, 0.004), np.full(n - n // 2, -0.004)])
ret = drift + rng.standard_normal(n) * 0.01 # up-trend then down-trend
price = 100 * np.exp(np.cumsum(ret))
buy_frac = BulkVolumeClassifier(30)(ret)
fig = make_subplots(rows=2, cols=1, shared_xaxes=True, row_heights=[0.6, 0.4],
vertical_spacing=0.08)
fig.add_trace(go.Scatter(y=price, mode='lines', name='price',
line=dict(color='steelblue')), row=1, col=1)
fig.add_trace(go.Scatter(y=buy_frac, mode='lines', name='buy fraction',
line=dict(color='seagreen')), row=2, col=1)
fig.add_hline(y=0.5, line=dict(color='gray', dash='dot'), row=2, col=1)
fig.update_layout(title='BulkVolumeClassifier: estimated buy share (>0.5 buy-driven)',
yaxis=dict(title='price'), yaxis2=dict(title='buy fraction', range=[0, 1]),
margin=dict(l=20, r=20, t=60, b=20), legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1))
fig.show()