Market Microstructure: Order Books & Execution Mechanics › Order Book Dynamics and Price Impact

Michael BrenndoerferJanuary 9, 20267 min read
Quantitative FinanceAll chapters
Part 12 of 15Market Making and High-Frequency Trading
Chapter 67Market Microstructure: Order Books & Execution Mechanics › Order Book Dynamics and Price Impact
Reading section 8 of 13 · Order Book Dynamics and Price Impact · 7 minSection 8 of 13 · 7 min

Book contents

Section 8 of 13 · 7 min read

Market Microstructure: Order Books & Execution Mechanics › Order Book Dynamics and Price ImpactOne section · Sections in this chapter8 / 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

The order book is not static; it evolves continuously as orders arrive, execute, and cancel. These dynamics shape execution and short-term trading strategies.

Event-Driven Order Book Updates

Order book changes occur through several events:

In[24]:
Code
class OrderBookEvent:
    """Base class for order book events."""

    pass


@dataclass
class NewOrder(OrderBookEvent):
    order: Order


@dataclass
class CancelOrder(OrderBookEvent):
    order_id: int


@dataclass
class Trade(OrderBookEvent):
    price: float
    quantity: int
    aggressor_side: Side  # which side initiated the trade


def simulate_order_book_dynamics(
    book: OrderBook, n_events: int = 100
) -> List[dict]:
    """Simulate order book dynamics and track midprice evolution."""
    np.random.seed(42)

    history = []
    midprice = book.get_midprice()

    for t in range(n_events):
        # Record state before event
        mid_before = book.get_midprice()
        spread_before = book.get_spread()

        # Generate random event
        event_type = np.random.choice(
            [
                "new_bid",
                "new_ask",
                "cancel_bid",
                "cancel_ask",
                "market_buy",
                "market_sell",
            ],
            p=[0.25, 0.25, 0.15, 0.15, 0.10, 0.10],
        )

        if event_type == "new_bid":
            price = midprice - np.random.exponential(0.02)
            qty = np.random.randint(100, 500)
            book.add_limit_order(Side.BID, round(price, 2), qty)

        elif event_type == "new_ask":
            price = midprice + np.random.exponential(0.02)
            qty = np.random.randint(100, 500)
            book.add_limit_order(Side.ASK, round(price, 2), qty)

        elif event_type in ["market_buy", "market_sell"]:
            side = Side.BID if event_type == "market_buy" else Side.ASK
            qty = np.random.randint(200, 800)
            execute_market_order(book, side, qty)

        # Skip cancel events for simplicity in this simulation

        # Record state after event
        mid_after = book.get_midprice()
        spread_after = book.get_spread()

        if mid_after is not None:
            midprice = mid_after
            history.append(
                {
                    "time": t,
                    "event": event_type,
                    "midprice": mid_after,
                    "spread": spread_after,
                    "midprice_change": (mid_after - mid_before)
                    if mid_before
                    else 0,
                }
            )

    return history
In[25]:
Code
# Create fresh book and simulate
sim_book = OrderBook("XYZ")

# Initialize with some depth
for price in np.arange(99.70, 99.96, 0.01):
    for _ in range(3):
        sim_book.add_limit_order(
            Side.BID, round(price, 2), np.random.randint(200, 600)
        )

for price in np.arange(100.00, 100.26, 0.01):
    for _ in range(3):
        sim_book.add_limit_order(
            Side.ASK, round(price, 2), np.random.randint(200, 600)
        )

# Run simulation
history = simulate_order_book_dynamics(sim_book, n_events=200)
history_df = pd.DataFrame(history)
Out[26]:
Visualization
Line chart of midprice over event number with vertical markers for market buy and sell events.
Simulated midprice evolution over 200 events. Vertical markers identify market buys and sells that consume resting liquidity and may coincide with immediate midprice changes.
Area chart of the bid-ask spread over 200 events, with a low baseline interrupted by brief spikes.
Bid-ask spread during the same simulation. The stepwise series stays near its baseline most of the time and spikes when best-price liquidity is depleted.

The simulation illustrates how market orders can coincide with immediate price changes. The spread remains near its baseline for much of the run and briefly widens when the best available liquidity is depleted.

Order Flow Imbalance and Price Prediction

Research in market microstructure has established that order flow imbalance (the difference between buy and sell volume) predicts short-term price movements. The intuition behind this relationship is straightforward: when more buyers than sellers are actively trading, prices tend to rise to attract additional sellers, and vice versa.

We can define the Order Flow Imbalance (OFI) over a time window as:

OFI=∑i=1Nqi⋅di\text{OFI} = \sum_{i=1}^{N} q_i \cdot d_i

This formula captures the net directional pressure from actual trades. Let us understand each component:

  • OFI\text{OFI}: Order Flow Imbalance over the time window. This is a signed quantity: positive values indicate net buying pressure, negative values indicate net selling pressure.
  • qiq_i: quantity of the ii-th trade. Larger trades contribute more to the imbalance, which reflects the greater information content and market impact of size.
  • did_i: direction of the trade (+1 for buyer-initiated, -1 for seller-initiated). A buyer-initiated trade occurs when a buyer submits a market order that executes against resting limit sell orders. A seller-initiated trade occurs when a seller's market order executes against resting limit buy orders.
  • NN: number of trades in the window. The window choice affects signal characteristics: shorter windows capture high-frequency dynamics, while longer windows smooth out noise but may lag actual price movements.

This relationship forms the basis for many high-frequency trading strategies, as we discussed in Part VI.

In[27]:
Code
def calculate_order_flow_imbalance(
    trades: pd.DataFrame, window: int = 10
) -> pd.Series:
    """
    Calculate order flow imbalance over a rolling window.
    Assumes trades dataframe has 'quantity' and 'aggressor_side' columns.
    """
    # Signed volume: positive for buys, negative for sells
    trades["signed_volume"] = np.where(
        trades["aggressor_side"] == "buy",
        trades["quantity"],
        -trades["quantity"],
    )

    # Rolling sum of signed volume
    rolling_signed = trades["signed_volume"].rolling(window=window).sum()
    rolling_total = trades["quantity"].abs().rolling(window=window).sum()

    imbalance = rolling_signed / rolling_total
    return imbalance


# Create synthetic trade data
np.random.seed(456)
n_trades = 500
synthetic_trades = pd.DataFrame(
    {
        "time": np.arange(n_trades),
        "price": 100 + np.cumsum(np.random.randn(n_trades) * 0.01),
        "quantity": np.random.randint(100, 1000, n_trades),
        "aggressor_side": np.random.choice(
            ["buy", "sell"], n_trades, p=[0.52, 0.48]
        ),
    }
)

# Calculate imbalance
synthetic_trades["imbalance"] = calculate_order_flow_imbalance(
    synthetic_trades, window=20
)

# Construct a noisy short-horizon response linked to the imbalance signal.
# This deliberate coupling makes the synthetic teaching example internally
# consistent without implying that the coefficient transfers to real markets.
ofi_rng = np.random.default_rng(457)
synthetic_trades["future_return"] = 0.0007 * synthetic_trades[
    "imbalance"
] + ofi_rng.normal(0, 0.00022, n_trades)
In[28]:
Code
# Drop NaN values for analysis
analysis_df = synthetic_trades.dropna()

# Fit regression line
coef = np.polyfit(
    analysis_df["imbalance"], analysis_df["future_return"] * 100, 1
)
x_line = np.linspace(
    analysis_df["imbalance"].min(), analysis_df["imbalance"].max(), 100
)
y_line = coef[0] * x_line + coef[1]
correlation = analysis_df["imbalance"].corr(analysis_df["future_return"])
Out[29]:
Visualization
Scatter plot of synthetic order flow imbalance and future return with a positively sloped fitted line and substantial dispersion around it.
Synthetic order flow imbalance (OFI) versus a noisy short-horizon return constructed with a positive OFI component. The fitted slope is about 0.071 percentage points per unit of imbalance, with a sample correlation of about 0.59.

This synthetic sample deliberately couples returns to imbalance, producing a fitted slope of about 0.071 percentage points and a correlation near 0.59. The dispersion is a reminder that even a visible signal is noisy; in real data, its coefficient must be estimated out of sample and its value may be smaller than execution costs.