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 ¶
The book at L2: the size resting at each price level, kept one row at a time.
Each side is a set 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.
:meth:update applies one row of the depth table: a positive volume sets
the size at that price, and zero removes the price level. Every level is
kept until its own row removes it, as in the depth table, even when a
quote on the other side crosses it.
The touch the book reports is uncrossed. :meth:quoted_bids and
:meth:quoted_asks leave out the crossed levels at each side's best end,
by the rule of :func:~ob_analytics.engine.crossed_prefix_counts, which
order_book(..., uncross=True) also uses: while the best bid is at or
above the best ask, the level updated less recently is left out. A level
left out is not lost: once the quote that crossed it goes, the level is
quoted again, even if it is stale, as the depth table still holds it.
Recency is the order the rows are applied in, one price level at a time.
order_book applies the rule to orders by their receive timestamps, so
the two can differ: at one instant the depth summary follows the row
order, where order_book leaves out the bid; and a new order at a stale
price makes the whole level recent here, while order_book still sees
the stale order as old.
:class:DepthMetricsEngine builds the depth summary on this class, so the
quoted levels here are the depth summary's book after every row.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
price_dtype
|
numpy dtype
|
The dtype prices are held in: |
int64
|
Attributes:
| Name | Type | Description |
|---|---|---|
bid_prices, bid_volumes |
ndarray
|
Every bid level, ascending by price. |
ask_prices, ask_volumes |
ndarray
|
Every ask level, ascending by price. |
crossed_bids, crossed_asks |
int
|
How many levels at each side's best end the quoted book leaves out. |
update ¶
Apply one depth row and return whether the other side's quote changed.
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; |
required |
side
|
int
|
|
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
quoted_bids ¶
The quoted bid levels, ascending by price: prices and volumes.
quoted_asks ¶
The quoted ask levels, ascending by price: prices and volumes.
BookLevels ¶
Bases: NamedTuple
The best price levels of each side of a book, one row per book state.
Each array is (rows, levels): column 0 is the touch, and each side
runs best first (asks from the lowest price, bids from the highest).
Prices are integer ticks and volumes integer lots. A price level the
side does not have holds price 0 and volume 0, so an empty one has
volume 0.
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: each snapshot holds the quoted
levels, which leave out a level crossed by a more recently updated quote
on the other side, as the depth summary does. 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: |
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
|
Returns:
| Type | Description |
|---|---|
list of OrderBookSnapshot
|
One per entry of times, in the same order. Each has the shape
:func: |
Raises:
| Type | Description |
|---|---|
TypeError
|
If an instant and the depth timestamps disagree about being time-zone aware. |
DepthMetricsEngine ¶
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 ¶
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 |
required |
Returns:
| Type | Description |
|---|---|
DataFrame
|
Depth summary with |
update_side ¶
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 |
required |
depth_metrics ¶
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 ¶
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 |
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 |
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 |
filter_depth ¶
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 ¶
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 ¶
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), the micro-price
(:func:micro_price) and the mids of :func:depth_signals, the mids the
trade tape, the price view, the book signals and the depth heatmap draw,
the mid line of the book ladder and the price range of the book replay,
and the empty sides of the Level 1 quote table
(:class:~ob_analytics.metrics.L1TickerMetric).
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 |
required |
Returns:
| Type | Description |
|---|---|
DataFrame
|
The readable rows, in their original order. |