Market Microstructure: Order Books & Execution Mechanics › Practical Analysis: Level 2 Data
Quantitative FinanceAll chapters Market Microstructure: Order Books & Execution Mechanics › Practical Analysis: Level 2 DataOne section · Sections in this chapter10 / 13 ↓
Reading tools Definitions & inline links
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
Let's work through a practical example of analyzing Level 2 order book data, the type of data that you use to understand market microstructure in real-time.
def generate_synthetic_l2_snapshots(
n_snapshots: int = 100, levels: int = 5
) -> pd.DataFrame:
"""
Generate synthetic Level 2 order book snapshots.
Returns DataFrame with timestamp, bid/ask prices and sizes at each level.
"""
np.random.seed(789)
snapshots = []
midprice = 100.0
base_spread = 0.05
for t in range(n_snapshots):
# Random walk midprice
midprice += np.random.randn() * 0.02
# Time-varying spread (widens with volatility)
spread = base_spread * (1 + 0.2 * np.abs(np.random.randn()))
snapshot = {"timestamp": t}
# Generate bid levels
for i in range(levels):
level_price = midprice - spread / 2 - i * 0.01
level_size = int(np.random.exponential(500) * (1 + i * 0.3))
snapshot[f"bid_price_{i + 1}"] = round(level_price, 2)
snapshot[f"bid_size_{i + 1}"] = level_size
# Generate ask levels
for i in range(levels):
level_price = midprice + spread / 2 + i * 0.01
level_size = int(np.random.exponential(500) * (1 + i * 0.3))
snapshot[f"ask_price_{i + 1}"] = round(level_price, 2)
snapshot[f"ask_size_{i + 1}"] = level_size
snapshots.append(snapshot)
return pd.DataFrame(snapshots)
# Generate synthetic L2 data
l2_data = generate_synthetic_l2_snapshots(n_snapshots=500, levels=5)Now let's compute microstructure features from this data:
def compute_microstructure_features(l2_df: pd.DataFrame) -> pd.DataFrame:
"""Compute various microstructure features from L2 data."""
features = l2_df.copy()
# Best bid and ask
features["best_bid"] = features["bid_price_1"]
features["best_ask"] = features["ask_price_1"]
# Midprice and spread
features["midprice"] = (features["best_bid"] + features["best_ask"]) / 2
features["spread"] = features["best_ask"] - features["best_bid"]
features["spread_bps"] = features["spread"] / features["midprice"] * 10000
# Top of book imbalance
features["tob_imbalance"] = (
features["bid_size_1"] - features["ask_size_1"]
) / (features["bid_size_1"] + features["ask_size_1"])
# Total depth at 5 levels
bid_cols = [f"bid_size_{i}" for i in range(1, 6)]
ask_cols = [f"ask_size_{i}" for i in range(1, 6)]
features["total_bid_depth"] = features[bid_cols].sum(axis=1)
features["total_ask_depth"] = features[ask_cols].sum(axis=1)
# Full book imbalance
features["book_imbalance"] = (
features["total_bid_depth"] - features["total_ask_depth"]
) / (features["total_bid_depth"] + features["total_ask_depth"])
# Volume-weighted bid/ask prices (proxy for depth-weighted prices)
features["vwap_bid"] = (
sum(
features[f"bid_price_{i}"] * features[f"bid_size_{i}"]
for i in range(1, 6)
)
/ features["total_bid_depth"]
)
features["vwap_ask"] = (
sum(
features[f"ask_price_{i}"] * features[f"ask_size_{i}"]
for i in range(1, 6)
)
/ features["total_ask_depth"]
)
# Microprice (depth-weighted midprice)
features["microprice"] = (
features["best_bid"] * features["ask_size_1"]
+ features["best_ask"] * features["bid_size_1"]
) / (features["bid_size_1"] + features["ask_size_1"])
return features
# Compute features
features_df = compute_microstructure_features(l2_data)Microstructure Feature Summary Statistics
=======================================================
spread_bps tob_imbalance book_imbalance total_bid_depth total_ask_depth
count 500.000 500.000 500.000 500.000 500.00
mean 5.768 -0.005 -0.010 3995.422 4080.15
std 0.741 0.571 0.306 1830.485 1839.17
min 4.987 -1.000 -0.756 479.000 679.00
25% 5.006 -0.494 -0.249 2632.250 2812.75
50% 5.996 -0.020 -0.009 3683.500 3820.50
75% 6.010 0.487 0.211 5089.250 5122.75
max 8.020 0.988 0.887 11504.000 12962.00The microprice is particularly interesting: it's a depth-weighted midprice that accounts for order book imbalance. Unlike the standard midprice which treats the bid and ask symmetrically, the microprice adjusts based on available liquidity. The intuition is that prices are more likely to move toward the side with less depth, since less volume is required to push the price in that direction.
We can derive the microprice formula by thinking about it as a weighted average where the weights are the opposite side's depth. When there is more depth on the bid side, the price is more likely to move upward (toward the ask), so we weight the ask price more heavily:
Let us examine each variable to understand the economic intuition:
- : microprice. This represents a more informed estimate of the security's fair value than the simple midprice. It incorporates order book depth information that may predict short-term price movements.
- : best bid price. The highest price buyers are willing to pay.
- : best ask price. The lowest price sellers are willing to accept.
- : volume at the best bid. Represents the immediate buying liquidity. When this is large relative to the ask volume, it takes more selling to push the price down.
- : volume at the best ask. Represents the immediate selling liquidity. When this is large relative to the bid volume, it takes more buying to push the price up.
- : bid imbalance ratio, defined as . This ratio ranges from 0 to 1 and measures what fraction of top-of-book liquidity sits on the bid side.
When there's more depth on the bid side, the microprice shifts closer to the ask price, which reflects the likely direction of the next price move.


When book imbalance is positive (more bid depth), the microprice tends to be higher than the midprice, anticipating upward price pressure. This relationship forms the basis for many short-term trading signals.