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.
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
| Side | Direction | Price Ordering | Participants |
|---|---|---|---|
| Bid | Buy orders | Descending (highest bid at top) | Buyers willing to pay up to price P |
| Ask | Sell orders | Ascending (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
| Application | Description |
|---|---|
| Optimal Execution | VWAP/TWAP algorithms slice orders to minimize market impact using depth data |
| Market Making | Quote prices around the midpoint; use depth to manage inventory risk |
| Statistical Arbitrage | Order imbalance signals used as alpha factors |
| High-Frequency Trading | Sub-millisecond depth updates drive latency-sensitive strategies |
| Liquidity Research | Academic study of price formation and information asymmetry |
| Risk Management | Slippage 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:
numpyfor numerical operations and random data generationpandasfor DataFrame manipulationmatplotlibfor publication-quality visualizationtypingfor type hints (Python 3.9+ compatible)
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
| Parameter | Type | Default | Description |
|---|---|---|---|
mid_price | float | 100.0 | The reference midpoint price around which bids and asks are generated |
num_levels | int | 20 | Number of price levels on each side of the book |
tick_size | float | 0.10 | Minimum price increment between levels |
base_volume | float | 1000.0 | Maximum volume at the best bid/ask level |
seed | Optional[int] | 42 | Random 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 priceasks_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())
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
| Parameter | Type | Description |
|---|---|---|
bids_df | pd.DataFrame | Raw bids DataFrame with ['price', 'volume'] columns |
asks_df | pd.DataFrame | Raw 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())
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
| Parameter | Type | Description |
|---|---|---|
bids_cum | pd.DataFrame | Bids DataFrame with cumulative_volume column |
asks_cum | pd.DataFrame | Asks 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}%")
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
| Parameter | Type | Default | Description |
|---|---|---|---|
plot_data | dict | — | Output dictionary from prepare_depth_data() |
title | str | 'Order Book Depth' | Chart title |
figsize | Tuple[float, float] | (14, 7) | Figure dimensions in inches |
show_volume_bars | bool | True | Whether 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()
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
| Parameter | Type | Default | Description |
|---|---|---|---|
mid_price | float | 100.0 | Reference midpoint price |
num_levels | int | 25 | Number of price levels per side |
tick_size | float | 0.10 | Price tick increment |
base_volume | float | 1500.0 | Max volume at innermost level |
seed | Optional[int] | 42 | Random seed |
chart_title | str | 'Order Book Depth — Synthetic Exchange' | Chart title |
Outputs
Returns a Tuple[Figure, Axes, dict]:
fig,ax: Matplotlib objects for further customisationplot_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')
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.
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...
Pipeline complete. ────────────────────────────────────────────────────────────
Bonus: Live Crypto-Scale Order Book
Demonstrate the same pipeline scaled to Bitcoin/USDT price levels:
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...
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 Value | Interpretation |
|---|---|
| +0.30 to +1.0 | Strong bid-side pressure — buying interest dominates |
| −0.10 to +0.10 | Roughly balanced book |
| −0.30 to −1.0 | Strong 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$:
- On the ask curve, find where cumulative volume reaches $Q$. The corresponding price is your estimated average fill price for a buy market order.
- 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
| Operation | Complexity | Notes |
|---|---|---|
| Cumulative depth computation | O(n log n) | Dominated by sort; O(n) if pre-sorted |
| Depth chart rendering | O(n) | Linear in number of price levels |
| Spread / metric computation | O(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/KeyErrormessages - 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:
| Function | Responsibility |
|---|---|
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.