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 import Pipeline, sample_csv_path
from ob_analytics.visualization import plot_result
result = Pipeline().run(sample_csv_path())
fig = plot_result(result, "depth_heatmap") # drawn at L2 only
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(result.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,
transaction_costs, amihud, roll_spread, bars, trading_halts. All
but the last two 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 busiest
stretch of trading (busiest_window), half as long as a run shorter than two
hours, or a quarter as long as 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. A crossed frame has no
mid, so it adds its best bid and best ask to the path instead.
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, and the concept table says so: asking
another backend for it raises a KeyError that names Plotly. 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, replayed as the depth summary replays it:
of two crossed price levels, the one updated less recently is left out.
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. A crossed frame has no mid; it adds its best bid and best ask to the path instead, so a faithful replay still shows the orders that cross. 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 |
None
|
trades
|
DataFrame
|
The trades table; the replay shows every trade between the first and
last frame. |
None
|
events
|
DataFrame
|
The events table, classified (it needs the |
None
|
per_order
|
bool
|
|
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
|
uncross
|
bool
|
L3 only: evict crossed resting orders before the price range is
chosen, as |
True
|
Returns:
| Type | Description |
|---|---|
dict
|
|
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 | str | 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. |
required |
level
|
Level or str or None
|
Resolution level ( |
_UNSET
|
backend
|
str
|
Registered backend name (default |
'matplotlib'
|
ax
|
Axes
|
Axes to draw on (Matplotlib only; another backend refuses it). The
figure that holds ax is yours: the face draws on ax and leaves the
figure's layout alone, so call |
None
|
**data
|
Any
|
Prepared plot data, as returned by the matching |
{}
|
Returns:
| Type | Description |
|---|---|
Figure or Figure or figure
|
|
Raises:
| Type | Description |
|---|---|
UnknownNameError
|
If backend is not registered, or does not draw concept (at level). The message names the backends that do. |
ConfigError
|
If level was left out and backend draws concept at both L2 and L3, or ax is given for a backend other than Matplotlib. |
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: |
context |
str
|
Seaborn context name: |
font_scale |
float
|
Font scaling factor, applied on top of |
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. |
bokeh_figure |
dict[str, object]
|
Keyword arguments passed to :func: |
text_scale
property
¶
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.
Palette
dataclass
¶
Palette(
bid: str = "#0072B2",
ask: str = "#D55E00",
buy: str = "#009E73",
sell: str = "#D55E00",
filled: str = "#009E73",
partial: str = "#CC79A7",
cancelled: str = "#E69F00",
hidden_trade: str = "#F0E442",
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 trade that printed inside the visible spread, against an order the book did not show. |
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. |
save_figure ¶
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. |
required |
dpi
|
int
|
Resolution in dots per inch (default 150). |
150
|
**kwargs
|
object
|
Additional keyword arguments forwarded to
:meth: |
{}
|
Display windows¶
The gallery picks one time window and one price band per run, and every face it clips gets the same bounds. Pass them to your own faces to match it.
Renderer registry¶
Every plot of the package is declared once, in a concept table, at the
coordinate (concept, level, backend) (where level is a Level or None
for level-less analytics). The table says which backends draw each plot, and
the built-in backends register their renderers into RENDERERS from it.
Asking a backend for a plot it does not draw raises UnknownNameError
(also a KeyError) that names the backends that do. A concept is level-less or drawn at a level, and the
same kind on every backend: registering it the other way raises ConfigError.
Register a whole new backend module with
register_plot_backend (or advertise it under the ob_analytics.plot_backends
entry-point group), or a single renderer directly with
RENDERERS.register((concept, level, backend), fn); plot(), plot_result()
and the gallery find such a renderer as they find the table's. To see how a
concept is registered before adding to it, list its (level, backend) pairs
with RENDERERS.placements(concept).
register_plot_backend ¶
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:~ob_analytics.visualization.plot call that targets name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Backend name used in |
required |
module_path
|
str
|
Dotted import path, e.g. |
required |
Examples:
load_plot_backend_plugins ¶
Discover and register plot backends advertised via entry points.
Scans the :data:ENTRY_POINT_GROUP entry-point group and registers each
entry's module under the entry-point name, as :func:register_plot_backend
would. The module is not imported here: it is imported on the first
:func:~ob_analytics.visualization.plot call that targets the backend.
The scan runs once per process unless force is set. A plug-in that fails to register is skipped with a warning, so one broken package cannot stop the rest from loading.
Returns:
| Type | Description |
|---|---|
list of str
|
The backend names newly registered by this call. |
RendererRegistry ¶
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:~ob_analytics.visualization.plot and the
gallery call it the same way whichever backend draws it.
get ¶
Return the renderer for (concept, level, backend).
Raises:
| Type | Description |
|---|---|
UnknownNameError
|
If no renderer is registered under key. The message says which backends or levels do draw the concept, or names the concepts closest to it when no backend draws it. |
missing ¶
The error for a concept that backend does not draw at level.
placements ¶
Return each (level, backend) concept is registered at, in order.
A snapshot, so a caller can register while looping over it.
register ¶
Register value under the key (concept, level, backend).
Raises:
| Type | Description |
|---|---|
ConfigError
|
If key is not |