Skip to content

Theme plots, save artefacts, and export data

How to style figures, save them, serialise pipeline outputs, and switch to the interactive Plotly backend.

Themes and saving

There is no global theme to set. Pass a PlotTheme to plot() and it applies only to that call, on any backend:

from ob_analytics import Pipeline, sample_csv_path
from ob_analytics.visualization import plot, save_figure, prepare, PlotTheme

result = Pipeline().run(sample_csv_path())

theme = PlotTheme(
    style="whitegrid",
    context="talk",
    font_scale=1.2,
    rc={"axes.facecolor": "#f8f9fa", "figure.facecolor": "#ffffff"},
)

fig = plot("trade_tape", level="L2", theme=theme, **prepare.trades(result.trades))
save_figure(fig, "trades_hires.png", dpi=300)

The same theme styles the Plotly and Bokeh backends. Each backend reads what it can from the shared fields and ignores the other backends' overrides:

Field Matplotlib Plotly Bokeh
palette every mark's colour every mark's colour every mark's colour
context, font_scale Seaborn text sizes template font size title, axis, and tick text
style Seaborn style plotly_white, or seaborn for dark/darkgrid white, or Seaborn's gray for dark/darkgrid
rc applied last — —
plotly_layout — applied last to the template —
bokeh_figure — — passed to bokeh.plotting.figure

To change colours, pass a Palette. Its fields name what each colour means, such as bid/ask (side), buy/sell (aggressor), and price_line/reference_line (neutral marks):

from ob_analytics.visualization import Palette, PlotTheme

theme = PlotTheme(
    palette=Palette(buy="#1f77b4", sell="#d62728"),
    plotly_layout={"font": {"family": "Georgia"}},
)
fig = plot(
    "trade_tape",
    level="L2",
    backend="plotly",
    theme=theme,
    **prepare.trades(result.trades),
)

Serialisation

save_data writes one Parquet file per table. Pass it the result: each file then records the run's config, so the tables keep their tick size, lot size and display precision. load_result reads the folder back as the same result. load_data reads the tables alone, as a dict of DataFrames.

from ob_analytics import load_data, load_result, save_data

save_data(result, "output/my_analysis")

same = load_result("output/my_analysis")   # a PipelineResult, config included
data = load_data("output/my_analysis")     # {"events": ..., "trades": ..., ...}

load_result also reads the folders ob-analytics process writes. A folder that records only the tick and lot size (from run_windows, or an older version) gets those two, with the display precision that shows one step of each.

To hand the tables to another tool without writing files first, convert the result in memory:

tables = result.to_arrow()    # dict[str, pyarrow.Table]
frames = result.to_polars()   # dict[str, polars.DataFrame], needs polars

Both give the same four keys as the dict above. The Arrow tables carry the schema version, tick size, lot size and config in their metadata, the same as the Parquet files. See Frame types: pandas in, pandas out.

Formats, and whether each takes a folder or a file

save_data(..., fmt=...) writes the formats below. list_writers() in ob_analytics.data returns the same list, with any format a plug-in adds. This package's own formats write several files, so path is a folder, created when missing. An export for another tool writes one file, so path is that file.

fmt path is What it writes
"parquet" (default) a folder one Parquet file per table
"depth_csv" a folder depth.csv, and trades.csv when the run has trades
"bitstamp" a folder orders.csv, and trades.csv when the run has trades
"lobster" a folder a LOBSTER message file and orderbook file
"hftbacktest" a file an hftbacktest .npz feed
"nautilus" a file a Parquet file of Nautilus order-book deltas
"databento" a file a DBN file of MBO records

save_data checks path before it writes. A folder format refuses an existing file, and a name that ends in a file extension such as .parquet, .csv or .npz. A file format refuses an existing folder, and a name with no extension. The error says which the format expected. A folder name may hold other dots, such as btc_2024.05.01.

For LOBSTER round-trip output (back to message + orderbook CSVs), pass fmt="lobster" and a RunContext so the registered writer factory can pick up trading_date. The message file has one row per event, in time order. The orderbook file holds the book after each event, taken from the depth table (or computed from events when there is no depth), so a run from any source can be written. num_levels (default 10) sets how many price levels per side it holds. A LOBSTER price is a whole number of ten-thousandths of a dollar, so one tick must be a whole number of them: a cent tick can be written, a tick of a hundred-thousandth cannot. The writer raises ConfigError rather than round the prices:

from ob_analytics import save_data
from ob_analytics.protocols import RunContext

save_data(
    data, "round_trip/", fmt="lobster",
    config=result.config, ctx=RunContext(trading_date="2012-06-21"),
)

Plotly and Bokeh (interactive)

plot() accepts backend="plotly" for interactive figures with zoom, pan, and hover tooltips. Plotly is an optional dependency:

pip install "ob-analytics[interactive]"
from ob_analytics import Pipeline, sample_csv_path

result = Pipeline().run(sample_csv_path())

# col_bias is a power-law gamma: 1.0 (default) is linear so high-volume walls
# stand out; 0.1 brightens thin levels to expose near-touch structure in
# heavy-tailed books; <= 0 selects a log scale.
fig = result.plot("depth_heatmap", backend="plotly", col_bias=0.1)
fig.show()
fig.write_html("depth.html")

backend="bokeh" (with the [bokeh] extra: pip install "ob-analytics[bokeh]") renders the core concepts — trade_tape, depth_heatmap, book_snapshot, depth_chart — the same way, for Bokeh / Panel server dashboards and streaming views:

fig = result.plot("depth_heatmap", backend="bokeh", col_bias=0.1)

To step through the book over part of a run, draw the book_replay concept on Plotly. Drag the slider or press a play button, and click a trade to see the book just before it:

fig = result.plot("book_replay", "L2", backend="plotly")
fig.write_html("replay.html")

See Book replay for the speeds, the price range and the L3 face.

Whole new backends can be registered by module path:

from ob_analytics.visualization import register_plot_backend
register_plot_backend("altair", "my_package._altair_backend")