Flow Toxicity Metrics¶
Market microstructure measures for detecting informed trading and quantifying price impact.
Functions¶
compute_vpin ¶
compute_vpin(
trades: DataFrame,
bucket_volume: float | None = None,
n_buckets: int = 50,
sign_method: str | None = None,
quotes: DataFrame | None = None,
) -> pd.DataFrame
Compute the Volume-Synchronized Probability of Informed Trading.
Partitions cumulative trade volume into equal-sized buckets and
measures the normalised buy/sell imbalance within each bucket. The
trailing average of vpin over n_buckets is the headline VPIN
metric.
Works on feeds without a native aggressor side. When the trades frame
has no direction, the buy/sell split is inferred with a trade-sign
classifier (see sign_method), so VPIN runs on L2 / aggregated captures
too.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
trades
|
DataFrame
|
Trades with at least |
required |
bucket_volume
|
float
|
Total volume per bucket. This is highly instrument-specific. When
left out, it is picked by :func: |
None
|
n_buckets
|
int
|
Window length (in buckets) for the trailing VPIN average.
Default is 50, following the original paper. Fewer complete buckets
than this is reported in |
50
|
sign_method
|
str
|
How to obtain the buy/sell split when there is no native
|
None
|
quotes
|
DataFrame
|
Quote frame for |
None
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
One row per completed bucket (zero rows, same columns and dtypes, when the trades fill no bucket) with columns:
The frame's
|
Raises:
| Type | Description |
|---|---|
ConfigError
|
If required columns are missing. |
ObAnalyticsError
|
If trades is empty. |
ValueError
|
If bucket_volume is not positive, or it is left out and the trades
span no time (see :func: |
vpin_bucket_volume ¶
vpin_bucket_volume(
trades: DataFrame,
buckets_per_day: int = VPIN_BUCKETS_PER_DAY,
trading_day: str | Timedelta = "24h",
) -> float
Pick a VPIN bucket_volume from the trades: average daily volume ÷ 50.
Average daily volume is the traded volume per unit of time, scaled to one trading_day::
daily volume = total volume × trading_day / (last timestamp − first timestamp)
The same formula covers every session length. A 30-minute capture is
scaled up to a full day at the rate it traded; a week-long one is averaged
down to a day. On a short capture the result is therefore a large bucket,
and VPIN will fill only a few of them, which :func:compute_vpin then
reports in its diagnostics.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
trades
|
DataFrame
|
Trades with at least |
required |
buckets_per_day
|
int
|
How many buckets one average day of volume fills. Default 50. |
VPIN_BUCKETS_PER_DAY
|
trading_day
|
str or Timedelta
|
Length of one trading day, on the same clock as the span between the
first and last trade. Default |
'24h'
|
Returns:
| Type | Description |
|---|---|
float
|
The bucket volume, in the units of |
Raises:
| Type | Description |
|---|---|
ConfigError
|
If required columns are missing. |
ObAnalyticsError
|
If trades is empty. |
ValueError
|
If the trades span no time, or buckets_per_day or trading_day is not positive. |
compute_kyle_lambda ¶
compute_kyle_lambda(
trades: DataFrame,
window: str = "5min",
*,
n_boot: int = 1000,
ci_level: float = 0.95,
seed: int | Generator | None = 0,
) -> KyleLambdaResult
Estimate Kyle's Lambda via OLS regression.
For each time window, computes:
- ΔPrice = last trade price − first trade price
- signed_volume = Σ(buy volume) − Σ(sell volume)
Then regresses ΔPrice on signed_volume across all windows. The slope (λ) measures how much the price moves per unit of net order flow — a proxy for market illiquidity and adverse selection.
ΔPrice is read directly from trades["price"], which is an integer tick
count, so λ is in ticks per unit volume. It scales with the price
unit — a run at a finer tick_size reports a proportionally larger λ;
multiply by tick_size to express it in the quote currency.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
trades
|
DataFrame
|
Trades with |
required |
window
|
str
|
Pandas frequency string for grouping trades. Default |
'5min'
|
n_boot
|
int
|
Number of bootstrap resamples for the confidence interval. Default
1000; |
1000
|
ci_level
|
float
|
Coverage of the interval, strictly between 0 and 1. Default 0.95. |
0.95
|
seed
|
int or Generator
|
Seed or generator for the bootstrap. Default |
0
|
Returns:
| Type | Description |
|---|---|
KyleLambdaResult
|
Frozen dataclass with |
Raises:
| Type | Description |
|---|---|
ConfigError
|
If required columns are missing. |
ObAnalyticsError
|
If trades is empty. |
ValueError
|
If ci_level is not between 0 and 1, or n_boot is negative. |
order_flow_imbalance ¶
order_flow_imbalance(
trades: DataFrame,
window: str = "1min",
sign_method: str | None = None,
quotes: DataFrame | None = None,
) -> pd.DataFrame
Compute normalised order flow imbalance per time window.
For each window:
ofi = (buy_volume − sell_volume) / (buy_volume + sell_volume)
Values range from −1 (all sells) to +1 (all buys). Zero indicates balanced flow.
Works on feeds without a native aggressor side: when the trades frame
has no direction, it is inferred with a per-trade trade-sign
classifier (see sign_method).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
trades
|
DataFrame
|
Trades with |
required |
window
|
str
|
Pandas frequency string. Default |
'1min'
|
sign_method
|
str
|
How to obtain the buy/sell split when there is no native
|
None
|
quotes
|
DataFrame
|
Quote frame for |
None
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
Columns: |
Raises:
| Type | Description |
|---|---|
ConfigError
|
If required columns are missing. |
ObAnalyticsError
|
If trades is empty. |
Models¶
KyleLambdaResult
dataclass
¶
KyleLambdaResult(
lambda_: float,
t_stat: float,
r_squared: float,
n_windows: int,
regression_df: DataFrame = pd.DataFrame(),
ci_low: float = float("nan"),
ci_high: float = float("nan"),
ci_level: float = 0.95,
)
Result of a Kyle's λ OLS regression.
Attributes:
| Name | Type | Description |
|---|---|---|
lambda_ |
float
|
Slope — price change per unit signed order flow (higher = less liquid). |
t_stat |
float
|
t-statistic for |
r_squared |
float
|
Fraction of ΔPrice variance explained by signed order flow. |
n_windows |
int
|
Number of time windows in the regression. |
regression_df |
DataFrame
|
Per-window |
ci_low, ci_high |
float
|
Bounds of a block-bootstrap confidence interval for |
ci_level |
float
|
Coverage of that interval, for example |
diagnostics
property
¶
Reasons lambda_ should not be relied on; empty when there are none.
Checks the fit is defined, that there are at least
:data:KYLE_MIN_WINDOWS windows, and that |t_stat| reaches
:data:KYLE_MIN_T_STAT.