Protocol Interfaces¶
Contracts that pluggable pipeline components must satisfy. Uses structural (duck) typing — implement the right method signature and it works, no inheritance required.
| Protocol | Method | Purpose |
|---|---|---|
EventLoader |
load(source) → DataFrame |
Parse raw L3 data into events |
DepthSource |
load(source) → DataFrame |
Load an L2 price-level feed into the depth frame |
TradeSource |
load(events, source) → DataFrame |
Build the canonical trades DataFrame |
DataWriter |
write(data, dest) |
Serialize pipeline outputs |
Source |
level · feed_type · trade_attribution · settings |
The shape shared by every data source, file or live |
OfflineSource |
create_loader · create_trade_source |
A Source that replays stored files |
SupportsWriter |
create_writer |
Optional: write a run in the source's own format |
SupportsComputeDepth |
compute_depth |
Optional: supply the depth the venue ships |
SupportsRequiredContext |
required_context |
Optional: name the RunContext fields a run needs |
LiveSource |
snapshot · stream · shutdown_synthetic_events |
A Source that captures a live venue feed |
Metric |
compute(result) · prepare(frame) |
A measurement taken from a finished run, drawn as a level-less plot |
A Source declares two coordinates: a FeedType (matched_book, diff_feed
or price_levels; see Data quality) and a Level (L2 vs
L3; see Process L2 feeds). It also declares a
TradeAttribution: which orders of a trade its order events can name (see
Check data quality). A source that records the venue's
sequence number may also declare a SequenceKind: whether a skipped number
means a lost message. A source whose data has one clock declares Clocks, so
the clock checks know there is nothing to compare. It carries typed settings and registers in the source
registry (see Sources). It may state the tick and lot size its
data is usually on as grid, which a run's config starts from.
A run carries these declarations on its result as FeedDeclarations, and
result.audit() holds the run to them.
A source keeps nothing from a run. The pipeline builds each run's loader and
trade source from that run's config and RunContext, so one source can serve
many runs and many pipelines.
Level ¶
Bases: str, Enum
Order-book resolution a feed (or a plot) works at — MBP vs MBO.
The granularity axis, orthogonal to :class:FeedType's crossing
invariant. A source declares its :attr:Source.level so the
pipeline knows which stages apply:
- :attr:
L2— Market-By-Price (MBP): aggregate volume per price level, with no persistent order identity. Price-level feeds (Binance, Kalshi, Polymarket, most CCXT sources) are L2. The per-order stages (:func:~ob_analytics.analytics.set_order_types, :func:~ob_analytics.analytics.order_aggressiveness, queue reconstruction) have nothing to key on and are skipped; depth / spread / trade analytics run directly on the price-level book. - :attr:
L3— Market-By-Order (MBO): one primitive per resting order, with stable identity (queue position recoverable). The reconstruction model ob-analytics was built for (Bitstamp, LOBSTER, Databento).
The str mixin lets members slot directly into the visualization
renderer-registry tuple keys and, via the :meth:__str__ override, render
as the bare token ("L2") in file stems and f-strings rather than
"Level.L2".
FeedType ¶
Bases: str, Enum
How a data feed represents the order book — its crossing invariant.
A source declares its feed type so downstream code can reason about crossed books by coordinate, not by source name. The distinction is a property of the source, not of the reconstruction:
- :attr:
MATCHED_BOOK— an L3 feed emitted by the venue's own matching engine (LOBSTER, exchange MBO such as Databento). Bids can never rest above asks, so an uncrossed book is a guaranteed invariant of the data. - :attr:
DIFF_FEED— an L3 feed reconstructed from a public placement/cancellation diff stream (the Bitstamp public feed). It can contain genuinely crossed resting orders (a bid resting above an ask, neither filling); :func:~ob_analytics.analytics.order_bookreplays this faithfully — a crossed book in the output is a property of the feed, not a reconstruction bug. - :attr:
PRICE_LEVELS— an L2 feed: the venue's total size at each price, as a stream of changes, merged changes, repeated snapshots or polls. The venue does not publish a crossed book, so a crossed one means the capture's copy is wrong: most often, the capture kept a level the venue removed. - :attr:
UNKNOWN— a source that does not declare its feed type (the structural default for third-party sources predating this attribute).
Mixes in str so members compare and serialise as their value
(FeedType.DIFF_FEED == "diff_feed"), which keeps CLI/JSON output and
equality checks ergonomic.
TradeAttribution ¶
Bases: str, Enum
Which orders of a trade a feed's order events can name.
Every trade has a maker, the order that was resting in the book, and a taker, the order that arrived and traded against it. Whether the order events show both depends on the feed, not on the reconstruction:
- :attr:
BOTH— the feed reports every order the venue accepts, takers included, so both orders of a trade can be found. Bitstamp'slive_orderschannel does this. - :attr:
MAKER_ONLY— the feed shows resting orders only. A taker trades on arrival and never rests, so it never appears. Exchange order-by-order feeds (Nasdaq ITCH, from which LOBSTER is built, and Databento MBO) do not identify the aggressor, and a feed made of book snapshots cannot show it. - :attr:
NONE— the feed has no order identity (L2), so neither order can be named.
A source declares it so the unmatched-trades check in
:func:~ob_analytics.analytics.data_quality_summary counts only the orders
the feed can show. A source that does not declare it is read as
:attr:BOTH, the check's behaviour before this declaration existed.
Mixes in str so members compare and serialise as their value, as
:class:FeedType does.
trade_attribution_of ¶
Return what source declares as its :class:TradeAttribution.
A source that does not declare one is read as :attr:TradeAttribution.BOTH.
SequenceKind ¶
Bases: str, Enum
What a feed's venue sequence number promises.
A source declares it so a gap check knows what a skipped number means:
- :attr:
CONTIGUOUS— every message adds exactly one. A skipped number is a dropped message. This is the default. - :attr:
MONOTONIC— the number only rises. A skip is normal and says nothing about loss; only a step that does not rise is a fault. CCXT's booknonceis this kind: on Binance it is the last update ID of a diff that covers a range of IDs, andwatch_order_bookcan apply several diffs before it returns a book. Databento'ssequenceis too: it numbers every message on the venue's channel, but a file usually holds one instrument of that channel, and the trade and fill records it carries become trades rather than book events. So is cryptofeed's: on Bitfinex and Blockchain.com it numbers every message on the connection, trades and heartbeats too, and on Independent Reserve cryptofeed passes on no message about an order it does not hold.
Mixes in str so members compare and serialise as their value, as
:class:FeedType does.
sequence_kind_of ¶
Return what source declares as its :class:SequenceKind.
A source that does not declare one is read as
:attr:SequenceKind.CONTIGUOUS.
Clocks ¶
Bases: str, Enum
Which clocks a feed's book rows carry.
The schema has two clock columns: exchange_timestamp, the time the
venue stamped on the message, and timestamp, the time the capture
received it. Not every feed has both. Where one is missing, the other is
copied into its column, so the two columns are equal and comparing them
says nothing:
- :attr:
BOTH— the venue stamps each message and the capture stamps its receipt. The two clocks can be checked against each other. This is the default. - :attr:
RECEIVE_ONLY— the venue sends no time with its book, soexchange_timestampcopiestimestamp. The cryptofeed Bitfinex, Blockchain.com and Kraken books are like this. - :attr:
VENUE_ONLY— the data holds the venue's time only, sotimestampcopiesexchange_timestamp. LOBSTER files are like this.
A source declares it so :func:~ob_analytics.analytics.data_quality_summary
runs its clock checks only when there are two clocks to compare, and says
why when it does not. A live capture finds out from the venue's messages
and records it in meta.json.
Mixes in str so members compare and serialise as their value, as
:class:FeedType does.
clocks_of ¶
Return what source declares as its :class:Clocks.
A source that does not declare one is read as :attr:Clocks.BOTH.
FeedDeclarations
dataclass
¶
FeedDeclarations(
feed_type: FeedType = FeedType.UNKNOWN,
trade_attribution: TradeAttribution = TradeAttribution.BOTH,
sequence_kind: SequenceKind = SequenceKind.CONTIGUOUS,
clocks: Clocks = Clocks.BOTH,
sequence_restarts: int = 0,
)
What a run's data is declared to show, which audit holds it to.
A run gets these from its source (:func:declarations_of), and a live
capture's meta.json replaces any of them it recorded (see
:func:~ob_analytics.capture_record.run_declarations). A
:class:~ob_analytics.pipeline.PipelineResult carries them, and a saved
result keeps them, so
:meth:~ob_analytics.pipeline.PipelineResult.audit reads them from the
result. The defaults are what an undeclared source is read as.
Attributes:
| Name | Type | Description |
|---|---|---|
feed_type |
FeedType
|
The crossing invariant (:class: |
trade_attribution |
TradeAttribution
|
Which orders of a trade the feed names (:class: |
sequence_kind |
SequenceKind
|
What the venue |
clocks |
Clocks
|
Which clocks the book rows carry (:class: |
sequence_restarts |
int
|
How many times a capture's venue sequence started again at a new
opening book. This is not a declaration but a count the capture
records; |
from_dict
classmethod
¶
Read declarations written by :meth:to_dict.
A field that is missing keeps its default.
Raises:
| Type | Description |
|---|---|
ValueError
|
If a field holds a value this version does not know. |
declarations_of ¶
Return the :class:FeedDeclarations source makes.
A declaration the source does not make keeps the default, as
:func:trade_attribution_of, :func:sequence_kind_of and
:func:clocks_of read it; a source that declares no feed_type is
:attr:FeedType.UNKNOWN. None declares nothing.
grid_of ¶
Return the :class:~ob_analytics.config.InstrumentGrid source declares.
A source that does not declare one is read as the default grid: a cent
tick and a satoshi lot, the :class:~ob_analytics.config.PipelineConfig
defaults.
Raises:
| Type | Description |
|---|---|
TypeError
|
If source declares a |
EventLoader ¶
Bases: Protocol
Loads raw order-book events from a data source.
The returned DataFrame must contain at least the columns required by
ob_analytics.schemas.validate_events_df.
load ¶
Load events from source and return a DataFrame.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
Any
|
Data source identifier. The canonical type is |
required |
Returns:
| Type | Description |
|---|---|
DataFrame
|
Events with at least the columns required by
|
DepthSource ¶
Bases: Protocol
Loads a price-level (L2) depth stream into the canonical depth frame.
The L2 counterpart to :class:EventLoader. Where an EventLoader
returns per-order events that the pipeline later folds into depth via
:func:~ob_analytics.depth.price_level_volume, a DepthSource returns
the depth frame directly — a price-level feed is a depth stream, so
there is nothing to reconstruct.
An :attr:Level.L2 source's :meth:OfflineSource.create_loader returns a
DepthSource; the pipeline validates its output with
:func:~ob_analytics.schemas.validate_depth_df and feeds it straight to
:class:~ob_analytics.depth.DepthMetricsEngine.
Returned DataFrame columns (see
:data:~ob_analytics.schemas.DEPTH_COLUMNS):
timestamp— pandas datetime64[ns, UTC]price— int64, the price level in integer ticks (×tick_sizefor the quote currency)volume— int64, the level's new absolute resting size in integer lots (×lot_sizefor the base asset) after the update (0removes the level); not a signed deltadirection— categoricalbid/ask
load ¶
Load a price-level depth stream from source and return the frame.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
Any
|
Data source identifier — canonically a |
required |
Returns:
| Type | Description |
|---|---|
DataFrame
|
Depth with at least the columns required by
:func: |
TradeSource ¶
Bases: Protocol
Builds the trades DataFrame for a given run.
Implementations read explicit trade records (a separate
trades.csv, LOBSTER execution rows embedded in the events
frame, etc.) and project them into the canonical trades schema.
Returned DataFrame columns:
timestamp— pandas datetime64[ns, UTC]price— int64 (integer ticks; ×tick_sizefor the quote currency)volume— int64 (integer lots; ×lot_sizefor the base asset)direction— categoricalbuy/sell(taker side)maker_event_id— integer event id of the resting ordertaker_event_id— integer event id of the aggressing ordermaker— order id of the resting ordertaker— order id of the aggressing ordermaker_og— original_number of the maker eventtaker_og— original_number of the taker event
load ¶
Build the trades DataFrame.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
events
|
DataFrame
|
The processed events frame (post-loader). |
required |
source
|
Any
|
The same |
required |
Returns:
| Type | Description |
|---|---|
DataFrame
|
|
DataWriter ¶
Bases: Protocol
Writes pipeline results to a format-specific output.
Attributes:
| Name | Type | Description |
|---|---|---|
target |
WriteTarget
|
Whether :meth: |
target
property
¶
Whether this writer takes a folder or a file (:class:WriteTarget).
write ¶
Write pipeline DataFrames to dest.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
dict of str to DataFrame
|
Pipeline output keyed by name (e.g. |
required |
dest
|
str or Path
|
The folder or file to write, as :attr: |
required |
WriteTarget ¶
Bases: str, Enum
What a :class:DataWriter writes to: a folder or one file.
Each writer declares it as its :attr:DataWriter.target, and
:func:~ob_analytics.data.save_data checks the path it is given against
it:
- :attr:
FOLDER— this package's own formats (Parquet, depth CSV, Bitstamp, LOBSTER), which write several files into one folder. - :attr:
FILE— exports for another tool (hftbacktest, Nautilus, Databento DBN), which write one file.
Mixes in str so members compare as their value
(WriteTarget.FOLDER == "folder").
Source ¶
Bases: Protocol
Structural contract shared by every data source, file or live.
One shape covers both a file loader and a live capturer: a source states
the two coordinates downstream code reasons by — :attr:level (L2 vs L3)
and :attr:feed_type (the crossing invariant) — and carries its typed,
validated :attr:settings (a :class:~ob_analytics.config.SourceSettings)
in place of an untyped dict. There is no base class to inherit: any
object providing these members satisfies the contract (structural typing).
A source declares how it produces the shared schema by also satisfying a
capability protocol — :class:OfflineSource (replay stored files) and/or
:class:LiveSource (capture a live venue). A venue that supports both
(e.g. Bitstamp) satisfies both.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
Short lowercase identifier registered in
:data: |
level |
Level
|
Order-book resolution this source produces: :attr: |
feed_type |
FeedType
|
The source's crossing invariant (:class: |
trade_attribution |
TradeAttribution
|
Which orders of a trade the source's order events can name
(:class: |
sequence_kind |
SequenceKind
|
What the venue |
clocks |
Clocks
|
Which clocks the source's book rows carry (:class: |
settings |
SourceSettings
|
Typed per-source configuration. The empty base for a source that needs
none; a typed subclass (e.g. |
grid |
InstrumentGrid
|
The tick and lot size the source's data is usually on
(:class: |
A source holds no state from one run |
the pipeline builds each run's
|
|
loader and trade source from that run's config and context, so one source |
|
|
can serve many runs. |
|
OfflineSource ¶
Bases: Source, Protocol
A :class:Source that replays stored files into the shared schema.
Requires only the two factories every replay needs: how to load events
(or depth) and how to acquire trades. Pass instances to
Pipeline(source=...).
A source MAY also implement the optional capabilities
:class:SupportsWriter (write a run back in the source's own format),
:class:SupportsComputeDepth (supply its own depth) and
:class:SupportsRequiredContext (name the :class:RunContext fields it
needs). Each is a separate protocol, so an offline source never has to
define a method only to say it does not use it; callers detect them with
isinstance, as the live runner detects SupportsDiagnostics.
create_loader ¶
Return the loader for this source.
An :attr:Level.L3 source returns an :class:EventLoader (per-order
events); an :attr:Level.L2 source returns a :class:DepthSource
(the price-level depth frame directly).
create_trade_source ¶
Return a trade source for this source.
loader is the run's loader, the one that reads the events. A source whose trades come off the same read (Databento) takes them from it; most sources ignore it.
SupportsWriter ¶
Bases: Protocol
Optional offline capability: write a run in the source's own format.
:func:~ob_analytics.data.save_data with fmt=<source name> uses it,
so a venue writer lives on its source rather than in a second registry.
create_writer ¶
Return the writer for this source.
SupportsComputeDepth ¶
Bases: Protocol
Optional offline capability: supply the depth tables for a run.
The pipeline calls :meth:compute_depth in place of its own depth stage
when the source implements it. LOBSTER reads the venue's orderbook file
this way.
compute_depth ¶
compute_depth(
events: DataFrame,
config: Any,
source: Any,
ctx: RunContext,
loader: Any,
) -> tuple[pd.DataFrame, pd.DataFrame] | None
Return (depth, depth_summary), or None to use the standard stage.
loader is the run's loader, after it read events. LOBSTER takes
the orderbook file it found from it, and returns None when there
is none.
SupportsRequiredContext ¶
Bases: Protocol
Optional offline capability: name the :class:RunContext fields a run needs.
LOBSTER needs trading_date because its file names carry no date. The
CLI checks these before a run starts. Read it with
:func:required_context_of, which gives [] for a source without it.
required_context ¶
Return the :class:RunContext field names this source requires.
required_context_of ¶
Return the :class:RunContext fields source requires.
[] for a source that does not implement
:class:SupportsRequiredContext.
RunContext
dataclass
¶
RunContext(
trading_date: object | None = None,
session_tz: str | None = None,
symbol: str | None = None,
venue: str | None = None,
)
Per-run parameters that don't belong on the Source constructor.
Give one to Pipeline(ctx=...) as the default for every run, or to
Pipeline.run(source, ctx=...) for that run alone. The pipeline builds
each run's loader and trade source from that run's context
(OfflineSource.create_loader / create_trade_source), so one source
serves many runs, each with its own date, symbol and venue.
Attributes:
| Name | Type | Description |
|---|---|---|
trading_date |
(str or Timestamp, optional)
|
Calendar date anchor (LOBSTER needs this; venues with continuous trading do not). |
session_tz |
(str, optional)
|
The venue's local time zone for a session-relative feed (LOBSTER),
used to place its seconds-after-midnight on the shared UTC clock.
|
symbol |
(str, optional)
|
The instrument this run covers (e.g. |
venue |
(str, optional)
|
The source venue this run covers (e.g. |
LiveSource ¶
Bases: Source, Protocol
The live-capture capability of a :class:~ob_analytics.protocols.Source.
A live source is an async iterator of order/trade (or depth) events, plus
two bookend methods for snapshot + shutdown synthesis. It inherits the
source coordinates from :class:~ob_analytics.protocols.Source — name,
level, feed_type, and typed settings — so a venue that both
replays files and captures live agrees with itself on those.
:attr:~ob_analytics.protocols.Source.level routes the book events the
runner writes: :attr:~ob_analytics.protocols.Level.L3 → per-order events
to orders.csv; :attr:~ob_analytics.protocols.Level.L2 → price-level
depth updates to depth.csv.
Implementors only worry about parsing. Persistence, raw-frame archival,
and signal handling live in ob_analytics.live._runner.
A source does not reconnect on its own. When it loses its connection it
raises from :meth:stream: a book carried across a disconnect misses
every change made while it was down. :func:~ob_analytics.live.run_capture
then closes the segment, waits, and starts a new one from a fresh snapshot,
recording the time between them as a gap in manifest.json.
A live source MAY additionally implement :class:SupportsDiagnostics to
surface per-run counters in meta.json; that hook is a separate,
optional protocol so it is never required to conform to this one.
snapshot ¶
Yield the opening book: one event per resting order (L3) or level (L2).
Called once at startup before :meth:stream. L3: each event MUST
have action="created"; the runner writes them to orders.csv so
later changed / deleted events have matching creates. L2:
each event is an absolute-size depth row (side / price /
volume) written to depth.csv.
stream ¶
Yield (kind, event, raw_frame) for every live event.
kind is "order" (L3), "depth" (L2), or "trade".
raw_frame is the original JSON-decoded WebSocket payload (or
None); the runner writes it to raw.jsonl iff config.keep_raw.
Implementations should self-terminate after config.minutes of
wall-clock time. The runner also enforces this externally, so
cancellation must be cooperative.
shutdown_synthetic_events ¶
Yield synthetic close-out events for everything still on the book.
Called once at shutdown. L3: each event MUST have
action="deleted", giving every id in orders.csv a complete
created -> ... -> deleted lifecycle. L2: price levels have no
lifecycle to close, so an L2 capturer typically yields nothing here.
UnmappedMessageError ¶
Bases: ObAnalyticsError, ValueError
An L3 message a live source could not turn into order events.
An L3 source raises it from :meth:LiveSource.snapshot or
:meth:LiveSource.stream instead of leaving the message out: without
it the capture's book would no longer match the venue's. The runner
records it as :attr:CaptureResult.capture_error, and
:func:~ob_analytics.live.run_capture starts the next segment from a
new opening book. A message that does not change the book (a trade on
a feed whose order events carry the fills) can be left out instead, and
counted under dropped (see :class:SupportsDiagnostics).
Metric ¶
Bases: Protocol
Structural contract for a measurement taken from a finished run.
A metric reads a run's tables and returns one table of its own, then says
how to turn that table into a renderer payload. There is no base class
to inherit: any object providing these members satisfies the contract
(structural typing), and registering it in
:data:~ob_analytics.metrics.METRICS is what makes it run and plot.
:attr:name is both the registry key and the level-less plot concept the
metric draws under, so a renderer registered at (name, None, backend)
is the metric's face.
Both methods may take keyword-only settings after their first argument,
each with a default: :meth:compute the settings of the measurement (a
window, a bucket size) and :meth:prepare the settings of the picture (a
threshold line, a time window).
:func:~ob_analytics.visualization.plot_result sends each keyword it is
given to the method that names it, so the two must not share a name.
A size setting is in the instrument's own units (BTC, shares, contracts)
and a price setting in the quote currency, and so is the table
:meth:compute returns. :meth:compute is handed the pipeline result as
stored, its tables holding integer lots and ticks, by
result.metric(...), result.plot(...) and the gallery alike. Pass
the tables to functions that read the units off them (such as
:func:~ob_analytics.flow_toxicity.compute_vpin), or convert with the
scale that frame.attrs["lot_size"] and frame.attrs["tick_size"]
give.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
Short lowercase identifier registered in
:data: |
title |
str
|
Human-readable title for the metric's gallery card. |
levels |
tuple of Level
|
The resolutions the metric applies to. A metric that reads per-order
events declares |
needs |
tuple of str
|
The run's tables the metric cannot be computed without, by their
:class: |