Skip to content

Metric registry

A metric measures a finished run and draws as a level-less plot. Every metric registers here under a name; third-party metrics load through the ob_analytics.metrics entry-point group (see Extending). A registered value is a Metric instance — a metric needs no per-run construction, so the object registered is the object called.

The name is also the plot concept the metric draws under, so a renderer registered at (name, None, backend) is the metric's face. A registered metric shows up in available_concepts(), renders through result.plot(name), and gets its own gallery card — with no edit to ob-analytics.

Metrics run on demand, not during Pipeline.run: use PipelineResult.metric for one, PipelineResult.metrics() for every metric that applies to the run: its resolution is one of the metric's levels, and each table in the metric's needs has rows, so a run with no trades skips the trade metrics. Settings are keyword arguments: result.metric("vpin", n_buckets=20), or result.plot("vpin", n_buckets=20, threshold=0.8), where each keyword goes to compute or prepare, whichever names it.

Built-in metrics

Name Measures Wraps
l1_ticker Best bid, best ask and last trade. Draws the three over time, or the quote card at at=. the depth summary and the trades
book_signals Mid, micro-price and order-book imbalance depth_signals
vpin VPIN per volume bucket compute_vpin
kyle_lambda Kyle's λ regression compute_kyle_lambda
order_flow_imbalance Order flow imbalance per window order_flow_imbalance
ofi_horizon Order flow imbalance over several horizons ofi_by_horizon
transaction_costs Effective and realized spread and price impact per trade transaction_costs
amihud Amihud illiquidity per window amihud
roll_spread Roll's implied spread per window roll_spread

Each uses the defaults of the function it wraps, and the run's own depth_summary wherever that function takes quotes.

Every metric reports in the instrument's own units: prices in the quote currency and sizes in BTC, shares or contracts. result.metric, result.plot and the gallery all compute on the run as stored (integer ticks and lots), and each measure converts with the tick_size and lot_size the run's tables carry, so the three give the same numbers. A setting means the same everywhere too: a size such as bucket_volume is in the instrument's own units and a price in the quote currency, so result.metric("vpin", bucket_volume=0.5) and result.plot("vpin", bucket_volume=0.5) use the same 0.5 BTC buckets.

L1TickerMetric

The Level 1 quote: best bid, best ask and last trade through the run.

The table has one row each time the touch or the last trade changes. Its face draws the three prices over time, or, given at=, the quote card for that instant (see :func:~ob_analytics.visualization._data.prepare_l1_ticker_data).

compute

compute(result: Any) -> pd.DataFrame

Return timestamp, the touch prices and sizes, and the last trade.

Columns: timestamp, best_bid_price, best_bid_vol, best_ask_price, best_ask_vol, last_price, last_volume, prices in the quote currency and sizes in the instrument's units. Each row carries every value forward from the rows before it, so a row is the whole quote at that time. last_price is NaN before the first trade, and a side with no orders has a NaN price and size (the depth summary writes 0 there), read with the same test as :func:~ob_analytics.depth.readable_quotes. The other side is still shown.

prepare

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

Payload for the face; options are those of prepare_l1_ticker_data.

BookSignalsMetric

Mid, micro-price and order-book imbalance, from :func:~ob_analytics.depth.depth_signals.

The table is the run's depth_summary with the signal columns added, prices in the quote currency and sizes in the instrument's units. Its face draws the micro-price against the mid over an imbalance strip.

compute

compute(
    result: Any, *, depth_levels: int = 5
) -> pd.DataFrame

Return the summary with mid_price, micro_price, obi and obi_depth.

depth_levels is depth_signals's: the depth of obi_depth. The level count used, after clamping to the bins present, is in attrs["depth_levels"].

prepare

prepare(
    frame: DataFrame,
    *,
    start_time: Timestamp | None = None,
    end_time: Timestamp | None = None,
) -> dict[str, Any]

Payload for the face, for the rows from start_time to end_time.

VpinMetric

VPIN, from :func:~ob_analytics.flow_toxicity.compute_vpin.

compute

compute(
    result: Any,
    *,
    bucket_volume: float | None = None,
    n_buckets: int = 50,
    sign_method: str | None = None,
) -> pd.DataFrame

Return the per-bucket VPIN table; the settings are compute_vpin's.

bucket_volume is in the instrument's own units, as compute_vpin takes it.

prepare

prepare(
    frame: DataFrame, *, threshold: float = 0.7
) -> dict[str, Any]

Payload for the face; threshold is the alert line.

KyleLambdaMetric

Kyle's λ, from :func:~ob_analytics.flow_toxicity.compute_kyle_lambda.

The table is the per-window regression data. The fit itself is in the table's attrs: lambda_, t_stat, r_squared, n_windows, ci_low, ci_high, ci_level and diagnostics, as on :class:~ob_analytics.flow_toxicity.KyleLambdaResult.

compute

compute(
    result: Any,
    *,
    window: str = "5min",
    sign_method: str | None = None,
    n_boot: int = 1000,
    ci_level: float = 0.95,
    seed: int | None = 0,
) -> pd.DataFrame

Return the regression table; the settings are compute_kyle_lambda's.

prepare

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

Payload for the regression scatter.

OrderFlowImbalanceMetric

Order flow imbalance, from :func:~ob_analytics.flow_toxicity.order_flow_imbalance.

The table adds last_price, the last trade price in each window in the quote currency, which the face draws as a price line behind the bars.

compute

compute(
    result: Any,
    *,
    window: str = "1min",
    sign_method: str | None = None,
) -> pd.DataFrame

Return the per-window table; the settings are order_flow_imbalance's.

prepare

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

Payload for the bars, with the window prices as the price line.

OfiHorizonMetric

Order flow imbalance over several horizons, from :func:~ob_analytics.flow_toxicity.ofi_by_horizon.

compute

compute(
    result: Any,
    *,
    horizons: tuple[str, ...] = OFI_HORIZONS,
    grid: str = "5s",
    sign_method: str | None = None,
) -> pd.DataFrame

Return the grid table; the settings are ofi_by_horizon's.

prepare

prepare(
    frame: DataFrame,
    *,
    start_time: Timestamp | None = None,
    end_time: Timestamp | None = None,
) -> dict[str, Any]

Payload for the horizon graph, for the steps from start_time to end_time.

TransactionCostsMetric

Per-trade costs, from :func:~ob_analytics.cost.transaction_costs.

Measured against the run's own depth_summary. The face draws the effective and realized spread in basis points, averaged over windows.

compute

compute(
    result: Any,
    *,
    horizon: str = "1min",
    sign_method: str | None = None,
) -> pd.DataFrame

Return the per-trade cost table; the settings are transaction_costs's.

prepare

prepare(
    frame: DataFrame, *, window: str = "1min"
) -> dict[str, Any]

Payload for the face; window is the span the lines average over.

AmihudMetric

Amihud illiquidity, from :func:~ob_analytics.cost.amihud.

compute

compute(
    result: Any, *, window: str | None = None
) -> pd.DataFrame

Return the per-window table; the setting is amihud's.

prepare

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

Payload for the face: one point per window.

RollSpreadMetric

Roll's implied spread, from :func:~ob_analytics.cost.roll_spread.

compute

compute(
    result: Any, *, window: str | None = None
) -> pd.DataFrame

Return the per-window table; the setting is roll_spread's.

prepare

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

Payload for the face: one point per window.

Registry

register_metric

register_metric(metric: Metric) -> None

Register metric under its own :attr:~ob_analytics.protocols.Metric.name.

Case-insensitive; overwriting an existing registration is allowed (handy for tests and for a plug-in that intentionally shadows a built-in).

Raises:

Type Description
TypeError

If metric lacks a member of the :class:~ob_analytics.protocols.Metric protocol, such as needs. Checked here, so one incomplete plug-in fails when it loads instead of breaking every metric a run computes.

list_metrics

list_metrics() -> list[str]

Return a sorted list of registered metric names.

get_metric

get_metric(name: str) -> Metric

Return the metric registered under name (case-insensitive).

Raises:

Type Description
UnknownNameError

If no metric is registered under name; the message names the registered names closest to it.

load_metric_plugins

load_metric_plugins(*, force: bool = False) -> list[str]

Discover and register metrics advertised via entry points.

Scans the :data:ENTRY_POINT_GROUP entry-point group; each entry's value is loaded to a :class:~ob_analytics.protocols.Metric class, instantiated with no arguments, and registered under its own name. This is what lets a metric live in a separate installable package without editing ob-analytics.

The scan runs once per process unless force is set. A plug-in that fails to load is skipped with a warning, so one broken package cannot stop the rest from loading.

Returns:

Type Description
list of str

The names newly registered by this call.

skip_reason

skip_reason(metric: Metric, result: Any) -> str | None

Return why metric does not apply to result, or None when it does.

A metric applies when result is at one of its :attr:~ob_analytics.protocols.Metric.levels and every table it :attr:~ob_analytics.protocols.Metric.needs has rows. This is the one test :meth:~ob_analytics.pipeline.PipelineResult.metrics, the gallery and :func:~ob_analytics.visualization.plot_result use.

applicable_metrics

applicable_metrics(result: Any) -> list[Metric]

Return the registered metrics that apply to result, by name.

See :func:skip_reason for what applies.