Order Book Streaming
The order book is the most granular view of supply and demand for a security. Streaming order book data lets you see not just the best bid/ask, but the full depth of resting orders at every price level.
Level 1 vs Level 2
Market data comes in two levels of detail:
| Level 1 (Quotes) | Level 2 (Depth) | |
|---|---|---|
| What | Best bid and best ask | Multiple price levels on each side |
| Depth | Top of book only | 5-50 levels typically |
| Update rate | Every quote change | Every level change |
| Use case | Spread monitoring, simple strategies | Market microstructure, liquidity analysis |
| Availability | All providers | IBKR, some paid tiers |
Level 1 (you see): Level 2 (you see):
Bid: $150.20 x 300 Bids: Asks:
Ask: $150.25 x 200 $150.20 x 300 $150.25 x 200
$150.15 x 500 $150.30 x 400
$150.10 x 1200 $150.35 x 800
$150.05 x 2000 $150.40 x 150
The OrderBook Class
Puffin maintains a local OrderBook per symbol, updated incrementally from L2 stream messages:
from puffin.data.realtime import RealtimeEngine, OrderBook
engine = RealtimeEngine(provider)
engine.start(["AAPL"])
# Access the live order book
book = engine.get_order_book("AAPL")
# Top-of-book
print(f"Best bid: ${book.best_bid:.2f}")
print(f"Best ask: ${book.best_ask:.2f}")
print(f"Spread: ${book.spread:.4f}")
# Full depth
for price, size in book.bids[:5]: # top 5 bid levels
print(f" Bid: ${price:.2f} x {size}")
for price, size in book.asks[:5]: # top 5 ask levels
print(f" Ask: ${price:.2f} x {size}")
How Updates Work
Providers send incremental updates, not full snapshots. Each update says “at this price level, the size is now X”:
from puffin.data.realtime import OrderBookUpdate
from datetime import datetime
# New bid level appears
update = OrderBookUpdate("AAPL", "bid", 150.20, 300, datetime.now())
book.update(update) # Adds or updates the level
# Level gets removed (size = 0)
update = OrderBookUpdate("AAPL", "bid", 150.20, 0, datetime.now())
book.update(update) # Removes the level
Snapshot Reset on Reconnection
If the stream disconnects, you may miss updates. Applying incremental updates to a stale book produces incorrect state. The engine handles this automatically:
flowchart TD
D["Disconnect detected"] --> C["Clear all levels"]
C --> R["Reconnect"]
R --> S["Request full snapshot"]
S --> A["Apply snapshot"]
A --> I["Resume incremental updates"]
style D fill:#8b4513,stroke:#5c2d0e,color:#e8e0d4
style C fill:#8b4513,stroke:#5c2d0e,color:#e8e0d4
style R fill:#1a3a5c,stroke:#0d2137,color:#e8e0d4
style S fill:#1a3a5c,stroke:#0d2137,color:#e8e0d4
style A fill:#2d5016,stroke:#1a3a1a,color:#e8e0d4
style I fill:#2d5016,stroke:#1a3a1a,color:#e8e0d4
During the brief window between disconnect and snapshot rebuild, the order book is empty. Your strategy code should handle None returns from best_bid/best_ask gracefully.
Spread Analysis
The bid-ask spread is a key measure of liquidity and trading cost:
def monitor_spread(engine, symbol):
"""Track spread over time for analysis."""
spreads = []
def on_book_change(update):
if update.symbol != symbol:
return
book = engine.get_order_book(symbol)
if book.spread is not None:
spreads.append({
"timestamp": update.timestamp,
"spread": book.spread,
"best_bid": book.best_bid,
"best_ask": book.best_ask,
})
engine.on_book_update(on_book_change)
return spreads
Common spread metrics:
| Metric | Formula | What It Tells You |
|---|---|---|
| Absolute spread | ask - bid | Raw trading cost in dollars |
| Relative spread | (ask - bid) / midpoint | Cost as a percentage |
| Midpoint | (bid + ask) / 2 | Fair value estimate |
| Depth at touch | bid_size + ask_size at best levels | Immediate liquidity available |
Depth Imbalance
The ratio of bid to ask volume near the top of book can signal short-term price direction:
def calculate_imbalance(book: OrderBook, levels: int = 5) -> float:
"""Calculate order book imbalance.
Returns value between -1 (all asks) and +1 (all bids).
Positive values suggest buying pressure.
"""
bid_vol = sum(size for _, size in book.bids[:levels])
ask_vol = sum(size for _, size in book.asks[:levels])
total = bid_vol + ask_vol
if total == 0:
return 0.0
return (bid_vol - ask_vol) / total
Order book imbalance is a well-studied signal in market microstructure research. However, displayed liquidity is only part of the story — hidden orders, icebergs, and spoofing can all distort the visible book. Use imbalance as one input among many, not a standalone signal.
Visualizing the Order Book
A depth chart shows cumulative volume at each price level:
import matplotlib.pyplot as plt
def plot_depth(book: OrderBook, title: str = "Order Book Depth"):
"""Plot cumulative bid/ask depth."""
# Cumulative bid volume (right to left)
bid_prices = [p for p, _ in book.bids]
bid_cum = []
total = 0
for _, size in book.bids:
total += size
bid_cum.append(total)
# Cumulative ask volume (left to right)
ask_prices = [p for p, _ in book.asks]
ask_cum = []
total = 0
for _, size in book.asks:
total += size
ask_cum.append(total)
fig, ax = plt.subplots(figsize=(10, 5))
ax.fill_between(bid_prices, bid_cum, alpha=0.4, color="green", label="Bids")
ax.fill_between(ask_prices, ask_cum, alpha=0.4, color="red", label="Asks")
ax.set_xlabel("Price")
ax.set_ylabel("Cumulative Volume")
ax.set_title(title)
ax.legend()
plt.tight_layout()
return fig
Exercises
-
Spread tracker: Build a callback that tracks the spread for a symbol over time and computes the average, min, and max spread over a 5-minute window. Print a summary when the window closes.
-
Depth imbalance monitor: Implement the
calculate_imbalancefunction above and register it as a book update callback. Print warnings when the imbalance exceeds +0.7 or -0.7 (strong directional pressure). -
Order book visualization: Using the
plot_depthfunction, capture a snapshot of the order book at 10-second intervals for 2 minutes. Create an animation or grid of plots showing how the depth profile evolves over time.
Source Code
Browse the implementation: puffin/data/
Further Reading
- The Data Pipeline chapter covers the
DataProviderinterface and historical data fetching. - The Live Trading chapter covers using real-time data to execute trades with Alpaca and IBKR.