Market Microstructure: Order Books & Execution Mechanics › Practical Analysis: Level 2 Data

Michael BrenndoerferJanuary 9, 20266 min read
Quantitative FinanceAll chapters
Part 12 of 15Market Making and High-Frequency Trading
Chapter 67Market Microstructure: Order Books & Execution Mechanics › Practical Analysis: Level 2 Data
Reading section 10 of 13 · Practical Analysis: Level 2 Data · 6 minSection 10 of 13 · 6 min

Book contents

Section 10 of 13 · 6 min read

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.

In[30]:
Code
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:

In[31]:
Code
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)
Out[32]:
Console
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.00

The 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:

Pmicro=PbVa+PaVbVb+Va(weighted average definition)=Pa(VbVb+Va)+Pb(VaVb+Va)(rearrange terms)=I⋅Pa+(1−I)⋅Pb(substitute imbalance ratio)\begin{aligned} P_{\text{micro}} &= \frac{P_b V_a + P_a V_b}{V_b + V_a} && \text{(weighted average definition)} \\ &= P_a \left(\frac{V_b}{V_b + V_a}\right) + P_b \left(\frac{V_a}{V_b + V_a}\right) && \text{(rearrange terms)} \\ &= I \cdot P_a + (1-I) \cdot P_b && \text{(substitute imbalance ratio)} \end{aligned}

Let us examine each variable to understand the economic intuition:

  • PmicroP_{\text{micro}}: 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.
  • PbP_b: best bid price. The highest price buyers are willing to pay.
  • PaP_a: best ask price. The lowest price sellers are willing to accept.
  • VbV_b: 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.
  • VaV_a: 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.
  • II: bid imbalance ratio, defined as VbVb+Va\frac{V_b}{V_b + V_a}. 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.

Out[33]:
Visualization
Two closely tracking lines show midprice and microprice over time, with small depth-driven deviations between them.
Standard midprice and depth-weighted microprice during the simulation. The microprice moves slightly above or below the midprice as bid-side and ask-side depth become unbalanced.
Vertical impulse chart of order book imbalance oscillating above and below zero, with distinct colors for positive and negative values.
Order book imbalance calculated from the top five levels. Upward marks indicate greater bid-side depth, downward marks indicate greater ask-side depth, and zero represents a balanced book.

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.