Skip to content

The visualization system

Every figure in this tutorial — the ladders, the keyframe strips, the heatmaps, the queue trajectories, the toxicity faces — came from one small system with three moving parts. This closing chapter names them, so that when you want a figure this tutorial did not draw, you know which of the three to change.

The three parts are:

  • conceptwhat to show (depth_heatmap, trade_tape, queue_position, …);
  • level — at what resolution, L2 (aggregated) or L3 (per-order), the distinction from chapter 2;
  • backend — with which renderer, Matplotlib (static, default) or Plotly (interactive).

Name a concept and a level; the system finds the data preparation and the renderer for you.

The one-liner

You have used it since chapter 1. A PipelineResult knows how to plot itself:

%matplotlib inline
from ob_analytics import Pipeline, sample_csv_path

result = Pipeline().run(sample_csv_path())
fig = result.plot("depth_heatmap")  # level defaults sensibly per concept

png

result.plot(concept, level) (and the equivalent free function plot_result(result, concept, level)) is the whole convenience layer. It does not guess what a result can draw — it knows, because that depends on the data's resolution, exactly as chapter 2 promised. Ask it:

from ob_analytics.visualization import available_concepts

available_concepts(result)
{'trade_tape': ['L2', 'L3'],
 'depth_heatmap': ['L2'],
 'order_activity': ['L2', 'L3'],
 'cancellations': ['L2', 'L3'],
 'book_snapshot': ['L2', 'L3'],
 'depth_chart': ['L2', 'L3'],
 'order_outcome': ['L3'],
 'queue_position': ['L3'],
 'liquidity_at_touch': ['L2', 'L3'],
 'volume_percentiles': ['L2'],
 'price_view': ['L2'],
 'events_histogram': ['L2'],
 'trade_size': ['L2']}

Each concept maps to the levels this particular result can render. A Bitstamp capture carries per-order identity, so most concepts offer both L2 and L3; some are inherently one resolution — depth_heatmap aggregates by construction (L2 only), while queue_position and order_outcome need per-order identity (L3 only). Run this on a feed that lost identity and the L3 rows would simply not appear: the menu is a function of the data, not a fixed list.

The concept catalogue

Here is the whole vocabulary at a glance — every concept rendered from this one capture, grouped by the question it answers and tagged with the chapter that introduced it:

import matplotlib.pyplot as plt
import pandas as pd

from _docs_theme import DOCS_THEME
from ob_analytics.depth import get_spread
from ob_analytics.visualization import plot, prepare

t0 = result.trades["timestamp"].min()
t1 = t0 + pd.Timedelta(minutes=10)
spread = get_spread(result.depth_summary)

catalogue = [
    (
        "depth_heatmap",
        "L2",
        lambda: prepare.price_levels(
            result.depth,
            spread=spread,
            trades=result.trades,
            col_bias=0.4,
            start_time=t0,
            end_time=t1,
        ),
    ),
    (
        "trade_tape",
        "L2",
        lambda: prepare.trades(
            result.trades, spread=spread, start_time=t0, end_time=t1
        ),
    ),
    ("order_outcome", "L3", lambda: prepare.order_outcome_l3(result.events)),
    (
        "queue_position",
        "L3",
        lambda: prepare.queue_position_l3(result.events, start_time=t0, end_time=t1),
    ),
    (
        "volume_percentiles",
        "L2",
        lambda: prepare.volume_percentiles(
            result.depth_summary, start_time=t0, end_time=t1
        ),
    ),
    ("cancellations", "L3", lambda: prepare.cancellations_l3(result.events)),
]

fig, axes = plt.subplots(2, 3, figsize=(16, 8))
for ax, (concept, level, make_payload) in zip(axes.ravel(), catalogue):
    plot(concept, level=level, ax=ax, theme=DOCS_THEME, **make_payload())
    ax.set_title(f"{concept}  ·  {level}", fontsize=10)
fig.tight_layout()
/tmp/ipykernel_2592/1378649985.py:52: UserWarning: This figure includes Axes that are not compatible with tight_layout, so results might be incorrect.
  fig.tight_layout()

png

Six of the fourteen concepts, one capture, one loop. The full set, organised:

Question Concepts Introduced
What does the book look like? book_snapshot, depth_chart, depth_heatmap 1, 5
What traded? trade_tape, trade_size, events_histogram 1
How much liquidity, and where? volume_percentiles, liquidity_at_touch, price_view 5
Who placed, waited, cancelled, filled? order_activity, order_outcome, queue_position, cancellations 2, 4

A fifth family — the flow-toxicity faces vpin, kyle_lambda, order_flow_imbalance, ofi_horizon from chapter 6 — sits slightly apart: they plot a computed metric, not the result directly, so they do not appear in available_concepts and are drawn the long way (next section).

Under the one-liner: plot and prepare

The one-liner is a shortcut for two explicit steps you have watched every composite figure in this tutorial take. Prepare the data for a concept, then plot it:

payload = prepare.trades(result.trades, spread=spread, start_time=t0, end_time=t1)
fig = plot("trade_tape", level="L2", **payload)

png

prepare.<concept>(...) turns analysis frames into the exact payload a face needs and hands it back as a dict; plot(concept, level, **payload) renders it. result.plot("trade_tape", "L2") is precisely these two lines with the arguments filled in for you.

Reach past the one-liner for three things, all of which appeared earlier in this tutorial:

  1. Custom arguments to a face — the col_bias, start_time, price_from windowing knobs from chapter 5 are prepare arguments.
  2. ax= to place a face in a multi-panel figure — every keyframe strip and story composite in this tutorial is plot(..., ax=...) into a grid.
  3. Concepts that need a computed input — the toxicity faces:
from ob_analytics.flow_toxicity import compute_vpin

vpin = compute_vpin(result.trades, bucket_volume=result.trades["volume"].sum() / 20)
fig = plot("vpin", **prepare.vpin(vpin, threshold=0.7))

png

The progression across this whole tutorial has been exactly this ladder: result.plot(...) when you want a figure now, plot + prepare when you want control, and — for data past the in-memory envelope — the pre-slicing you met in chapter 3. Convenience, control, scale — the same three levels seaborn and similar libraries offer.

Two backends

Everything so far rendered with Matplotlib. Pass backend="plotly" for an interactive figure — zoom, pan, hover — from the same concept and data (Plotly ships in the [interactive] extra):

fig = result.plot("depth_heatmap", backend="plotly", col_bias=0.4)
type(fig).__module__.split(".")[0], type(fig).__name__
('plotly', 'Figure')

The call returns a Plotly figure instead of a Matplotlib one; in a notebook fig.show() renders it live, and fig.write_html("depth.html") saves a standalone interactive file. The rule of thumb:

You want… Backend
A figure for a paper, README, or the docs matplotlib (default)
To explore — zoom into a burst, read exact values on hover plotly
A third renderer (Bokeh, …) register your own (below)

Backends live in a registry, so a new one is a module path away — register_plot_backend("bokeh", "my_package._bokeh_backend") — the same structural-typing story as loaders and formats from chapter 3. See Extending ob-analytics.

Theming

There is no global style to set and accidentally leak between figures. A PlotTheme is passed per call and applies only to that call (it is how every figure in this tutorial shares one look — the DOCS_THEME used above):

from ob_analytics.visualization import PlotTheme, save_figure

theme = PlotTheme(style="whitegrid", context="talk", font_scale=1.1)
fig = plot(
    "trade_tape",
    level="L2",
    theme=theme,
    **prepare.trades(result.trades, spread=spread, start_time=t0, end_time=t1),
)
save_figure(fig, "trades_talk.png", dpi=200)

png

PlotTheme bundles a Seaborn style, context, font scale, and any Matplotlib rc overrides; save_figure writes at the DPI you ask for. Theming is Matplotlib-only — Plotly figures carry their own styling.

Wrapping up

You started with a single number on a screen and the claim that it was the top of an order book. Eight chapters later you can load one from any of three feeds, reconstruct it to the individual order, read its depth and its queue, score the toxicity of its flow, and render any of it two ways — all from frames small enough, at the toy scale, to check by hand.

Where to go next:


Vocabulary introduced here — concept, level, backend, the plot / prepare split — lives in the Glossary.