Skip to content

The toy session

Every concept in this tutorial is introduced on a toy order-book session you can verify with mental arithmetic before it is shown on real data: 24 events, 12 orders, 5 trades, one synthetic minute, prices 98–103 around a mid of 100, sizes 1 to 3. It ships in the package — toy_events() and toy_trades() return ordinary DataFrames in exactly the layout every real loader produces, so everything you do with them works unchanged on a 300,000-event capture.

The orders, and what happens to each:

Actor Order Outcome Classified as
Alice bid 2 @ 99 (t=0) filled fully by Sam's sweep (t=56) resting-limit
Bob ask 3 @ 101 (t=2) filled by Frank (1) then Hana (2) resting-limit
Chen bid 1 @ 98 (t=5) still in the book at the end resting-limit
Ivy bid 2 @ 99 (t=6) queued behind Alice; only 1 fills (t=57) resting-limit
Dana bid 3 @ 98 (t=8) cancelled (t=40) flashed-limit
Erin ask 2 @ 102 (t=12) survives resting-limit
Frank market buy 1 (t=20) crosses the spread, fills against Bob market
Gus ask 2 @ 103 (t=35) survives resting-limit
Eve bid 1 @ 100 (t=45.0) pulled after 800 ms — a flash flashed-limit
Hana bid 3 @ 101 (t=48) takes 2 from Bob, rests 1, later hit market-limit
Iris market sell 1 (t=52) hits Hana's resting order market
Sam market sell 3 (t=56) sweeps the 99 queue: Alice fully, Ivy half market
%matplotlib inline
from ob_analytics import toy_events, toy_trades

events = toy_events()
trades = toy_trades()
events.head(6)[["event_id", "actor", "action", "direction", "price", "volume", "fill"]]
event_id actor action direction price volume fill
0 1 Alice created bid 99.0 2.0 0.0
1 2 Bob created ask 101.0 3.0 0.0
2 3 Chen created bid 98.0 1.0 0.0
3 4 Ivy created bid 99.0 2.0 0.0
4 5 Dana created bid 98.0 3.0 0.0
5 6 Erin created ask 102.0 2.0 0.0

The stream replayed: the trade tape on top shows only the five executions — the effects. The strip below shows the causes: the full book every five seconds, each order labeled, queued in arrival order (first arrival nearest the axis). Watch Ivy line up behind Alice at 99 (t=10), Dana vanish at t=40, Eve's flash exist for exactly one frame (t=45) — moving the mid without a single trade — and Sam's sweep leave only half of Ivy standing (t=60).

import matplotlib.pyplot as plt
import pandas as pd

from _docs_theme import DOCS_THEME, plot_book_keyframes
from ob_analytics.analytics import set_order_types
from ob_analytics.depth import depth_metrics, get_spread, price_level_volume
from ob_analytics.visualization import plot, prepare

classified = set_order_types(events, trades)
spread = get_spread(depth_metrics(price_level_volume(classified)))

fig = plt.figure(figsize=(17, 8))
gs = fig.add_gridspec(2, 13, height_ratios=[1.5, 1.0], hspace=0.3, wspace=0.15)
ax_tape = fig.add_subplot(gs[0, :])
plot(
    "trade_tape",
    level="L2",
    ax=ax_tape,
    theme=DOCS_THEME,
    **prepare.trades(trades, spread=spread),
)
# The face steps the mid natively; darken it for the tutorial and give it
# a legend entry so the reference line is unmissable next to the gridlines.
for i, line in enumerate(ax_tape.get_lines()):
    line.set_color("#333333")
    line.set_linewidth(1.6)
    line.set_alpha(1.0)
    if i == 0:
        line.set_label("mid price")
t0 = events["timestamp"].iloc[0]
for k in range(0, 61, 5):
    ax_tape.axvline(t0 + pd.Timedelta(seconds=k), color="#e3e3e3", lw=0.5, zorder=0)
# Share the keyframes' price frame so the panels correspond level for level
# (the tape would otherwise autoscale to the traded range and hide 102/103).
ax_tape.set_ylim(events["price"].min() - 0.6, events["price"].max() + 0.6)
ax_tape.set_yticks(sorted(events["price"].unique()))
ax_tape.margins(x=0.03)
ax_tape.legend(loc="lower left", framealpha=0.9)
ax_tape.set_title("The toy session: trades above, the book that caused them below")

key_axes = [fig.add_subplot(gs[1, i]) for i in range(13)]
for axk in key_axes[1:]:
    axk.sharey(key_axes[0])
plot_book_keyframes(classified, trades, ax_row=key_axes)
for axk in key_axes[1:]:
    axk.tick_params(labelleft=False)
/home/runner/work/ob-analytics/ob-analytics/ob_analytics/visualization/_matplotlib.py:261: UserWarning: This figure includes Axes that are not compatible with tight_layout, so results might be incorrect.
  fig.tight_layout()

png

Twelve orders, four classes. set_order_types assigns each order its type from the raw events — with these twelve, every classification is checkable by eye against the table above:

classified.groupby("actor", observed=True)["type"].first().sort_values()
actor
Dana     flashed-limit
Eve      flashed-limit
Alice    resting-limit
Bob      resting-limit
Chen     resting-limit
Erin     resting-limit
Gus      resting-limit
Ivy      resting-limit
Hana      market-limit
Frank           market
Iris            market
Sam             market
Name: type, dtype: category
Categories (6, str): ['unknown' < 'pre-existing' < 'flashed-limit' < 'resting-limit' < 'market-limit' < 'market']

The same mechanics at full scale

Everything above transfers unchanged to real data. The bundled sample is a ~30-minute Bitstamp BTC/USD capture — ~314,000 events instead of 24, thousands of price levels instead of five. The depth heatmap below (zoomed to the capture's busiest ten minutes) is nothing but the keyframe strip compressed into pixels: one column per instant, one row per price, brightness = resting volume.

from ob_analytics import Pipeline, sample_csv_path

from _docs_theme import plot_sample_heatmap

result = Pipeline().run(sample_csv_path())
fig = plot_sample_heatmap(result, col_bias=0.4)

png

Next: chapter 1 starts from zero — what a price actually is, what an exchange does, and why the number on a brokerage app was an order book all along.