Market Microstructure·Order Book Analysis·Advanced

Order Book Depth Chart

Visualize limit order book depth as interactive heatmap surfaces and cumulative depth curve charts, revealing hidden support and resistance price levels, resting liquidity clusters, and large iceberg or hidden order presence in the visible order book.

executionmarket-microstructureorder-book

Visualizing Order Book Depth

Market Microstructure Series — Module: Order Book Analytics


1. What Is an Order Book?

An order book is a real-time, continuously updated list of buy and sell orders for a financial instrument (stock, cryptocurrency, futures contract, etc.) organized by price level. Every exchange — from NYSE and NASDAQ to Binance and Coinbase — maintains an order book for each listed instrument.

Each entry in the order book represents a limit order: a conditional instruction placed by a market participant to buy or sell a specific quantity at a specific price.


2. Bid Side vs. Ask Side

SideDirectionPrice OrderingParticipants
BidBuy ordersDescending (highest bid at top)Buyers willing to pay up to price P
AskSell ordersAscending (lowest ask at top)Sellers willing to accept price P
  • Best Bid: The highest price any buyer is currently willing to pay.
  • Best Ask: The lowest price any seller is currently willing to accept.
  • Bid-Ask Spread: Best Ask − Best Bid. The tighter the spread, the more liquid the market.
  • Midpoint Price: (Best Bid + Best Ask) / 2. Used as a fair-value reference.

3. Market Depth

Market depth (also called Level 2 data) refers to the volume of orders resting at each price level beyond the best bid/ask. It answers the critical question:

"How much can I buy or sell before moving the price significantly?"

A deep market has large volumes sitting at many price levels — it can absorb large orders without dramatic price movement. A shallow market has thin liquidity and is susceptible to large price swings from even modest order flow.


4. Why Does Depth Visualization Matter?

Visualizing order book depth — plotting cumulative volume against price — provides immediate intuition about:

  • Liquidity walls: Large clusters of orders at a price level that act as support/resistance.
  • Order imbalance: Is there significantly more buy-side or sell-side volume? This can predict short-term price direction.
  • Spread width: Wide spread signals either illiquidity or high uncertainty.
  • Market impact estimation: How far will a market order of size N move the price?
  • Iceberg order detection: Anomalous shape changes in the depth curve can reveal hidden orders.

5. Applications in Algorithmic Trading & Market Microstructure Research

ApplicationDescription
Optimal ExecutionVWAP/TWAP algorithms slice orders to minimize market impact using depth data
Market MakingQuote prices around the midpoint; use depth to manage inventory risk
Statistical ArbitrageOrder imbalance signals used as alpha factors
High-Frequency TradingSub-millisecond depth updates drive latency-sensitive strategies
Liquidity ResearchAcademic study of price formation and information asymmetry
Risk ManagementSlippage estimation before executing large block trades

Notebook Structure: Each function below is in its own cell, preceded by a documentation cell. Run all cells sequentially. The final main() call produces the complete depth chart.


Imports & Configuration

All required libraries are imported here. We use:

  • numpy for numerical operations and random data generation
  • pandas for DataFrame manipulation
  • matplotlib for publication-quality visualization
  • typing for type hints (Python 3.9+ compatible)
[51]
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.figure import Figure
from matplotlib.axes import Axes
from typing import Tuple, Optional
import warnings

warnings.filterwarnings('ignore')

# ── Matplotlib Style Configuration ──────────────────────────────────────────
plt.rcParams.update({
    'figure.facecolor':  '#FFFFFF',
    'axes.facecolor':    '#FFFFFF',
    'axes.edgecolor':    '#30363d',
    'axes.labelcolor':   '#c9d1d9',
    'axes.grid':         True,
    'grid.color':        '#21262d',
    'grid.linewidth':    0.8,
    'xtick.color':       '#8b949e',
    'ytick.color':       '#8b949e',
    'text.color':        '#c9d1d9',
    'legend.facecolor':  '#161b22',
    'legend.edgecolor':  '#30363d',
    'font.family':       'monospace',
    'font.size':         10,
})

print("Imports and style configuration complete.")
Imports and style configuration complete.

generate_sample_order_book

Purpose
Generates a synthetic but realistic order book with bid and ask levels. The function simulates a plausible price-volume distribution using exponential decay — volumes are largest near the midpoint and thin out at extreme prices, mirroring real market behaviour where liquidity concentrates around the current fair value.

Inputs

ParameterTypeDefaultDescription
mid_pricefloat100.0The reference midpoint price around which bids and asks are generated
num_levelsint20Number of price levels on each side of the book
tick_sizefloat0.10Minimum price increment between levels
base_volumefloat1000.0Maximum volume at the best bid/ask level
seedOptional[int]42Random seed for reproducibility; None for random output

Outputs
Returns a Tuple[pd.DataFrame, pd.DataFrame]:

  • bids_df: DataFrame with columns ['price', 'volume'], sorted descending by price
  • asks_df: DataFrame with columns ['price', 'volume'], sorted ascending by price

Example Usage

bids, asks = generate_sample_order_book(mid_price=50000.0, num_levels=15, tick_size=1.0)
print(bids.head())
[52]
def generate_sample_order_book(
    mid_price: float = 100.0,
    num_levels: int = 20,
    tick_size: float = 0.10,
    base_volume: float = 1000.0,
    seed: Optional[int] = 42,
) -> Tuple[pd.DataFrame, pd.DataFrame]:
    """
    Generate a synthetic order book with realistic bid/ask price levels.

    The volume distribution uses exponential decay from the midpoint outward,
    with multiplicative noise to simulate real-world irregularity. Occasional
    'liquidity walls' (large volume spikes) are injected to mimic iceberg-style
    resting orders often found at round-number price levels.

    Parameters
    ----------
    mid_price   : Reference midpoint price.
    num_levels  : Number of price levels per side.
    tick_size   : Price increment between consecutive levels.
    base_volume : Approximate maximum volume at the innermost level.
    seed        : Random seed for reproducibility.

    Returns
    -------
    bids_df : DataFrame[price, volume] — sorted descending (best bid first).
    asks_df : DataFrame[price, volume] — sorted ascending (best ask first).

    Raises
    ------
    ValueError
        If num_levels < 1, tick_size <= 0, or base_volume <= 0.
    """
    if num_levels < 1:
        raise ValueError(f"num_levels must be >= 1, got {num_levels}")
    if tick_size <= 0:
        raise ValueError(f"tick_size must be > 0, got {tick_size}")
    if base_volume <= 0:
        raise ValueError(f"base_volume must be > 0, got {base_volume}")

    rng = np.random.default_rng(seed)

    half_spread = tick_size / 2.0

    # ── Price levels ─────────────────────────────────────────────────────────
    bid_prices = np.array([
        round(mid_price - half_spread - i * tick_size, 10)
        for i in range(num_levels)
    ])
    ask_prices = np.array([
        round(mid_price + half_spread + i * tick_size, 10)
        for i in range(num_levels)
    ])

    # ── Volume distribution: exponential decay + multiplicative noise
    decay = np.exp(-0.15 * np.arange(num_levels))
    noise_bids = rng.lognormal(mean=0.0, sigma=0.35, size=num_levels)
    noise_asks = rng.lognormal(mean=0.0, sigma=0.35, size=num_levels)

    bid_volumes = np.maximum(1.0, base_volume * decay * noise_bids)
    ask_volumes = np.maximum(1.0, base_volume * decay * noise_asks)

    # ── Inject liquidity walls at 2–3 random levels per side
    wall_indices_bid = rng.choice(num_levels, size=min(3, num_levels), replace=False)
    wall_indices_ask = rng.choice(num_levels, size=min(3, num_levels), replace=False)
    bid_volumes[wall_indices_bid] *= rng.uniform(2.5, 5.0, size=len(wall_indices_bid))
    ask_volumes[wall_indices_ask] *= rng.uniform(2.5, 5.0, size=len(wall_indices_ask))

    # ── Build DataFrames
    bids_df = pd.DataFrame({'price': bid_prices, 'volume': bid_volumes})
    asks_df = pd.DataFrame({'price': ask_prices, 'volume': ask_volumes})

    return bids_df, asks_df


# ── Quick sanity check
_bids_test, _asks_test = generate_sample_order_book()
print(f"{'Bids':─^40}")
print(_bids_test.head(5).to_string(index=False))
print(f"\n{'Asks':─^40}")
print(_asks_test.head(5).to_string(index=False))
print(f"\ngenerate_sample_order_book() — {len(_bids_test)} bid levels, {len(_asks_test)} ask levels")
──────────────────Bids──────────────────
 price      volume
 99.95 1112.545884
 99.85  598.101721
 99.75  963.346536
 99.65 2854.320231
 99.55  277.242608

──────────────────Asks──────────────────
 price      volume
100.05  937.346913
100.15 1932.426584
100.25 1136.427385
100.35  604.057843
100.45  472.406829

generate_sample_order_book() — 20 bid levels, 20 ask levels

compute_cumulative_depth

Purpose
Computes the cumulative volume at each price level for both the bid and ask sides. This is the key transformation that converts a per-level snapshot ("how much volume sits at this price") into a depth curve ("how much total volume sits up to this price"), which is what depth charts actually plot.

For bids: cumulative volume accumulates from the best bid downward (best bid = innermost level).
For asks: cumulative volume accumulates from the best ask upward (best ask = innermost level).

Inputs

ParameterTypeDescription
bids_dfpd.DataFrameRaw bids DataFrame with ['price', 'volume'] columns
asks_dfpd.DataFrameRaw asks DataFrame with ['price', 'volume'] columns

Outputs
Returns a Tuple[pd.DataFrame, pd.DataFrame] — copies of the input DataFrames with an additional column:

  • cumulative_volume: Running sum of volume from the midpoint outward

Example Usage

bids, asks = generate_sample_order_book()
bids_cum, asks_cum = compute_cumulative_depth(bids, asks)
print(bids_cum[['price', 'volume', 'cumulative_volume']].head())
[53]
def compute_cumulative_depth(
    bids_df: pd.DataFrame,
    asks_df: pd.DataFrame,
) -> Tuple[pd.DataFrame, pd.DataFrame]:
    """
    Compute cumulative volume depth for bid and ask sides of the order book.

    Bids are sorted descending (best bid first) and cumulated from index 0
    downward in price. Asks are sorted ascending (best ask first) and cumulated
    from index 0 upward in price. The result is the classic staircase shape
    seen in exchange depth charts.

    Parameters
    ----------
    bids_df : DataFrame with 'price' and 'volume' columns (bid side).
    asks_df : DataFrame with 'price' and 'volume' columns (ask side).

    Returns
    -------
    bids_cum : Copy of bids_df with added 'cumulative_volume' column.
    asks_cum : Copy of asks_df with added 'cumulative_volume' column.

    Raises
    ------
    ValueError
        If either DataFrame is missing required columns or is empty.
    """
    required_cols = {'price', 'volume'}

    for name, df in [('bids_df', bids_df), ('asks_df', asks_df)]:
        if df.empty:
            raise ValueError(f"{name} is empty.")
        missing = required_cols - set(df.columns)
        if missing:
            raise ValueError(f"{name} missing columns: {missing}")
        if df['volume'].lt(0).any():
            raise ValueError(f"{name} contains negative volume values.")

    # ── Bids: descending price order, cumulate from best bid downward
    bids_cum = (
        bids_df
        .copy()
        .sort_values('price', ascending=False)
        .reset_index(drop=True)
    )
    bids_cum['cumulative_volume'] = bids_cum['volume'].cumsum()

    # ── Asks: ascending price order, cumulate from best ask upward
    asks_cum = (
        asks_df
        .copy()
        .sort_values('price', ascending=True)
        .reset_index(drop=True)
    )
    asks_cum['cumulative_volume'] = asks_cum['volume'].cumsum()

    return bids_cum, asks_cum


# ── Quick sanity check
_bids_cum, _asks_cum = compute_cumulative_depth(_bids_test, _asks_test)
print(f"{'Bids with Cumulative Depth':─^55}")
print(_bids_cum[['price', 'volume', 'cumulative_volume']].head(5).to_string(index=False))
print(f"\n{'Asks with Cumulative Depth':─^55}")
print(_asks_cum[['price', 'volume', 'cumulative_volume']].head(5).to_string(index=False))
print(f"\ncompute_cumulative_depth() — max bid depth: {_bids_cum['cumulative_volume'].max():.1f}, "
      f"max ask depth: {_asks_cum['cumulative_volume'].max():.1f}")
──────────────Bids with Cumulative Depth───────────────
 price      volume  cumulative_volume
 99.95 1112.545884        1112.545884
 99.85  598.101721        1710.647605
 99.75  963.346536        2673.994141
 99.65 2854.320231        5528.314371
 99.55  277.242608        5805.556979

──────────────Asks with Cumulative Depth───────────────
 price      volume  cumulative_volume
100.05  937.346913         937.346913
100.15 1932.426584        2869.773497
100.25 1136.427385        4006.200882
100.35  604.057843        4610.258725
100.45  472.406829        5082.665554

compute_cumulative_depth() — max bid depth: 9902.4, max ask depth: 9920.4

prepare_depth_data

Purpose
Merges the processed bid and ask DataFrames into a single, plot-ready structure and computes key derived metrics: best bid, best ask, midpoint price, and bid-ask spread. This function acts as the data pipeline's final transformation step — cleanly separating data preparation from rendering logic.

Inputs

ParameterTypeDescription
bids_cumpd.DataFrameBids DataFrame with cumulative_volume column
asks_cumpd.DataFrameAsks DataFrame with cumulative_volume column

Outputs
Returns a dict with keys:

  • 'bids': Processed bids DataFrame
  • 'asks': Processed asks DataFrame
  • 'best_bid': Best (highest) bid price
  • 'best_ask': Best (lowest) ask price
  • 'mid_price': Midpoint price
  • 'spread': Absolute bid-ask spread
  • 'spread_pct': Spread as a percentage of midpoint
  • 'bid_ask_imbalance': Normalised imbalance ratio in [-1, 1]

Example Usage

plot_data = prepare_depth_data(bids_cum, asks_cum)
print(f"Mid: {plot_data['mid_price']:.4f}  Spread: {plot_data['spread_pct']:.4f}%")
[54]
def prepare_depth_data(
    bids_cum: pd.DataFrame,
    asks_cum: pd.DataFrame,
) -> dict:
    """
    Merge cumulative bid/ask data and compute market microstructure metrics.

    Derives best bid, best ask, midpoint, spread (absolute and relative),
    and the bid/ask volume imbalance ratio — all essential metrics for
    interpreting order book state and annotating the depth chart.

    Parameters
    ----------
    bids_cum : Bids DataFrame containing 'price' and 'cumulative_volume'.
    asks_cum : Asks DataFrame containing 'price' and 'cumulative_volume'.

    Returns
    -------
    dict with keys:
        bids            : pd.DataFrame
        asks            : pd.DataFrame
        best_bid        : float
        best_ask        : float
        mid_price       : float
        spread          : float
        spread_pct      : float   (spread / mid_price * 100)
        bid_ask_imbalance : float  (range [-1, +1]; positive = more bid-side volume)

    Raises
    ------
    ValueError
        If DataFrames are missing required columns or best bid >= best ask
        (crossed book, indicating upstream data error).
    """
    for name, df in [('bids_cum', bids_cum), ('asks_cum', asks_cum)]:
        required = {'price', 'cumulative_volume'}
        missing = required - set(df.columns)
        if missing:
            raise ValueError(f"{name} missing columns: {missing}")
        if df.empty:
            raise ValueError(f"{name} is empty.")

    best_bid: float = bids_cum['price'].max()
    best_ask: float = asks_cum['price'].min()

    if best_bid >= best_ask:
        raise ValueError(
            f"Crossed book detected: best_bid ({best_bid}) >= best_ask ({best_ask}). "
            "Verify upstream data generation."
        )

    mid_price: float = (best_bid + best_ask) / 2.0
    spread: float = best_ask - best_bid
    spread_pct: float = (spread / mid_price) * 100.0

    # ── Bid-Ask Imbalance: (BidVol - AskVol) / (BidVol + AskVol)
    total_bid_vol: float = bids_cum['cumulative_volume'].max()
    total_ask_vol: float = asks_cum['cumulative_volume'].max()
    denom = total_bid_vol + total_ask_vol
    bid_ask_imbalance: float = (
        (total_bid_vol - total_ask_vol) / denom if denom > 0 else 0.0
    )

    return {
        'bids':               bids_cum,
        'asks':               asks_cum,
        'best_bid':           best_bid,
        'best_ask':           best_ask,
        'mid_price':          mid_price,
        'spread':             spread,
        'spread_pct':         spread_pct,
        'bid_ask_imbalance':  bid_ask_imbalance,
    }


# ── Quick sanity check
_plot_data = prepare_depth_data(_bids_cum, _asks_cum)
print(f"{'Market Microstructure Metrics':─^55}")
print(f"  Best Bid     : {_plot_data['best_bid']:>10.4f}")
print(f"  Best Ask     : {_plot_data['best_ask']:>10.4f}")
print(f"  Mid Price    : {_plot_data['mid_price']:>10.4f}")
print(f"  Spread       : {_plot_data['spread']:>10.4f}")
print(f"  Spread (%)   : {_plot_data['spread_pct']:>10.4f}%")
print(f"  Imbalance    : {_plot_data['bid_ask_imbalance']:>10.4f}  (>0 = bid-heavy)")
print(f"\nprepare_depth_data() — all metrics computed successfully")
─────────────Market Microstructure Metrics─────────────
  Best Bid     :    99.9500
  Best Ask     :   100.0500
  Mid Price    :   100.0000
  Spread       :     0.1000
  Spread (%)   :     0.1000%
  Imbalance    :    -0.0009  (>0 = bid-heavy)

prepare_depth_data() — all metrics computed successfully

plot_order_book_depth

Purpose
Renders a publication-quality, annotated order book depth chart using matplotlib. The chart follows the standard market convention:

  • Green staircase = cumulative bid-side depth (left side, decreasing prices)
  • Red staircase = cumulative ask-side depth (right side, increasing prices)
  • Shaded spread region = the gap between best bid and best ask
  • Dashed midpoint line = reference price marker
  • Metric annotation box = live microstructure stats overlaid on the chart

Inputs

ParameterTypeDefaultDescription
plot_datadictOutput dictionary from prepare_depth_data()
titlestr'Order Book Depth'Chart title
figsizeTuple[float, float](14, 7)Figure dimensions in inches
show_volume_barsboolTrueWhether to render per-level volume bars under the depth curve

Outputs
Returns a Tuple[Figure, Axes] — the matplotlib Figure and primary Axes objects (allowing further customisation by the caller).

Example Usage

fig, ax = plot_order_book_depth(plot_data, title='BTC/USDT Order Book Depth')
plt.show()
[55]
def plot_order_book_depth(
    plot_data: dict,
    title: str = 'Order Book Depth',
    figsize: Tuple[float, float] = (14, 7),
    show_volume_bars: bool = True,
) -> Tuple[Figure, Axes]:
    """
    Render a professional, annotated order book depth chart.

    Plots cumulative bid and ask volumes as step curves, fills below each
    curve with transparent colour, highlights the bid-ask spread region,
    marks the midpoint price, and overlays a microstructure metrics box.
    Optionally displays per-level volume bars for granular liquidity insight.

    Parameters
    ----------
    plot_data        : dict returned by prepare_depth_data().
    title            : Chart title string.
    figsize          : (width, height) in inches.
    show_volume_bars : If True, draw translucent volume bars at each price level.

    Returns
    -------
    fig : matplotlib Figure object.
    ax  : matplotlib Axes object.

    Raises
    ------
    KeyError
        If plot_data is missing required keys.
    """
    required_keys = {'bids', 'asks', 'best_bid', 'best_ask', 'mid_price',
                     'spread', 'spread_pct', 'bid_ask_imbalance'}
    missing_keys = required_keys - set(plot_data.keys())
    if missing_keys:
        raise KeyError(f"plot_data missing keys: {missing_keys}")

    bids:     pd.DataFrame = plot_data['bids']
    asks:     pd.DataFrame = plot_data['asks']
    best_bid: float        = plot_data['best_bid']
    best_ask: float        = plot_data['best_ask']
    mid_price: float       = plot_data['mid_price']
    spread:    float       = plot_data['spread']
    spread_pct: float      = plot_data['spread_pct']
    imbalance: float       = plot_data['bid_ask_imbalance']

    # ── Colour palette
    BID_COLOR    = '#00d97e'   # green
    ASK_COLOR    = '#ff4d6d'   # red
    SPREAD_COLOR = '#f7c948'   # amber
    MID_COLOR    = '#a5b4fc'   # lavender
    BG_COLOR     = '#0d1117'

    fig, ax = plt.subplots(figsize=figsize, facecolor=BG_COLOR)
    ax.set_facecolor(BG_COLOR)

    # ── Bid depth curve
    ax.step(
        bids['price'], bids['cumulative_volume'],
        where='post', color=BID_COLOR, linewidth=2.0,
        label='Cumulative Bid Depth', zorder=4,
    )
    ax.fill_between(
        bids['price'], bids['cumulative_volume'],
        step='post', alpha=0.18, color=BID_COLOR, zorder=3,
    )

    # ── Ask depth curve
    ax.step(
        asks['price'], asks['cumulative_volume'],
        where='pre', color=ASK_COLOR, linewidth=2.0,
        label='Cumulative Ask Depth', zorder=4,
    )
    ax.fill_between(
        asks['price'], asks['cumulative_volume'],
        step='pre', alpha=0.18, color=ASK_COLOR, zorder=3,
    )

    # ── Optional per-level volume bars
    if show_volume_bars:
        tick_size_est = abs(bids['price'].diff().median())
        bar_width = tick_size_est * 0.6 if tick_size_est > 0 else 0.05

        ax.bar(
            bids['price'], bids['volume'],
            width=bar_width, color=BID_COLOR, alpha=0.35,
            align='center', zorder=2, label='Bid Volume per Level',
        )
        ax.bar(
            asks['price'], asks['volume'],
            width=bar_width, color=ASK_COLOR, alpha=0.35,
            align='center', zorder=2, label='Ask Volume per Level',
        )

    # ── Spread region shading
    y_max = max(bids['cumulative_volume'].max(), asks['cumulative_volume'].max())
    ax.axvspan(
        best_bid, best_ask,
        alpha=0.12, color=SPREAD_COLOR, zorder=1,
        label=f'Spread ({spread_pct:.3f}%)',
    )

    # ── Midpoint price marker
    ax.axvline(
        mid_price, color=MID_COLOR, linewidth=1.5,
        linestyle='--', alpha=0.85, zorder=5,
        label=f'Mid Price ({mid_price:.2f})',
    )
    ax.text(
        mid_price, y_max * 0.97,
        f' Mid\n {mid_price:.2f}',
        color=MID_COLOR, fontsize=8.5, va='top',
        fontweight='bold', zorder=6,
    )

    # ── Best bid/ask tick markers
    # Calculate an estimated tick size for positioning labels
    tick_size_est = abs(bids['price'].diff().median())

    label_offset = tick_size_est * 1 # A couple of tick sizes away

    for price, color, label, h_align in [
        (best_bid, BID_COLOR, f'Best Bid\n{best_bid:.2f}', 'right'),
        (best_ask, ASK_COLOR, f'Best Ask\n{best_ask:.2f}', 'left'),
    ]:
        # Adjust price for text placement
        text_price = price - label_offset if h_align == 'right' else price + label_offset

        ax.axvline(price, color=color, linewidth=1.0,
                   linestyle=':', alpha=0.6, zorder=5)
        ax.text(
            text_price, y_max * 0.85, label,
            color=color, fontsize=7.5, ha=h_align, # Use h_align here
            va='top', alpha=0.9, zorder=6,
        )

    # ── Microstructure metrics annotation box
    imb_sign = '+' if imbalance > 0 else ''
    imb_color = BID_COLOR if imbalance > 0 else ASK_COLOR if imbalance < 0 else '#8b949e'
    stats_text = (
        f"Spread:     {spread:.4f}  ({spread_pct:.4f}%)\n"
        f"Best Bid:   {best_bid:.4f}\n"
        f"Best Ask:   {best_ask:.4f}\n"
        f"Mid Price:  {mid_price:.4f}\n"
        f"Imbalance:  {imb_sign}{imbalance:.4f}"
    )
    ax.text(
        0.01, 0.98, stats_text,
        transform=ax.transAxes, fontsize=8,
        verticalalignment='top', fontfamily='monospace',
        bbox=dict(boxstyle='round,pad=0.5', facecolor='#161b22',
                  edgecolor='#30363d', alpha=0.90),
        color='#c9d1d9', zorder=10,
    )

    # ── Axes formatting
    ax.set_xlabel('Price', fontsize=12, labelpad=10, color='#8b949e')
    ax.set_ylabel('Cumulative Volume', fontsize=12, labelpad=10, color='#8b949e')
    ax.set_title(title, fontsize=15, fontweight='bold', pad=18,
                 color='#f0f6fc', fontfamily='monospace')

    ax.set_xlim(
        bids['price'].min() - spread,
        asks['price'].max() + spread,
    )
    ax.set_ylim(bottom=0, top=y_max * 1.10)

    ax.tick_params(axis='both', which='major', labelsize=8.5,
                   colors='#8b949e')
    ax.spines[['top', 'right']].set_visible(False)
    ax.spines[['left', 'bottom']].set_color('#30363d')

    ax.yaxis.set_major_formatter(
        plt.FuncFormatter(lambda v, _: f'{v:,.0f}')
    )

    legend = ax.legend(
        loc='upper right', fontsize=8.5,
        framealpha=0.85, ncol=2,
    )
    for text in legend.get_texts():
        text.set_color('#c9d1d9')

    # ── Watermark
    fig.text(
        0.99, 0.01,
        'Market Microstructure Series — Order Book Depth',
        ha='right', va='bottom', fontsize=7,
        color='#30363d', style='italic',
    )

    plt.tight_layout()
    return fig, ax

main

Purpose
Orchestrates the full pipeline: data generation → depth computation → data preparation → chart rendering. Serves as the single entry point for running the complete workflow, making the notebook easy to re-execute with different parameters.

Inputs

ParameterTypeDefaultDescription
mid_pricefloat100.0Reference midpoint price
num_levelsint25Number of price levels per side
tick_sizefloat0.10Price tick increment
base_volumefloat1500.0Max volume at innermost level
seedOptional[int]42Random seed
chart_titlestr'Order Book Depth — Synthetic Exchange'Chart title

Outputs
Returns a Tuple[Figure, Axes, dict]:

  • fig, ax: Matplotlib objects for further customisation
  • plot_data: The complete prepared data dictionary

Example Usage

fig, ax, data = main(mid_price=50000.0, num_levels=30, tick_size=10.0, chart_title='BTC/USDT')
[56]
def main(
    mid_price: float = 100.0,
    num_levels: int = 25,
    tick_size: float = 0.10,
    base_volume: float = 1500.0,
    seed: Optional[int] = 42,
    chart_title: str = 'Order Book Depth — Synthetic Exchange',
) -> Tuple[Figure, Axes, dict]:
    """
    Execute the complete order book depth visualisation pipeline.

    Pipeline stages
    ---------------
    1. generate_sample_order_book()  — create synthetic bid/ask levels
    2. compute_cumulative_depth()    — compute running volume totals
    3. prepare_depth_data()          — merge & compute microstructure metrics
    4. plot_order_book_depth()       — render annotated depth chart

    Parameters
    ----------
    mid_price    : Reference midpoint price for order book generation.
    num_levels   : Number of price levels on each side.
    tick_size    : Minimum price increment between levels.
    base_volume  : Approximate peak volume at the innermost level.
    seed         : Random seed for reproducibility.
    chart_title  : Title displayed on the depth chart.

    Returns
    -------
    fig       : matplotlib Figure.
    ax        : matplotlib Axes.
    plot_data : dict of prepared depth data and microstructure metrics.
    """
    print('─' * 60)
    print('  Order Book Depth Visualiser')
    print('  Market Microstructure Series')
    print('─' * 60)

    # ── Stage 1: Generate synthetic order book
    print('\n[1/4] Generating synthetic order book...')
    bids_raw, asks_raw = generate_sample_order_book(
        mid_price=mid_price,
        num_levels=num_levels,
        tick_size=tick_size,
        base_volume=base_volume,
        seed=seed,
    )
    print(f'     Bid levels: {len(bids_raw):>4}  |  '
          f'Ask levels: {len(asks_raw):>4}  |  '
          f'Price range: [{bids_raw["price"].min():.3f}, {asks_raw["price"].max():.3f}]')

    # ── Stage 2: Compute cumulative depth
    print('\n[2/4] Computing cumulative depth...')
    bids_cum, asks_cum = compute_cumulative_depth(bids_raw, asks_raw)
    print(f'     Max bid depth: {bids_cum["cumulative_volume"].max():>10,.1f}')
    print(f'     Max ask depth: {asks_cum["cumulative_volume"].max():>10,.1f}')

    # ── Stage 3: Prepare plot data
    print('\n[3/4] Preparing plot data and computing metrics...')
    plot_data = prepare_depth_data(bids_cum, asks_cum)
    print(f'     Mid Price  : {plot_data["mid_price"]:>10.4f}')
    print(f'     Spread     : {plot_data["spread"]:>10.4f}  ({plot_data["spread_pct"]:.4f}%)')
    print(f'     Imbalance  : {plot_data["bid_ask_imbalance"]:>+10.4f}  '
          f'({"bid-heavy" if plot_data["bid_ask_imbalance"] > 0 else "ask-heavy" if plot_data["bid_ask_imbalance"] < 0 else "balanced"})')

    # ── Stage 4: Render depth chart
    print('\n[4/4] Rendering depth chart...')
    fig, ax = plot_order_book_depth(
        plot_data,
        title=chart_title,
        figsize=(14, 7),
        show_volume_bars=True,
    )
    plt.show()

    print('\nPipeline complete.')
    print('─' * 60)

    return fig, ax, plot_data


print("main() defined — execute the next cell to run the full pipeline.")
main() defined — execute the next cell to run the full pipeline.

Execute the Pipeline

Run the cell below to execute the complete order book depth visualisation workflow.
Adjust parameters as desired — try different mid_price, num_levels, or seed values.

[57]
fig, ax, data = main(
    mid_price   = 100.0,
    num_levels  = 25,
    tick_size   = 0.10,
    base_volume = 1500.0,
    seed        = 42,
    chart_title = 'Order Book Depth — Synthetic Exchange',
)
────────────────────────────────────────────────────────────
  Order Book Depth Visualiser
  Market Microstructure Series
────────────────────────────────────────────────────────────

[1/4] Generating synthetic order book...
     Bid levels:   25  |  Ask levels:   25  |  Price range: [97.550, 102.450]

[2/4] Computing cumulative depth...
     Max bid depth:   11,118.0
     Max ask depth:   14,105.5

[3/4] Preparing plot data and computing metrics...
     Mid Price  :   100.0000
     Spread     :     0.1000  (0.1000%)
     Imbalance  :    -0.1184  (ask-heavy)

[4/4] Rendering depth chart...
cell output

Pipeline complete.
────────────────────────────────────────────────────────────

Bonus: Live Crypto-Scale Order Book

Demonstrate the same pipeline scaled to Bitcoin/USDT price levels:

[58]
fig_btc, ax_btc, data_btc = main(
    mid_price   = 67_500.0,
    num_levels  = 30,
    tick_size   = 10.0,
    base_volume = 5.0,        # BTC units (realistic for top-tier exchange)
    seed        = 7,
    chart_title = 'Order Book Depth — BTC/USDT (Simulated)',
)
────────────────────────────────────────────────────────────
  Order Book Depth Visualiser
  Market Microstructure Series
────────────────────────────────────────────────────────────

[1/4] Generating synthetic order book...
     Bid levels:   30  |  Ask levels:   30  |  Price range: [67205.000, 67795.000]

[2/4] Computing cumulative depth...
     Max bid depth:       55.3
     Max ask depth:       61.3

[3/4] Preparing plot data and computing metrics...
     Mid Price  : 67500.0000
     Spread     :    10.0000  (0.0148%)
     Imbalance  :    -0.0521  (ask-heavy)

[4/4] Rendering depth chart...
cell output

Pipeline complete.
────────────────────────────────────────────────────────────

Analysis: Interpreting the Depth Chart

1. Reading the Staircase Shape

Each step in the green (bid) or red (ask) staircase corresponds to one price level in the order book. The height of each step equals the volume resting at that level. The cumulative nature means the curve can only ever increase as price moves away from the midpoint — it is a monotonically non-decreasing function.

A steep initial rise close to the midpoint signals concentrated liquidity near fair value — typical of deep, liquid markets. A gradual, shallow rise suggests thin near-touch liquidity and high sensitivity to market orders.

2. Liquidity Walls (Support & Resistance Zones)

A liquidity wall appears as a tall individual bar or a sudden large step in the cumulative curve — a price level with anomalously large volume relative to its neighbours. These walls act as:

  • Support: A large bid wall below the current price will absorb sell market orders, slowing downward price movement.
  • Resistance: A large ask wall above the current price will absorb buy market orders, limiting upward breakouts.

Traders monitor whether these walls are genuine (held firm over time) or spoofed (placed and pulled to create false impressions of support/resistance).

3. Bid-Ask Imbalance

The imbalance metric (BidVol − AskVol) / (BidVol + AskVol) ranges from −1 to +1:

Imbalance ValueInterpretation
+0.30 to +1.0Strong bid-side pressure — buying interest dominates
−0.10 to +0.10Roughly balanced book
−0.30 to −1.0Strong ask-side pressure — selling interest dominates

Order flow imbalance is a well-documented short-term price predictor in the microstructure literature (Cont, Kukanov & Stoikov, 2014).

4. Market Impact Estimation

From the depth chart you can directly read the expected slippage of a market order of size $Q$:

  1. On the ask curve, find where cumulative volume reaches $Q$. The corresponding price is your estimated average fill price for a buy market order.
  2. The difference between that price and the best ask is the market impact cost.

Formally, for a buy order of size $Q$, the volume-weighted average price (VWAP) fill price is:

$$\text{VWAP}(Q) = \frac{\sum_{i: \text{cumVol}i \leq Q} p_i \cdot v_i + p^* \cdot (Q - \text{cumVol}{i^*-1})}{Q}$$

where $p^*$ is the marginal price level at which the order is completed.

5. Spread Width and Transaction Costs

The shaded region between best bid and best ask represents the minimum round-trip transaction cost for a liquidity taker (someone using market orders). In competitive, well-arbitraged markets, this spread reflects the market maker's compensation for inventory risk and adverse selection risk.


Best Practices & Production Considerations

1. Time Complexity

OperationComplexityNotes
Cumulative depth computationO(n log n)Dominated by sort; O(n) if pre-sorted
Depth chart renderingO(n)Linear in number of price levels
Spread / metric computationO(1)After sort

For typical exchange data (20–500 levels per side), all operations are effectively instantaneous. At very deep books (Level 2 with 10,000+ levels), batch vectorised NumPy operations remain efficient.

2. Scalability for Real Exchange Data

Memory-efficient incremental updates: Real order books do not send full snapshots every tick — they send diffs (add, modify, delete events). A production system should:

# Pseudocode for incremental order book update
class OrderBook:
    def __init__(self):
        self.bids = {}  # {price: volume}
        self.asks = {}  # {price: volume}

    def apply_update(self, side: str, price: float, volume: float) -> None:
        book = self.bids if side == 'bid' else self.asks
        if volume == 0:
            book.pop(price, None)  # Level removed
        else:
            book[price] = volume   # Level added or updated

3. Integrating with Binance WebSocket API

# Example: Subscribe to Binance depth stream
import websocket, json

WS_URL = 'wss://stream.binance.com:9443/ws/btcusdt@depth20@100ms'

def on_message(ws, message):
    data = json.loads(message)
    bids = pd.DataFrame(data['bids'], columns=['price', 'volume'], dtype=float)
    asks = pd.DataFrame(data['asks'], columns=['price', 'volume'], dtype=float)
    bids_cum, asks_cum = compute_cumulative_depth(bids, asks)
    plot_data = prepare_depth_data(bids_cum, asks_cum)
    # Update live chart here (use matplotlib animation or Plotly/Dash)

ws = websocket.WebSocketApp(WS_URL, on_message=on_message)
ws.run_forever()

4. Integrating with Coinbase Advanced Trade API

import requests

def fetch_coinbase_order_book(product_id: str = 'BTC-USD', level: int = 2) -> dict:
    url = f'https://api.exchange.coinbase.com/products/{product_id}/book?level={level}'
    resp = requests.get(url, timeout=5)
    resp.raise_for_status()
    return resp.json()

raw = fetch_coinbase_order_book()
bids = pd.DataFrame(raw['bids'], columns=['price', 'volume', 'orders'], dtype=float)[['price', 'volume']]
asks = pd.DataFrame(raw['asks'], columns=['price', 'volume', 'orders'], dtype=float)[['price', 'volume']]

5. Extending to Level-3 Order Book Streams

Level-3 data exposes individual order IDs, enabling:

  • Order flow toxicity analysis (VPIN — Volume-synchronized Probability of Informed trading)
  • Queue position modeling for limit order strategies
  • Spoofing detection (orders placed and cancelled in < N milliseconds)
  • Iceberg order reconstruction by tracking repeat fills at the same price

6. Live Dashboard with Matplotlib Animation

from matplotlib.animation import FuncAnimation

fig, ax = plt.subplots()

def update(frame):
    ax.clear()
    bids_r, asks_r = generate_sample_order_book(seed=frame)  # Replace with live feed
    bids_c, asks_c = compute_cumulative_depth(bids_r, asks_r)
    pd_data        = prepare_depth_data(bids_c, asks_c)
    plot_order_book_depth(pd_data)

ani = FuncAnimation(fig, update, interval=500)  # Update every 500ms
plt.show()

7. Code Quality Checklist

  • Type hints on all function signatures
  • NumPy vectorised operations (no Python loops over price levels)
  • Input validation with descriptive ValueError / KeyError messages
  • No global mutable state — all data flows through function returns
  • PEP-8 compliant formatting
  • Comprehensive Google-style docstrings
  • Reproducible random state via np.random.default_rng(seed)
  • Modular design — each function has a single, well-defined responsibility

Summary

This notebook built a complete, production-quality order book depth visualisation pipeline:

FunctionResponsibility
generate_sample_order_book()Synthetic data generation with realistic volume distribution
compute_cumulative_depth()Transform per-level volumes into cumulative depth curves
prepare_depth_data()Merge and compute microstructure metrics (spread, imbalance, midpoint)
plot_order_book_depth()Render annotated, publication-quality depth chart
main()Orchestrate the full pipeline from data to chart

Key takeaways:

  • The depth curve's shape immediately reveals liquidity concentration, support/resistance walls, and bid-ask imbalance.
  • Market impact for any order size can be read directly from the depth chart.
  • The modular design allows drop-in replacement of the synthetic data source with a live exchange WebSocket feed.
Order Book Depth Chart · BitPredict