Skip to content

Analytics

Format-agnostic post-processing analytics. These functions work with the output of any format's pipeline run (Bitstamp, LOBSTER, or custom).

Trade Analysis

order_aggressiveness

order_aggressiveness(
    events: DataFrame, depth_summary: DataFrame
) -> pd.DataFrame

Calculate order aggressiveness with respect to the best bid or ask in BPS.

Parameters:

Name Type Description Default
events DataFrame

The events DataFrame (must contain direction, action, type, timestamp, event_id, price columns).

required
depth_summary DataFrame

The order book summary statistics DataFrame (must contain timestamp and event_id columns).

required

Returns:

Type Description
DataFrame

The events DataFrame with an added aggressiveness_bps column.

trade_impacts

trade_impacts(trades: DataFrame) -> pd.DataFrame

Generate a DataFrame containing order book impact summaries.

Aggregates trade records by taker order ID to summarise how each aggressive order swept through the book (price range, number of fills, total volume, VWAP, duration).

Parameters:

Name Type Description Default
trades DataFrame

The trades DataFrame (must contain taker, price, volume, timestamp, direction columns).

required

Returns:

Type Description
DataFrame

A DataFrame summarising market order impacts with columns: id, min_price, max_price, vwap, hits, vol, start_time, end_time, dir. The price-valued columns (min_price, max_price, vwap) are in the same integer-tick units as trades["price"]; multiply by tick_size for the quote currency.

Order Type Classification

set_order_types

set_order_types(
    events: DataFrame, trades: DataFrame
) -> pd.DataFrame

Determine limit order types.

Classifies each order as one of: market, resting-limit, flashed-limit, or market-limit, based on how the order interacts with the book over its lifetime.

Parameters:

Name Type Description Default
events DataFrame

The limit order events DataFrame.

required
trades DataFrame

The executions DataFrame.

required

Returns:

Type Description
DataFrame

The events DataFrame with an updated 'type' column indicating order types.

Order Book Reconstruction

The reconstructions themselves live in the order-book engine; these are their frame-level faces.

order_book

order_book(
    events: DataFrame,
    tp: datetime | None = None,
    max_levels: int | None = None,
    bps_range: int = 0,
    min_bid: float = 0,
    max_ask: float = np.inf,
    uncross: bool = False,
) -> dict[str, datetime | pd.Timestamp | pd.DataFrame]

Reconstruct the order book at a specific point in time.

The reconstruction itself is :func:ob_analytics.engine.book_state; this function is its frame adapter, and owns the display window (max_levels, bps_range) the engine has no opinion about.

Parameters:

Name Type Description Default
events DataFrame

DataFrame containing order events.

required
tp datetime or Timestamp

The point in time at which to evaluate the order book. If None, uses the latest event timestamp in the data.

None
max_levels int

The maximum number of price levels to include for bids and asks.

None
bps_range int

Basis points range to filter the bids and asks. Default is 0.

0
min_bid float

Minimum bid price. Default is 0.

0
max_ask float

Maximum ask price. Default is infinity.

inf
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 (see :class:~ob_analytics.protocols.FeedType) are replayed as-is rather than silently uncrossed. Has no effect on a matched-book feed, which is never crossed.

False

Returns:

Type Description
dict[str, datetime or DataFrame]

A dictionary containing: - 'timestamp': The evaluation timestamp. - 'asks': DataFrame of active ask orders. - 'bids': DataFrame of active bid orders.

order_lifecycles

order_lifecycles(events: DataFrame) -> pd.DataFrame

Collapse events into one row per order: placement → outcome.

The canonical lifecycle table (one derivation, shared by the L3 faces and the order-book reconstruction). A thin frame wrapper over :func:ob_analytics.engine.order_lifecycles, which relies on the schemas.py volume contract: volume is the outstanding size after each event and fill the executed delta, so an order is terminated when a deleted row arrives or its outstanding size reaches zero — the latter is how fully-executed LOBSTER orders end, which never emit a deleted event.

Parameters:

Name Type Description Default
events DataFrame

Events satisfying the schemas.py contract. Orders without a created row (pre-existing book, hidden executions) are excluded — their placement is unknown.

required

Returns:

Type Description
DataFrame

One row per order id:

  • id, direction, price — placement identity.
  • type — classifier label (when the column is present).
  • placed_ts, placed_vol — from the created row.
  • filled_vol — total executed quantity (Σ fill).
  • end_ts — termination time (NaT while still resting; callers clip to their window end for display).
  • outcomefilled / partial / cancelled / resting. Flashed orders are the cancelled subset whose type is flashed-limit.
  • aggressiveness_bps — placement distance (when present).

uncross_book_sides

uncross_book_sides(
    bids: DataFrame, asks: DataFrame
) -> tuple[pd.DataFrame, pd.DataFrame]

Evict crossed levels from two reconstructed book sides for display.

The frame-level counterpart of order_book(..., uncross=True) for callers that already hold per-order book sides — e.g. the book_snapshot / depth_chart visualization prepares. Both frames are returned best-first (bids by descending price, asks by ascending price) with the crossed best-end orders removed so best_bid < best_ask; liquidity is recomputed when present and every other column is preserved.

Parameters:

Name Type Description Default
bids DataFrame

Per-order book sides carrying at least price and timestamp (as returned in :func:order_book's "bids" / "asks" frames).

required
asks DataFrame

Per-order book sides carrying at least price and timestamp (as returned in :func:order_book's "bids" / "asks" frames).

required

Returns:

Type Description
tuple of (pandas.DataFrame, pandas.DataFrame)

The uncrossed (bids, asks) sides, best-first.

Data Quality

See Data quality: matched book vs diff feed for the concepts and the audit how-to for the CLI.

data_quality_summary

data_quality_summary(
    events: DataFrame,
    trades: DataFrame,
    *,
    feed_type: FeedType = FeedType.UNKNOWN,
    depth: DataFrame | None = None,
) -> DataQualitySummary

Summarise the data quality of one reconstructed session.

Surfaces the health signals that matter before trusting a feed — most importantly how crossed the resting book is, which distinguishes a matched book from a diff feed (see :class:~ob_analytics.protocols.FeedType).

Parameters:

Name Type Description Default
events DataFrame

Classified events (must carry the canonical columns and the type column from :func:set_order_types). For a price-level (L2) run this is the empty, schema-valid frame from :func:~ob_analytics._utils.empty_events: the per-order metrics then report zero and crossing is read from depth (pass PipelineResult.depth).

required
trades DataFrame

The trades frame, with maker_event_id / taker_event_id.

required
feed_type FeedType

The source's declared feed type, recorded on the summary and used to interpret crossed_pct. Read it off the format: getattr(fmt, "feed_type", FeedType.UNKNOWN).

UNKNOWN
depth DataFrame

A faithful price-level-volume frame (e.g. PipelineResult.depth). When None it is computed from events via :func:~ob_analytics.depth.price_level_volume. Do not pass depth_summary — that is already uncrossed and would report ~0%.

None

Returns:

Type Description
DataQualitySummary

DataQualitySummary dataclass

DataQualitySummary(
    feed_type: FeedType,
    n_events: int,
    n_orders: int,
    n_trades: int,
    crossed_pct: float,
    crossed_episodes: int,
    unmatched_trades_pct: float,
    duplicate_event_ids: int,
    duplicate_created_ids: int,
    pre_existing_orders: int,
    events_with_sequence: int = 0,
    sequence_gaps: int = 0,
    sequence_out_of_order: int = 0,
    orphan_orders: int = 0,
    orphan_events: int = 0,
    nonpositive_price_rows: int = 0,
    negative_volume_rows: int = 0,
    exchange_time_after_receive: int = 0,
    exchange_time_reordered: int = 0,
)

Per-run data-quality metrics for a reconstructed session.

Built by :func:data_quality_summary. All percentages are 0–100 floats.

The fields are the measurements; :attr:checks turns them into pass/fail verdicts with a :class:Severity each, and :attr:ok is the one-line answer to "is this feed trustworthy?" that ob-analytics audit exits on.

Attributes:

Name Type Description
feed_type FeedType

The source's declared crossing invariant (see :class:~ob_analytics.protocols.FeedType); sets expectations for crossed_pct.

n_events, n_orders, n_trades int

Row / distinct-order / trade counts.

crossed_pct float

Percentage of session time the faithful book is crossed (best_bid > best_ask). Expected ~0 for a matched book; a genuine, faithfully-replayed property of a diff feed.

crossed_episodes int

Number of distinct crossed intervals.

unmatched_trades_pct float

Percentage of trades missing a resolved maker_event_id or taker_event_id (could not be tied to a resting order).

duplicate_event_ids int

Count of event_id values occurring more than once (event_id should be globally unique — any non-zero value is suspect).

duplicate_created_ids int

Count of order ids with more than one created event.

pre_existing_orders int

Distinct orders resting before the capture window (classifier label pre-existing — structurally unclassifiable, not failures).

events_with_sequence int

Rows carrying a non-null venue sequence (see :func:detect_sequence_gaps). 0 when the source has no sequence or track_sequence was off at load — the sequence metrics below are then trivially zero.

sequence_gaps int

Dropped-message count: skipped venue sequence numbers.

sequence_out_of_order int

Reordered or duplicated messages: sequence steps that did not advance.

orphan_orders int

Distinct order ids with a changed or deleted event but no created one. Every order resting before the capture began is an orphan, so a capture that starts mid-stream reports a small, stable count; this is also the signal a live capture's stream drifting from its opening snapshot shows up as.

orphan_events int

Rows belonging to those orphan orders.

nonpositive_price_rows int

Rows priced at or below zero. Legal in the schema (prices are signed integer ticks) but not a tradeable level.

negative_volume_rows int

Rows with a negative volume (or negative fill): impossible size.

exchange_time_after_receive int

Rows whose venue clock (exchange_timestamp) is later than the local receive clock (timestamp) — an event received before it happened.

exchange_time_reordered int

Steps where the venue clock goes backwards while the receive clock moves forward: messages that reached the capture out of order.

checks property

checks: tuple[QualityCheck, ...]

Every check this run was scored against, errors first.

The crossing check reads its severity off :attr:feed_type: a crossed resting book is a defect in a matched book and a faithful property of a diff feed, so the same number means opposite things and only the declared feed type can tell them apart.

errors property

errors: tuple[QualityCheck, ...]

Failed checks whose severity is :attr:Severity.ERROR.

warnings property

warnings: tuple[QualityCheck, ...]

Failed checks whose severity is :attr:Severity.WARNING.

ok property

ok: bool

True when no error-severity check failed (warnings may still stand).

to_dict

to_dict() -> dict[str, Any]

Return the summary as a plain, JSON-serialisable dict.

render

render() -> str

Return a fixed-width, human-readable report block.

QualityCheck dataclass

QualityCheck(
    name: str, passed: bool, severity: Severity, detail: str
)

One named data-quality check and how it read on this run.

Attributes:

Name Type Description
name str

Stable identifier, matching the summary field it reads (e.g. "duplicate_event_ids").

passed bool

Whether the data satisfied the check.

severity Severity

What a failure means (see :class:Severity).

detail str

One line saying what was found and how to read it.

to_dict

to_dict() -> dict[str, Any]

Return the check as a plain, JSON-serialisable dict.

Severity

Bases: str, Enum

How much a failed data-quality check matters.

A check carries its severity so the policy — what fails a run — lives with the measurement rather than in each caller. The enum mixes in str (Severity.ERROR == "error"), which keeps CLI and JSON output plain.

Attributes:

Name Type Description
ERROR

The data contradicts something that must hold (a duplicate event_id, a dropped venue message, a negative volume). Any failing error check fails the run.

WARNING

A signal worth reading before trusting the feed, but one a sound capture can legitimately show (orders resting before the capture began, zero-priced levels, messages reordered in transit). Fails the run only under ob-analytics audit --strict.

INFO

Reported for context; never fails a run.

detect_sequence_gaps

detect_sequence_gaps(
    frame: DataFrame,
    *,
    sequence_col: str = SEQUENCE_COLUMN,
    order_col: str = INGEST_SEQ_COLUMN,
    group_cols: Sequence[str] = _SEQUENCE_GROUP_COLUMNS,
) -> SequenceGapReport

Report missing or out-of-order venue sequence numbers in frame.

Parameters:

Name Type Description Default
frame DataFrame

Events or depth rows. A missing sequence_col (a source with no venue sequence) yields an empty, clean report — the column is optional.

required
sequence_col str

Column holding the venue's per-event sequence (nullable integer).

SEQUENCE_COLUMN
order_col str

Column defining ingest order; when absent, the frame's current row order is used instead.

INGEST_SEQ_COLUMN
group_cols sequence of str

Columns that identify one channel (instrument / venue). Only those present in frame are used; with none present the whole frame is one channel. Sequences from different channels are not comparable, so each group is scored on its own.

_SEQUENCE_GROUP_COLUMNS

Returns:

Type Description
SequenceGapReport
Notes

Consecutive equal sequence values are collapsed before comparison, so a single venue update that emits several rows (e.g. every changed level of one book diff) counts once. A non-consecutive repeat still shows as out-of-order.

SequenceGapReport dataclass

SequenceGapReport(
    n_sequenced: int,
    n_updates: int,
    n_missing: int,
    n_out_of_order: int,
    max_gap: int,
    first_break_seq: int | None,
)

Result of scanning a frame's venue sequence column for breaks.

Built by :func:detect_sequence_gaps. A feed numbers each message on a channel with a sequence that should rise by exactly one; a skip is a dropped message and a step that does not rise is a reordered (or repeated) one. Rows are read in ingest order (ingest_seq when present, else the frame's row order) and grouped per instrument / venue when those columns exist, so interleaved channels are scored independently.

Attributes:

Name Type Description
n_sequenced int

Rows carrying a non-null venue sequence. 0 means the frame has nothing to check (the report is then trivially clean).

n_updates int

Distinct consecutive sequence values seen — one per venue message (a book update that emits several rows shares one sequence, counted once).

n_missing int

Total count of skipped sequence numbers: the dropped-message count.

n_out_of_order int

Steps where the sequence did not advance (a repeat or a decrease) — a reordered or duplicated message.

max_gap int

Largest single run of consecutive missing numbers (0 when none).

first_break_seq int or None

The last in-order sequence value before the first break, a hint for where to resync; None when the frame is clean.

has_sequence property

has_sequence: bool

Whether any row carried a venue sequence to check.

clean property

clean: bool

True when no missing and no out-of-order sequence values were found.

to_dict

to_dict() -> dict[str, Any]

Return the report as a plain, JSON-serialisable dict.