Skip to content

Order-book engine

The stateful core: order events in, book states and order lifecycles out. This is the only part of the library that knows how a book is rebuilt, and the only part that holds no pandas — everything crosses its interface as NumPy arrays, so the inside can be made faster later (numba, then possibly Rust) without anything above it moving.

Most users never call it directly. Its frame-level faces are order_book, order_lifecycles, and the functions in ob_analytics.queue; they convert to and from these arrays.

What crosses the interface

OrderEvents is the shared event schema (data contracts) in column form. Results carry a row index back into those arrays rather than copying the event's own columns out, so a caller reads the exchange_timestamp, the classifier type, the venue and symbol, or anything else it tracks off its own table at that row. Adding a column to the schema therefore never widens this interface, and the engine never has to learn a vocabulary — order types, venue names — that belongs to the layer above.

Timestamps are int64 nanoseconds since the epoch, UTC: a time zone is a presentation detail, so it is stripped on the way in and re-attached on the way out. Prices are integer ticks; the engine only compares and subtracts them, so a float column from a pre-tick frame still works.

Categorical columns arrive as integer codes — Direction, Action, Outcome. Each derives the schema's string from its own member name, so the integer the engine compares and the label a frame carries cannot fall out of step.

Input

OrderEvents dataclass

OrderEvents(
    order_id: ndarray,
    timestamp: ndarray,
    price: ndarray,
    volume: ndarray,
    direction: ndarray,
    action: ndarray,
    fill: ndarray | None = None,
    is_market: ndarray | None = None,
)

One order-event stream as parallel numpy columns.

Every array has the same length: one entry per event, in stream order.

Ordering contract

Rows must arrive in the venue's canonical event order — the total order :func:ob_analytics.schemas.time_order_keys defines (timestamp, then the tie-breaks the frame carries). At minimum, the rows belonging to one order_id must be chronological: the reconstructions read the last row per order as that order's current state, and replay the stream front to back. Sorting is the caller's job, so the engine never has to guess which tie-break columns a venue publishes.

Attributes:

Name Type Description
order_id ndarray

The venue's per-order identifier (int64). :data:HIDDEN_ORDER_ID marks an order with no public identity.

timestamp ndarray

Receive-clock time as int64 nanoseconds since the epoch, UTC. The zone is dropped on the way in and re-attached on the way out, so the engine compares plain integers.

price ndarray

Price as a whole number of ticks (int64). A float column from a pre-tick frame also works — the engine only ever compares and subtracts prices.

volume ndarray

The order's outstanding size after the event (float64), per the schema's volume contract.

direction ndarray

:class:Direction codes.

action ndarray

:class:Action codes.

fill ndarray or None

Quantity executed at this event (float64), 0 when nothing traded. Required by :func:~ob_analytics.engine.order_lifecycles; ignored by the other reconstructions.

is_market ndarray or None

True where the classifier labelled the order market — an order that crosses rather than rests. Market rows never join the book, so :func:~ob_analytics.engine.book_state excludes them. None means "nothing is a market order".

Raises:

Type Description
ValueError

If the columns are not all one-dimensional and the same length.

visible

visible(*, side: Direction | None = None) -> np.ndarray

Rows holding visible orders, optionally on one side only.

Orders sharing :data:HIDDEN_ORDER_ID have no public identity, so they never join the visible queue. Both queue reconstructions select rows through here, and so does any caller that needs to size a window over the same rows the engine will replay — the rule lives in one place.

require_fill

require_fill(who: str) -> np.ndarray

Return :attr:fill, or raise when the caller omitted it.

market_mask

market_mask() -> np.ndarray

Return :attr:is_market, or an all-False mask when it is absent.

Codes

Direction

Bases: Code

Which side of the book an order sits on.

Action

Bases: Code

What an event did to the order it names.

Outcome

Bases: Code

How an order ended.

Reconstructions

book_state

book_state(
    events: OrderEvents, *, at: int, uncross: bool = False
) -> BookState

Reconstruct the order book at one instant.

Parameters:

Name Type Description Default
events OrderEvents

The event stream, in canonical order.

required
at int

The instant to evaluate the book at, in nanoseconds since the epoch (UTC). Events after it are ignored.

required
uncross bool

When True, evict crossed resting orders so the snapshot satisfies best_bid < best_ask — a display convenience mirroring the depth engine's crossed-level eviction. The default is False: the reconstruction stays faithful to the feed, so a diff feed's genuinely crossed resting orders are replayed as they arrived rather than silently uncrossed. It has no effect on a matched-book feed, which is never crossed.

False

Returns:

Type Description
BookState

Both sides, best price first, each carrying the row that put every resting order in its current state.

order_lifecycles

order_lifecycles(
    events: OrderEvents,
    *,
    fill_tolerance: float = DEFAULT_FILL_TOLERANCE,
) -> OrderLifecycles

Collapse events into one row per order.

Termination follows the schema's volume contract: an order ends when a deleted row arrives or its outstanding size reaches zero. The second is how a fully executed order ends on a venue that emits no delete for it (LOBSTER); the created row itself is excluded from the test so a zero-size placement does not terminate itself.

Parameters:

Name Type Description Default
events OrderEvents

The event stream, in canonical order, carrying fill.

required
fill_tolerance float

How far short of its placed size an order's executed total may fall and still count as fully filled. Defaults to :data:DEFAULT_FILL_TOLERANCE; raise it for a venue whose quantities are coarser than 8 decimal places.

DEFAULT_FILL_TOLERANCE

Returns:

Type Description
OrderLifecycles

One row per submitted order, ordered by first placement.

queue_positions

queue_positions(
    events: OrderEvents, *, touch_only: bool = True
) -> QueuePositions

Reconstruct the FIFO queue position of each visible limit order over time.

Price-time priority: a created event appends to the back of its level; a size reduction (partial fill or partial cancel) keeps the order's place; a deleted — or a reduction to zero — removes it.

Parameters:

Name Type Description Default
events OrderEvents

The event stream, in canonical order. Order matters here more than anywhere else in the engine: it is the queue's priority.

required
touch_only bool

Keep only the events where the order rests at the best bid or ask at that instant — the input to the touch-queue faces. False keeps every visible level.

True

Returns:

Type Description
QueuePositions

One row per surviving order event.

queue_age_grid

queue_age_grid(
    events: OrderEvents, *, side: int, at: ndarray
) -> QueueAgeGrid

Snapshot one side's touch queue at each of the instants at.

Replays the side's events and, at every sample instant, records the age of each order resting at the best price by FIFO rank.

Parameters:

Name Type Description Default
events OrderEvents

The event stream, in canonical order.

required
side Direction

Which touch to compose.

required
at ndarray

Sample instants in int64 nanoseconds, ascending. The caller chooses the window and the spacing; the engine only replays to them.

required

Returns:

Type Description
QueueAgeGrid

The age-by-rank grid and its depth.

crossed_prefix_counts

crossed_prefix_counts(
    bid_prices: ndarray,
    bid_ts: ndarray,
    ask_prices: ndarray,
    ask_ts: ndarray,
) -> tuple[int, int]

How many best-end bids / asks to evict to uncross two book sides.

bid_prices descend from the best bid, ask_prices ascend from the best ask, each paired with its order timestamp. Walks the touch: while the top bid is priced at or above the top ask (crossed, or locked when equal), evict the older of the two touching orders — the static-snapshot analogue of :class:~ob_analytics.depth.DepthMetricsEngine trusting the fresher quote. The evicted orders are exactly the contiguous best-end prefixes, so the two returned counts describe the eviction completely.

Results

BookState dataclass

BookState(bids: BookSide, asks: BookSide)

The book at one instant: both sides, best price first.

The instant itself is not repeated here — the caller supplied it as :func:book_state's at and still holds it.

Attributes:

Name Type Description
bids BookSide

Resting bids, highest price first.

asks BookSide

Resting asks, lowest price first.

BookSide dataclass

BookSide(row: ndarray, liquidity: ndarray, bps: ndarray)

One side of a reconstructed book, best price first.

The identity of each resting order — its id, both clocks, price, and outstanding size — is not copied here: :attr:row points at the event that left the order in this state, so a caller reads any column it wants straight off its own event table. Only the two derived quantities travel.

Attributes:

Name Type Description
row ndarray

For each resting order, the index in the :class:OrderEvents arrays of its latest event at the snapshot instant (int64).

liquidity ndarray

Cumulative outstanding volume from the touch down to and including this order (float64).

bps ndarray

Distance from this side's own touch, in basis points (float64). Zero at the touch, rising away from it on both sides.

OrderLifecycles dataclass

OrderLifecycles(
    order_id: ndarray,
    created_row: ndarray,
    filled_vol: ndarray,
    end_ts: ndarray,
    outcome: ndarray,
)

One row per order, in the order the orders were placed.

As with :class:~ob_analytics.engine.BookSide, the placement columns are not copied: :attr:created_row points at the order's created event, so a caller reads the placement price, size, direction, and any label it attached (the classifier type, the placement aggressiveness) off its own event table at that row.

Orders with no created row are absent — a pre-existing opening book and hidden executions have no placement to anchor a lifecycle to.

Attributes:

Name Type Description
order_id ndarray

The order's identifier.

created_row ndarray

Index in the :class:OrderEvents arrays of the order's first created event (int64).

filled_vol ndarray

Total quantity executed over the order's life, in the units the events carried: integer lots (int64) for a canonical frame, base-asset float64 for one holding float sizes.

end_ts ndarray

Termination time in int64 nanoseconds, or :data:~ob_analytics.engine. NAT_NS while the order is still resting.

outcome ndarray

:class:Outcome codes. Flashed orders are the cancelled subset the classifier labelled flashed-limit.

QueuePositions dataclass

QueuePositions(
    row: ndarray,
    action: ndarray,
    rank: ndarray,
    queue_len: ndarray,
    ahead_volume: ndarray,
    remaining: ndarray,
    age_s: ndarray,
)

One row per order event, reporting that order's place in its level.

The event's own columns — time, order id, direction, price — are not copied: :attr:row points back at the event in the :class:OrderEvents arrays.

Attributes:

Name Type Description
row ndarray

Index in the :class:OrderEvents arrays of the event this row reports on (int64).

action ndarray

What the event did to the queue, as :class:~ob_analytics.engine.Action codes: joined the back (created), kept its place at a smaller size (changed), or left (deleted). A reduction to zero is reported as a deleted, whatever the venue called it.

rank ndarray

1-based position from the front of the level (int64). Within one order's life it is monotone non-increasing: newcomers join the back.

queue_len ndarray

Number of orders resting at the level (int64).

ahead_volume ndarray

Outstanding size of the orders ahead of this one, in the size dtype the events carried — int64 lots for a canonical stream.

remaining ndarray

This order's own outstanding size after the event, in that same dtype.

age_s ndarray

Seconds since the order was placed (float64).

QueueAgeGrid dataclass

QueueAgeGrid(ages: ndarray, max_rank: int)

Touch-queue composition over time: the age of the order at each rank.

Attributes:

Name Type Description
ages ndarray

A (max_rank, n_samples) float array: ages[r, t] is the age in seconds of the order at rank r + 1 (front = row 0) at sample t, or NaN where the queue is shorter than r + 1.

max_rank int

The deepest the touch queue got over the sampled window — the number of rows in :attr:ages.