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 |
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 | 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
|
Resolution level ( |
_UNSET
|
backend
|
str
|
Registered backend name (default |
'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 |
None
|
**data
|
Any
|
Prepared plot data, as returned by the matching |
{}
|
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: |
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",
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. |
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: |
{}
|
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 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 |
required |
module_path
|
str
|
Dotted import path, e.g. |
required |
Examples:
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:plot and the gallery call it the same way
whichever backend draws it.
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 |
|---|---|
ValueError
|
If key is not |