Skip to content

Depth Metrics

Order book depth computation: price-level volume tracking, depth summary metrics in basis-point bins, spread extraction, and depth filtering.

PriceLevelBook is the book at L2: the size at each price level, kept one depth row at a time. DepthMetricsEngine builds the depth summary on it, and price_level_snapshots replays it to give the book at many instants, so a snapshot's touch always equals the depth summary's at that instant.

PriceLevelBook

PriceLevelBook(price_dtype: type | dtype = np.int64)

The book at L2: the size resting at each price level, kept one row at a time.

Each side is a pair of parallel numpy arrays sorted ascending by price, so the best ask is the first entry of its side and the best bid the last. Lookups are a searchsorted, and evicting crossed levels is one slice.

:meth:update applies one row of the depth table: a positive volume sets the size at that price, and zero removes the price level. A positive update also evicts every opposing price level it strictly crosses. The fresh quote is trusted over the older opposing level, whose delete is most likely missing from the feed. Equal prices are left alone, so a locked book is kept.

:class:DepthMetricsEngine builds the depth summary on this class, so a book kept here holds the depth summary's touch after every row.

Parameters:

Name Type Description Default
price_dtype numpy dtype

The dtype prices are held in: int64 ticks (the default), or float64 for a depth table already converted to the quote currency.

int64

Attributes:

Name Type Description
bid_prices, bid_volumes ndarray

The bid side, ascending by price.

ask_prices, ask_volumes ndarray

The ask side, ascending by price.

update

update(price: float, volume: float, side: int) -> bool

Apply one depth row and return whether it evicted opposing levels.

Parameters:

Name Type Description Default
price int or float

The price level, in the book's price units.

required
volume float

The size now resting at price; 0 removes the level.

required
side int

0 for a bid, 1 for an ask (the codes of :class:ob_analytics.engine.Direction).

required

Returns:

Type Description
bool

True when the update evicted crossed levels from the other side, so that side changed too.

price_level_snapshots

price_level_snapshots(
    depth: DataFrame,
    times: Iterable[datetime | Timestamp],
    max_levels: int | None = None,
) -> list[OrderBookSnapshot]

Return the book at L2 at each of times, replayed from the depth table.

One pass over depth drives a :class:PriceLevelBook, the book :class:DepthMetricsEngine builds the depth summary on, and copies it out at each instant. So the touch of each snapshot equals the depth summary's last row at or before that instant, and a stale level that a fresher opposing quote crossed is evicted here as it is there. This is the L2 counterpart of :func:ob_analytics.analytics.order_book, which rebuilds the per-order book at one instant from the events.

Parameters:

Name Type Description Default
depth DataFrame

The depth table: timestamp, price, volume and direction. Prices are integer ticks, or floats in the quote currency when the table was converted for display; both are kept as given.

required
times iterable of datetime.datetime or pandas.Timestamp

The instants to take a snapshot at, in any order. A row counts at an instant when its timestamp is at or before it.

required
max_levels int

Keep only this many price levels per side, nearest the touch first. None keeps every level.

None

Returns:

Type Description
list of OrderBookSnapshot

One per entry of times, in the same order. Each has the shape :func:~ob_analytics.analytics.order_book returns, with one row per price level: bids best first and asks best last, each with price, volume and liquidity (cumulative volume from the touch).

Raises:

Type Description
TypeError

If an instant and the depth timestamps disagree about being time-zone aware.

DepthMetricsEngine

DepthMetricsEngine(config: PipelineConfig | None = None)

Incrementally compute order book depth metrics.

Replaces the monolithic :func:depth_metrics function with a stateful, testable class. :meth:compute processes a whole depth frame; internally each event is applied via :meth:update_side, which writes one metrics row into a pre-allocated numpy buffer.

The book itself is a :class:PriceLevelBook, which holds each side as parallel numpy arrays sorted ascending by integer price. BPS-bin sums vectorize over the in-window slice of those arrays instead of iterating every active level in Python (levels average ~1.8k per side on the bundled sample, making that iteration the pipeline's former hot loop).

Output is written into a pre-allocated numpy matrix and converted to a DataFrame once at the end; bin boundaries are @lru_cache-d.

Parameters:

Name Type Description Default
config PipelineConfig

Pipeline configuration.

None

compute

compute(depth: DataFrame) -> pd.DataFrame

Process an entire depth DataFrame and return metrics.

This is the main entry point, equivalent to the legacy :func:depth_metrics function.

Parameters:

Name Type Description Default
depth DataFrame

Price-level volume data with columns timestamp, price, volume, direction.

required

Returns:

Type Description
DataFrame

Depth summary with timestamp, best_bid_price, best_bid_vol, best_ask_price, best_ask_vol, and volume-in-BPS-bin columns (e.g. bid_vol25bps).

update_side

update_side(
    price: int, volume: float, side: int, out: ndarray
) -> None

Process one depth event and write a metrics row into out.

Parameters:

Name Type Description Default
price int

Price as an integer tick count.

required
volume float

Volume at this price level (0 means deletion).

required
side int

0 = bid, 1 = ask.

required
out ndarray

Pre-allocated 1-D array of length row_len to fill.

required

depth_metrics

depth_metrics(
    depth: DataFrame, bps: int = 25, bins: int = 20
) -> pd.DataFrame

Compute limit order book depth metrics.

This is a convenience wrapper around :class:DepthMetricsEngine.

Parameters:

Name Type Description Default
depth DataFrame

DataFrame containing depth data.

required
bps int

Basis points increment for volume bins. Default is 25.

25
bins int

Number of bins to use for volume aggregation. Default is 20.

20

Returns:

Type Description
DataFrame

DataFrame containing depth metrics over time.

price_level_volume

price_level_volume(events: DataFrame) -> pd.DataFrame

Calculate the cumulative volume for each price level over time.

Parameters:

Name Type Description Default
events DataFrame

A pandas DataFrame containing limit order events. Sizes are the pipeline's integer lots, or floats such as the base-asset sizes of a table converted for display. Float sizes on a decimal grid are summed exactly on that grid, so a level that empties reads exactly 0 in either case. Float sizes on no such grid are summed as floats, with a warning.

required

Returns:

Type Description
DataFrame

A pandas DataFrame with the cumulative volume for each price level: one row per change to a level, holding the level's volume after the event named by event_id. Rows are in the canonical event order (:func:~ob_analytics.schemas.time_order_keys), and the sequence and ingest_seq columns are carried over from events when it has them, so the depth rows sort the same way the events do. The volume is in the units and dtype family of events["volume"].

Warns:

Type Description
UserWarning

When the events would take a level's size below zero. That means they take more size off the level than they put on it, which a loader that follows the schema's volume and fill rule never does. The warning names the level and the first event that overdrew it. The level's running total is held at zero, so it never reads a negative size, and the next order on it starts from zero. audit counts these rows as negative_level.

filter_depth

filter_depth(
    d: DataFrame,
    from_timestamp: Timestamp,
    to_timestamp: Timestamp,
) -> pd.DataFrame

Filter depth data within a specified time range.

Parameters:

Name Type Description Default
d DataFrame

DataFrame containing depth data.

required
from_timestamp Timestamp

Start of the time range.

required
to_timestamp Timestamp

End of the time range.

required

Returns:

Type Description
DataFrame

Filtered depth data within the specified time range.

get_spread

get_spread(depth_summary: DataFrame) -> pd.DataFrame

Extract the bid/ask spread from the depth summary.

Parameters:

Name Type Description Default
depth_summary DataFrame

A pandas DataFrame containing depth summary statistics.

required

Returns:

Type Description
DataFrame

A pandas DataFrame with the bid/ask spread data.

readable_quotes

readable_quotes(quotes: DataFrame) -> pd.DataFrame

Return the rows of quotes whose book can be read as a price.

This is the package's one test of a usable quote. The measurements that read a quote apply it: the mid a trade arrived into (:func:~ob_analytics.trade_sign.mid_before, and so Lee-Ready and the effective spread), the mid at an instant (:func:~ob_analytics.trade_sign.prevailing_mid), the spread a hidden trade printed inside (:func:~ob_analytics.hidden_liquidity.hidden_trades), the book columns of the feature table (:func:~ob_analytics.features.features) and the spread line on the depth heatmap.

Two states get through a depth summary that are not books anything could have traded against, and both would otherwise reach a reader as ordinary numbers:

A side with nothing resting on it. The depth engine writes a price and a volume of 0 for an empty side, which is a marker and not a price. Taken at face value it makes a spread the width of the whole instrument, a mid at half the other side, and a micro-price of zero: three finite numbers, none of them true, and none of them marked.

A crossed book, where the best bid is above the best ask. A diff feed can hold genuinely crossed resting orders, so this is an expected state on such a feed rather than a fault, but its midpoint is not a price and its spread is negative. A locked book, bid equal to ask, is a real state at a spread of zero and is kept.

A reader that drops these rows reaches back to the last quote that could be read, the way it reaches back over any other instant with no quote of its own. The test reads the first bid/ask pair the frame carries: best_bid_price / best_ask_price, best_bid / best_ask, or bid / ask. A frame with none of them cannot be tested and is returned unchanged.

Parameters:

Name Type Description Default
quotes DataFrame

Book snapshots: a pipeline depth_summary, or any frame shaped like one.

required

Returns:

Type Description
DataFrame

The readable rows, in their original order.