Skip to content

Visualization

To plot straight from a pipeline result, the one-liner plot_result (or result.plot) names a concept and wires up the prepare function and context for you; available_concepts(result) lists what a given result can render:

from ob_analytics.visualization import plot_result

fig = plot_result(result, "depth_heatmap")          # level defaults to L2
fig = result.plot("trade_tape", "L3", backend="plotly")

For full control, the unified plot() dispatcher renders a concept at a resolution level on a chosen backend from already-prepared data. Prepare the payload with the matching helper in the public prepare namespace (friendly wrappers over the internal prepare-data functions) and spread it as keyword arguments:

from ob_analytics.visualization import plot, prepare

fig = plot("trade_tape", level="L2", **prepare.trades(trades))

backend="matplotlib" (default) returns a Matplotlib figure; backend="plotly" returns an interactive Plotly figure (requires pip install ob-analytics[interactive]); backend="bokeh" returns a Bokeh figure (requires pip install ob-analytics[bokeh]), covering the core concepts — trade_tape, depth_heatmap, book_snapshot, depth_chart — for Bokeh / Panel server dashboards and streaming views. Renderers never call plt.show().

Every concept declares a resolution level — Level.L2 (Market-By-Price aggregate) or Level.L3 (Market-By-Order, per order). A concept registered at a single level resolves it automatically, so you pass only the concept name; comparable concepts (both L2 and L3 registered) take an explicit level=.

Concepts with both L2 and L3 faces: trade_tape, order_activity, cancellations, book_snapshot, depth_chart, book_replay, liquidity_at_touch. L2-only: time_series, depth_heatmap, volume_percentiles, events_histogram, hidden_executions, price_view, trade_size. L3-only: order_outcome, queue_position. Level-less analytics: vpin, order_flow_imbalance, kyle_lambda, ofi_horizon, l1_ticker, book_signals, bars, transaction_costs, trading_halts. The first five are also registered metrics, so result.plot("vpin") computes and draws them.

Book replay

book_replay steps through the book over part of a run. The ladder shows the book at one instant per interval. Beside it, the trades panel shows every trade of the replay and the mid, with a line at the time on screen. Drag the slider, or press a play button: Play 1× is real time, and each speed in speeds gets a button. A speed is smooth only when a frame lasts about 50 ms or less, so at 1 s per frame, Play 20× glides and the slower speeds step. For smoother slow playback, pass a shorter interval. The file grows with the number of frames, which max_frames caps at 600. When interval would need more frames, the replay uses a longer interval and logs a warning.

result.plot() and the gallery replay the gallery's zoom window: the middle half of a run shorter than two hours, or the second quarter of a longer run. They set interval to 1 s, or longer so that the window fits in about 300 frames. On the sample data the window is about 15 minutes, so each frame is about 3 s and every speed steps. To choose the window and the interval yourself, pass start_time, end_time and interval:

import pandas as pd

start = result.trades["timestamp"].iloc[100]
fig = result.plot(
    "book_replay", "L2", backend="plotly",
    start_time=start, end_time=start + pd.Timedelta("5min"), interval="1s",
)

Click a trade to move the replay to the last frame at or before it. The price level the trade took is marked in the trade's colour: the ask for a buy, the bid for a sell.

The L2 face replays the depth table, so its touch equals the depth summary's at every frame. The L3 face rebuilds the per-order book from the events at each frame, with stale crossed orders removed (uncross=True, the default here) so that both faces show the same market. Pass uncross=False for the faithful L3 book. Every frame keeps one price range: the mid's path, wide enough for about price_levels price levels per side.

from ob_analytics.visualization import plot_result

fig = plot_result(result, "book_replay", "L3", backend="plotly")
fig.write_html("replay.html")

The replay is drawn by Plotly only. The clicking needs a script, which Plotly cannot store inside a figure, so the replay is a BookReplayFigure: a Plotly figure that adds the script whenever it becomes HTML (write_html, to_html, show, and display in a notebook). In a notebook it displays as HTML, so the notebook must be trusted for the clicking to work.

book_replay

book_replay(
    depth: DataFrame | None = None,
    trades: DataFrame | None = None,
    events: DataFrame | None = None,
    per_order: bool = False,
    start_time: Timestamp | None = None,
    end_time: Timestamp | None = None,
    interval: str | timedelta = "1s",
    max_frames: int = 600,
    price_levels: int = 20,
    speeds: tuple[float, ...] = (1, 5, 20),
    volume_scale: float | None = None,
    uncross: bool = True,
) -> dict[str, Any]

Prepare the book at one instant per interval, for a replay.

Each frame is a :func:prepare_book_snapshot_data payload, so a replay frame draws exactly as a book_snapshot does at that instant.

per_order=False (L2) replays depth with :func:~ob_analytics.depth.price_level_snapshots: one bar per price level, and a touch equal to the depth summary's at every frame. per_order=True (L3) rebuilds the book from events at each frame with :func:~ob_analytics.analytics.order_book: one segment per order. The L3 rebuild reads the whole event table once per frame, so it is the slower of the two.

Both faces show the book with stale crossed orders removed. The L2 face gets this from the depth table, where a fresher quote evicts the opposing price levels it crosses. The L3 face gets it from uncross=True, the default here, which applies the same rule order by order. The two faces then show the same market, and the trades sit around the mid on both. Pass uncross=False to replay the faithful L3 book instead; on a diff feed with stale orders, its touch can sit far from the trades.

Every frame keeps the same price range: the path of the mid, widened so a typical frame shows about price_levels price levels per side. Each frame holds every price level inside that range, so a bar leaves the plot only when its price level leaves the book or the range.

Parameters:

Name Type Description Default
depth DataFrame

The depth table. Required when per_order=False.

None
trades DataFrame

The trades table; the replay shows every trade between the first and last frame. None shows none.

None
events DataFrame

The events table, classified (it needs the type column). Required when per_order=True.

None
per_order bool

False for the L2 face, True for the L3 face.

False
start_time Timestamp

The first and last frame. Default to the first and last timestamp of the table the face replays.

None
end_time Timestamp

The first and last frame. Default to the first and last timestamp of the table the face replays.

None
interval str or timedelta

The time between frames, as a pandas offset string or a duration.

'1s'
max_frames int

The most frames to build, at least 1. Each frame is a book snapshot held in the payload and in the figure, so this caps their size. When interval would need more, the interval is widened to fit, and a warning says so.

600
price_levels int

About how many price levels per side a typical frame shows; it sets the width of the price range.

20
speeds tuple of float

The playback speeds offered, as multiples of real time; each gets a play button. A frame is shown for interval divided by the speed, so a speed is smooth only when that is short (about 50 ms or less) and steps visibly otherwise. The fastest speeds run a little slower than stated, by the time the browser takes to draw each frame.

(1, 5, 20)
volume_scale float or None

Size display scale. None infers one scale from every frame, so bar lengths compare across frames.

None
uncross bool

L3 only: evict crossed resting orders before the price range is chosen, as order_book(uncross=True) does. True by default, to match the L2 face; the L2 face is uncrossed by the depth table and ignores it.

True

Returns:

Type Description
dict

snapshots (one book-snapshot payload per frame, in time order), price_range (the (low, high) price range every frame shares, or None when no frame has both sides), interval (the time between frames, as a :class:pandas.Timedelta), speeds, trades (timestamp, price, volume and direction, sizes scaled, from the first frame to the last), per_order and volume_scale.

Raises:

Type Description
ConfigError

If the table the face replays is missing or empty, interval is not positive, max_frames or price_levels is less than 1, speeds is empty or holds a speed that is not positive, or end_time is before start_time.

Dispatcher

plot

plot(
    concept: str,
    level: Level | None = _UNSET,
    *,
    backend: str = "matplotlib",
    ax: Axes | None = None,
    **data: Any,
) -> Any

Render concept at level on backend from already-prepared data.

Prepare data with the matching prepare_<concept>_data in :mod:ob_analytics.visualization._data (or a gallery helper) and spread it as keyword arguments::

from ob_analytics.visualization import plot
from ob_analytics.visualization import _data
fig = plot("trade_tape", backend="matplotlib",
           **_data.prepare_trades_data(trades))

Parameters:

Name Type Description Default
concept str

Plot concept, e.g. "trade_tape" or "order_activity".

required
level Level

Resolution level (Level.L2/Level.L3). Omit to auto-resolve when the concept is registered at a single level; required for a comparable concept registered at both.

_UNSET
backend str

Registered backend name (default "matplotlib").

'matplotlib'
ax Axes

Axes to draw on (matplotlib only; ignored by other backends). The figure that holds ax is yours: the face draws on ax and leaves the figure's layout alone, so call fig.tight_layout() (or use a layout engine) once all the panels are drawn. Without ax, the face creates its own figure and lays it out.

None
**data Any

Prepared plot data, as returned by the matching prepare_* helper. May include theme=PlotTheme(...) to override :data:DEFAULT_THEME for this call, on any backend.

{}

Returns:

Type Description
Figure or Figure or figure

Raises:

Type Description
ValueError

If backend is not registered, or concept is comparable and no level was given.

KeyError

If concept (at the resolved level) is not registered.

Theme and Saving

Pass theme=PlotTheme(...) to plot() to override DEFAULT_THEME for a single call, on any backend; there is no global theme to set.

PlotTheme dataclass

PlotTheme(
    style: str = "white",
    context: str = "notebook",
    font_scale: float = 1.05,
    palette: Palette = DEFAULT_PALETTE,
    rc: dict[str, object] = (
        lambda: {
            "axes.grid": True,
            "grid.linestyle": ":",
            "grid.alpha": 0.35,
            "axes.spines.top": False,
            "axes.spines.right": False,
            "axes.edgecolor": DEFAULT_PALETTE.rule,
            "axes.titlelocation": "left",
            "axes.titleweight": "bold",
            "lines.linewidth": 2.0,
        }
    )(),
    plotly_layout: dict[str, object] = (
        lambda: {
            "title": {
                "x": 0.0,
                "xanchor": "left",
                "xref": "paper",
                "font": {"weight": "bold"},
            }
        }
    )(),
    bokeh_figure: dict[str, object] = dict(),
)

Configurable visual theme for ob-analytics plots, in every backend.

Attributes:

Name Type Description
style str

Seaborn style name: "white", "whitegrid", "ticks", "dark", or "darkgrid". In plotly and bokeh the two dark styles draw on seaborn's gray background and "ticks" draws outside tick marks.

context str

Seaborn context name: "paper", "notebook", "talk", or "poster". Scales text in every backend.

font_scale float

Font scaling factor, applied on top of context in every backend.

palette Palette

The colours every backend draws with.

rc dict[str, object]

Matplotlib rc overrides applied on top of the seaborn theme (matplotlib only).

plotly_layout dict[str, object]

Plotly layout properties applied on top of the theme's template (plotly only), e.g. {"font": {"family": "Georgia"}}.

bokeh_figure dict[str, object]

Keyword arguments passed to :func:bokeh.plotting.figure on top of the theme's defaults (bokeh only), e.g. {"width": 1200}.

dark property

dark: bool

Whether the style draws on a dark (gray) background.

text_scale property

text_scale: float

Text size relative to the default theme (context x font_scale).

The plotly and bokeh backends multiply their base font sizes by this, so the default theme keeps their base sizes exactly.

DEFAULT_THEME module-attribute

DEFAULT_THEME: PlotTheme = PlotTheme()

Palette dataclass

Palette(
    bid: str = "#0072B2",
    ask: str = "#D55E00",
    buy: str = "#009E73",
    sell: str = "#D55E00",
    flashed: str = "#E69F00",
    resting: str = "#009E73",
    filled: str = "#009E73",
    partial: str = "#CC79A7",
    cancelled: str = "#E69F00",
    hidden_trade: str = "#F0E442",
    check_trade: str = "#999999",
    price_line: str = "#222222",
    reference_line: str = "#888888",
    rule: str = "#444444",
    label: str = "#555555",
    spread_fill: str = "#9aa0a6",
    neutral: str = "#7f8c8d",
    series: str = "#5dade2",
    emphasis: str = "#e74c3c",
    threshold: str = "#f39c12",
    secondary_axis: str = "#f1c40f",
    imbalance: str = "#6d28d9",
    effective_spread: str = "#0072B2",
    realized_spread: str = "#CC79A7",
)

Named plot colours, shared by every rendering backend.

Every field is a hex colour string. Override single fields with :func:dataclasses.replace or by passing them to the constructor::

Palette(bid="#1f77b4", ask="#d62728")

Attributes:

Name Type Description
bid, ask str

Side of a resting order (Okabe–Ito blue / vermillion).

buy, sell str

Aggressor side of an execution: buyer-initiated (lifts the ask) and seller-initiated (hits the bid).

flashed, resting str

Order-activity fate: placed and pulled / rested or filled.

filled, partial, cancelled str

Order-outcome fate: fully executed / partly executed with the remainder removed / removed without any execution.

hidden_trade str

A confirmed hidden-order trade.

check_trade str

A trade flagged as hidden whose maker order was visible (a diff feed's stale depth summary), which needs checking rather than trusting.

price_line str

The main price line (trade price, microprice) and dark markers.

reference_line str

Secondary reference marks: the mid line, dotted guides, "created" markers, and "no data" messages.

rule str

Zero lines, the book's mid rule, and axis edges.

label str

Annotation text.

spread_fill str

The band between best bid and best ask.

neutral str

Points with no side (for example hidden executions of unknown direction).

series str

A single unclassified data series (time series, VPIN buckets, Kyle's-lambda scatter).

emphasis str

A summary drawn over a series: rolling average, fitted line, halt span.

threshold str

A threshold line.

secondary_axis str

A series on a secondary y-axis, and that axis's labels.

imbalance str

The order book imbalance line.

effective_spread, realized_spread str

The two transaction-cost series.

DEFAULT_PALETTE module-attribute

DEFAULT_PALETTE: Palette = Palette()

save_figure

save_figure(
    fig: Figure,
    path: str | Path,
    *,
    dpi: int = 150,
    **kwargs: object,
) -> None

Save a matplotlib fig to path with sensible defaults.

Parameters:

Name Type Description Default
fig Figure

The matplotlib figure to save.

required
path str or Path

Destination file path (e.g. "output/plot.png").

required
dpi int

Resolution in dots per inch (default 150).

150
**kwargs object

Additional keyword arguments forwarded to :meth:~matplotlib.figure.Figure.savefig. Pass bbox_inches="tight" explicitly to crop to artist extents — it is not the default because it forces a second full draw (roughly doubling save time on dense figures), and every renderer already lays out the figures it creates.

{}

infer_volume_scale module-attribute

infer_volume_scale = infer_volume_scale

Renderer registry

Backends self-register their renderers into RENDERERS, keyed by the coordinate (concept, level, backend) (where level is a Level or None for level-less analytics). A concept is level-less or drawn at a level, and the same kind on every backend: registering it the other way raises ValueError. Register a whole new backend module with register_plot_backend, or a single renderer directly with RENDERERS.register((concept, level, backend), fn). To see how a concept is registered before adding to it, list its (level, backend) pairs with RENDERERS.placements(concept).

register_plot_backend

register_plot_backend(name: str, module_path: str) -> None

Register a visualization backend module.

The module at module_path must call RENDERERS.register((concept, level, name), fn) for each plot it supports (typically at import time). It is imported lazily on the first :func:plot call that targets name.

Parameters:

Name Type Description Default
name str

Backend name used in plot(..., backend=name).

required
module_path str

Dotted import path, e.g. "my_package._bokeh_backend".

required

Examples:

>>> from ob_analytics.visualization import register_plot_backend
>>> register_plot_backend("bokeh", "my_pkg._bokeh")

RENDERERS module-attribute

RENDERERS: RendererRegistry = RendererRegistry('renderer')

RendererRegistry

RendererRegistry(kind: str)

Bases: Registry[RendererKey, RendererFn]

The renderer :class:Registry, which keeps each concept one kind.

A concept is either level-less, registered at None, or drawn at a level, registered at Level.L2 and/or Level.L3. It is the same kind on every backend: :func:plot and the gallery call it the same way whichever backend draws it.

placements

placements(
    concept: str,
) -> tuple[tuple[Level | None, str], ...]

Return each (level, backend) concept is registered at, in order.

A snapshot, so a caller can register while looping over it.

register

register(key: RendererKey, value: RendererFn) -> None

Register value under the key (concept, level, backend).

Raises:

Type Description
ValueError

If key is not (concept, level, backend) with a text concept, a :class:Level or None level and a text backend, or its concept is already registered, on any backend, as the other kind: at a level when key is level-less, or level-less when key has a level.