Skip to content

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_book replays 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's live_orders channel 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

trade_attribution_of(source: Any) -> TradeAttribution

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 book nonce is this kind: on Binance it is the last update ID of a diff that covers a range of IDs, and watch_order_book can apply several diffs before it returns a book. Databento's sequence is 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

sequence_kind_of(source: Any) -> SequenceKind

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, so exchange_timestamp copies timestamp. The cryptofeed Bitfinex, Blockchain.com and Kraken books are like this.
  • :attr:VENUE_ONLY — the data holds the venue's time only, so timestamp copies exchange_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

clocks_of(source: Any) -> Clocks

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:FeedType).

trade_attribution TradeAttribution

Which orders of a trade the feed names (:class:TradeAttribution).

sequence_kind SequenceKind

What the venue sequence promises (:class:SequenceKind).

clocks Clocks

Which clocks the book rows carry (:class:Clocks).

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; audit does not count these steps back as faults. 0 for data that is not a capture.

to_dict

to_dict() -> dict[str, Any]

Return the declarations as JSON-serialisable values.

from_dict classmethod

from_dict(fields: Mapping[str, Any]) -> FeedDeclarations

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

declarations_of(source: Any) -> FeedDeclarations

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

grid_of(source: Any) -> InstrumentGrid

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 grid that is not an InstrumentGrid.

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(source: Any) -> pd.DataFrame

Load events from source and return a DataFrame.

Parameters:

Name Type Description Default
source Any

Data source identifier. The canonical type is str | Path (a file path), but loaders may accept richer descriptors such as dicts, dataclasses, or connection strings.

required

Returns:

Type Description
DataFrame

Events with at least the columns required by ob_analytics.schemas.validate_events_df.

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_size for the quote currency)
  • volume — int64, the level's new absolute resting size in integer lots (× lot_size for the base asset) after the update (0 removes the level); not a signed delta
  • direction — categorical bid/ask

load

load(source: Any) -> pd.DataFrame

Load a price-level depth stream from source and return the frame.

Parameters:

Name Type Description Default
source Any

Data source identifier — canonically a str | Path (a snapshot + price-level-delta file), but implementations may accept richer descriptors.

required

Returns:

Type Description
DataFrame

Depth with at least the columns required by :func:~ob_analytics.schemas.validate_depth_df.

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_size for the quote currency)
  • volume — int64 (integer lots; × lot_size for the base asset)
  • direction — categorical buy/sell (taker side)
  • maker_event_id — integer event id of the resting order
  • taker_event_id — integer event id of the aggressing order
  • maker — order id of the resting order
  • taker — order id of the aggressing order
  • maker_og — original_number of the maker event
  • taker_og — original_number of the taker event

load

load(events: DataFrame, source: Any) -> pd.DataFrame

Build the trades DataFrame.

Parameters:

Name Type Description Default
events DataFrame

The processed events frame (post-loader).

required
source Any

The same source value passed to :meth:EventLoader.load. Used by file-based readers to locate companion files.

required

Returns:

Type Description
DataFrame

DataWriter

Bases: Protocol

Writes pipeline results to a format-specific output.

Attributes:

Name Type Description
target WriteTarget

Whether :meth:write takes a folder or a file. :func:~ob_analytics.data.save_data refuses a path of the other kind before it calls :meth:write.

target property

target: WriteTarget

Whether this writer takes a folder or a file (:class:WriteTarget).

write

write(
    data: dict[str, DataFrame],
    dest: str | Path,
    **kwargs: Any,
) -> Path | tuple[Path, ...]

Write pipeline DataFrames to dest.

Parameters:

Name Type Description Default
data dict of str to DataFrame

Pipeline output keyed by name (e.g. "events", "trades", "depth", "depth_summary").

required
dest str or Path

The folder or file to write, as :attr:target says.

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:~ob_analytics.sources.SOURCES, e.g. "bitstamp".

level Level

Order-book resolution this source produces: :attr:Level.L3 (per-order events) or :attr:Level.L2 (price-level depth).

feed_type FeedType

The source's crossing invariant (:class:FeedType), so downstream code reasons about crossed books by coordinate, not by source name.

trade_attribution TradeAttribution

Which orders of a trade the source's order events can name (:class:TradeAttribution). Every source in this package declares it. It is not a required member, so a plug-in written before it existed still satisfies the contract; read it with :func:trade_attribution_of, which treats a missing one as :attr:TradeAttribution.BOTH.

sequence_kind SequenceKind

What the venue sequence the source records promises (:class:SequenceKind), so a gap check knows whether a skipped number is a lost message. Optional: read it with :func:sequence_kind_of, which treats a missing one as :attr:SequenceKind.CONTIGUOUS.

clocks Clocks

Which clocks the source's book rows carry (:class:Clocks), so the clock checks run only when there are two to compare. Optional: read it with :func:clocks_of, which treats a missing one as :attr:Clocks.BOTH.

settings SourceSettings

Typed per-source configuration. The empty base for a source that needs none; a typed subclass (e.g. CcxtSettings) for one with venue knobs.

grid InstrumentGrid

The tick and lot size the source's data is usually on (:class:~ob_analytics.config.InstrumentGrid), which a pipeline run starts its config from. Optional: read it with :func:grid_of, which treats a missing one as the default grid.

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.

level property

level: Level

The source's resolution (:class:Level).

feed_type property

feed_type: FeedType

The source's crossing invariant (:class:FeedType).

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

create_loader(
    config: Any, ctx: RunContext
) -> EventLoader | DepthSource

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

create_trade_source(
    config: Any, ctx: RunContext, loader: Any
) -> TradeSource

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

create_writer(config: Any, ctx: RunContext) -> DataWriter

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

required_context() -> list[str]

Return the :class:RunContext field names this source requires.

required_context_of

required_context_of(source: Any) -> list[str]

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. None lets the loader use its own default (ob_analytics.lobster.LOBSTER_DEFAULT_TZ). Ignored by venues that already carry an absolute clock (Bitstamp, CCXT).

symbol (str, optional)

The instrument this run covers (e.g. "BTC/USD"). When supplied, loaders tag each row with an optional symbol column so cross-venue frames can be told apart. None leaves it untagged.

venue (str, optional)

The source venue this run covers (e.g. "bitstamp"). When supplied, it overrides the loader's own source name in the optional venue column; None falls back to that source name. Supplying either symbol or venue is what turns identity tagging on.

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

snapshot(config: CaptureConfig) -> AsyncIterator[EventDict]

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

stream(
    config: CaptureConfig,
) -> AsyncIterator[tuple[str, EventDict, Any]]

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

shutdown_synthetic_events() -> AsyncIterator[EventDict]

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:~ob_analytics.metrics.METRICS, e.g. "amihud".

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 (Level.L3,) only, so it is skipped on an L2 run rather than failing on an empty events table.

needs tuple of str

The run's tables the metric cannot be computed without, by their :class:~ob_analytics.pipeline.PipelineResult attribute names, e.g. ("trades",). The metric is skipped on a run where one of them has no rows, such as a run with no trades.

compute

compute(result: Any) -> pd.DataFrame

Return this metric's table for result (a PipelineResult).

prepare

prepare(frame: DataFrame) -> dict[str, Any]

Turn :meth:compute's table into the payload the renderer takes.