Skip to content

Trade-Sign Classification

Infer the aggressor side of each trade when the feed doesn't label it. L3 crypto (Bitstamp) ships buy_order_id / sell_order_id, so the trades frame carries a real direction; L2 / aggregated feeds and many CCXT sources don't, so the signed-flow metrics (compute_vpin, order_flow_imbalance) have nothing to work with. These classifiers fill that gap and are wired in as an automatic fallback.

Functions

classify_trade_sign

classify_trade_sign(
    trades: DataFrame,
    method: str = "lee_ready",
    quotes: DataFrame | None = None,
) -> pd.Series

Classify the aggressor side of each trade.

A drop-in source of the direction column for feeds that don't label the aggressor. Sorts trades chronologically, applies the chosen per-trade classifier, and returns the result realigned to the original index.

Parameters:

Name Type Description Default
trades DataFrame

Trades with at least timestamp and price.

required
method str

"tick" (:func:tick_rule) or "lee_ready" (:func:lee_ready, the default). "bvc" is not a per-trade classifier — it labels volume bars; use :func:bulk_volume_classification or :func:~ob_analytics.flow_toxicity.compute_vpin with sign_method="bvc" instead.

'lee_ready'
quotes DataFrame

Required for method="lee_ready". A quote frame with timestamp plus either a mid column (mid / midprice / mid_price) or a bid/ask pair (best_bid_price / best_ask_price, best_bid / best_ask, or bid / ask) — e.g. a pipeline depth_summary. The midpoint prevailing at or before each trade is used (a backward as-of join).

None

Returns:

Type Description
Series

Categorical "buy" / "sell" values named "direction", indexed like trades.

Raises:

Type Description
ConfigError

If method is unknown, if "bvc" is requested here, if required columns are missing, or if lee_ready is requested without usable quotes.

ObAnalyticsError

If trades is empty.

tick_rule

tick_rule(prices: ndarray | Series) -> np.ndarray

Classify trade signs by the tick rule.

Signs each trade from the sign of its price change relative to the previous trade: an uptick is buyer-initiated (+1), a downtick seller-initiated (-1). A zero tick (unchanged price) inherits the last non-zero sign — the classic Lee–Ready convention.

prices must already be in trade order (chronological). A leading run of zero ticks (before the first price move) is back-filled from the first determinable sign; a perfectly flat series has no information and defaults to +1.

Parameters:

Name Type Description Default
prices ndarray or Series

Trade prices in chronological order.

required

Returns:

Type Description
ndarray

int8 array of +1 (buy) / -1 (sell), one per trade.

lee_ready

lee_ready(
    prices: ndarray | Series, mid: ndarray | Series
) -> np.ndarray

Classify trade signs by the Lee–Ready quote-midpoint test.

A trade above the prevailing mid is buyer-initiated (+1), below it seller-initiated (-1). A trade at the mid — or one with no prevailing quote (mid is NaN) — falls back to the :func:tick_rule.

prices and mid must be equal-length and in chronological trade order; mid is the quote midpoint prevailing at (or just before) each trade — see :func:classify_trade_sign for aligning quotes to trades.

Parameters:

Name Type Description Default
prices ndarray or Series

Trade prices in chronological order.

required
mid ndarray or Series

Prevailing quote midpoint per trade (NaN where unknown).

required

Returns:

Type Description
ndarray

int8 array of +1 (buy) / -1 (sell), one per trade.

bulk_volume_classification

bulk_volume_classification(
    trades: DataFrame,
    bucket_volume: float,
    *,
    sigma: float | None = None,
) -> pd.DataFrame

Split volume bars into buy / sell fractions (Easley–LdP–O'Hara BVC).

Partitions cumulative trade volume into equal-sized buckets (the same volume bars :func:~ob_analytics.flow_toxicity.compute_vpin uses; a trade straddling a boundary is split proportionally) and estimates each bucket's buy fraction as

buy_fraction = Φ(ΔP / σ)

where ΔP is the bucket's close-to-close price change and σ the standard deviation of those changes. Unlike a per-trade classifier this labels volume, so it needs neither the aggressor side nor quotes — the VPIN-native method for feeds that carry only trade prints.

Parameters:

Name Type Description Default
trades DataFrame

Trades with timestamp, price, and volume.

required
bucket_volume float

Total volume per bucket (instrument-specific).

required
sigma float

Standard deviation of bucketed price changes. Estimated from the data (sample std of the bucket ΔP series) when omitted.

None

Returns:

Type Description
DataFrame

One row per completed bucket with columns bucket, timestamp_start, timestamp_end, close, delta_price, buy_fraction, buy_volume, sell_volume. Empty (no rows) if the trades don't fill a single bucket.

Raises:

Type Description
ConfigError

If required columns are missing.

ObAnalyticsError

If trades is empty.

ValueError

If bucket_volume is not positive, or sigma is not positive.