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 ( |
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 ( |
volume |
ndarray
|
The order's outstanding size after the event ( |
direction |
ndarray
|
:class: |
action |
ndarray
|
:class: |
fill |
ndarray or None
|
Quantity executed at this event ( |
is_market |
ndarray or None
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If the columns are not all one-dimensional and the same length. |
visible ¶
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 ¶
Return :attr:fill, or raise when the caller omitted it.
market_mask ¶
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 ¶
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 |
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 |
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
|
Returns:
| Type | Description |
|---|---|
OrderLifecycles
|
One row per submitted order, ordered by first placement. |
queue_positions ¶
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. |
True
|
Returns:
| Type | Description |
|---|---|
QueuePositions
|
One row per surviving order event. |
queue_age_grid ¶
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
¶
BookSide
dataclass
¶
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: |
liquidity |
ndarray
|
Cumulative outstanding volume from the touch down to and including this
order ( |
bps |
ndarray
|
Distance from this side's own touch, in basis points ( |
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: |
filled_vol |
ndarray
|
Total quantity executed over the order's life, in the units the events
carried: integer lots ( |
end_ts |
ndarray
|
Termination time in int64 nanoseconds, or :data: |
outcome |
ndarray
|
:class: |
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: |
action |
ndarray
|
What the event did to the queue, as
:class: |
rank |
ndarray
|
1-based position from the front of the level ( |
queue_len |
ndarray
|
Number of orders resting at the level ( |
ahead_volume |
ndarray
|
Outstanding size of the orders ahead of this one, in the size dtype the
events carried — |
remaining |
ndarray
|
This order's own outstanding size after the event, in that same dtype. |
age_s |
ndarray
|
Seconds since the order was placed ( |
QueueAgeGrid
dataclass
¶
Touch-queue composition over time: the age of the order at each rank.
Attributes:
| Name | Type | Description |
|---|---|---|
ages |
ndarray
|
A |
max_rank |
int
|
The deepest the touch queue got over the sampled window — the number of
rows in :attr: |