Market Making & Liquidity Provision: Optimal Quoting Models

Michael BrenndoerferDecember 31, 202557 min read

Learn how market makers profit from bid-ask spreads while managing inventory risk. Explore the Avellaneda-Stoikov model for optimal quote placement.

Reading Level

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.

Market Making and Liquidity Provision

Financial markets require participants willing to stand ready to buy and sell at any moment. You fulfill this essential role by continuously posting bid and ask quotes, providing liquidity to traders who need immediate execution. In exchange for this service, you capture the bid-ask spread, the difference between the price at which you buy and the price at which you sell.

Modern market making has evolved from human specialists on exchange floors to sophisticated algorithmic systems that quote across thousands of instruments simultaneously. You use mathematical models to determine optimal quote placement, manage inventory risk, and protect against informed traders. The business model is deceptively simple: buy at the bid, sell at the ask, pocket the spread. But the execution is extraordinarily complex, requiring real-time risk management, microsecond-level technology, and deep understanding of market dynamics.

This chapter explores the economics of market making, the mathematical models that guide quoting decisions, and the key risks you face. We will build a simulation framework to understand how these strategies work in practice and examine why speed and risk management are critical to success.

The Economics of Market Making

You profit by providing a service: immediacy. When a trader wants to buy a stock right now, they don't want to wait for another natural seller to appear. You offer to sell immediately, but at a premium, the ask price. Similarly, when someone wants to sell immediately, you buy at the bid price, which is lower than fair value. This willingness to take the other side of any trade, at any time, is what makes markets function smoothly. Without market makers, traders would face significant delays finding counterparties, and the uncertainty of execution timing would itself become a substantial cost of trading.

Bid-Ask Spread

The bid-ask spread is the difference between the highest price a buyer is willing to pay (the bid) and the lowest price a seller is willing to accept (the ask). You earn this spread by buying at the bid and selling at the ask.

Consider a simple example that illustrates the core economics. You quote a stock with a bid of 99.95andanaskof99.95 and an ask of 100.05. The spread is $0.10. If you buy 100 shares at the bid and sell 100 shares at the ask, the gross profit is:

Gross Profit=100×($100.05$99.95)=$10\text{Gross Profit} = 100 \times (\$100.05 - \$99.95) = \$10

This calculation is simple. However, several practical complications transform it into an optimization problem:

  • Inventory accumulation: Trades don't arrive in perfect alternation. You might buy 500 shares before selling any, accumulating inventory that could lose value if prices move against you. This imbalance is the norm. Order flow is unpredictable, often leading to unintended positions.
  • Adverse selection: Some traders have better information about future prices. When an informed trader buys, it often signals that prices will rise, meaning you just sold too cheaply. This information asymmetry is a serious risk in market making because it systematically transfers wealth from you to informed traders.
  • Competition: Competition for order flow compresses spreads. In liquid markets, even small inefficiencies can eliminate profit.
  • Volatility: Higher volatility means greater risk of price moves between when you buy and sell. A position held for even a few seconds during a volatile period can experience significant adverse price movement.

Understanding these factors allows us to express your expected profit per single trade in a more complete form:

E[Profit]=s2Adverse Selection CostInventory CostE[\text{Profit}] = \frac{s}{2} - \text{Adverse Selection Cost} - \text{Inventory Cost}

where:

  • ss: bid-ask spread
  • Adverse Selection Cost: expected loss to informed traders
  • Inventory Cost: cost of holding inventory risk

The factor of 12\frac{1}{2} appears because you earn half the spread on each side of a trade relative to the mid price. To understand why this is the case, consider that the mid price represents the market's best estimate of fair value. When you buy at the bid, you pay a price that is s2\frac{s}{2} below the mid price. When you sell at the ask, you receive a price that is s2\frac{s}{2} above the mid price. Each individual trade therefore generates revenue of half the spread relative to fair value. Successful market making requires setting spreads wide enough to cover adverse selection and inventory costs while remaining competitive enough to attract order flow. This delicate balance defines the central challenge of the market making business.

Out[2]:
Visualization
Market maker profit economics. The linear spread revenue (s/2) must exceed the sum of adverse selection and inventory costs for the trade to be profitable. The green shaded region indicates the zone where the spread is sufficient to generate net profit, while the red region indicates a loss.
Market maker profit economics. The linear spread revenue (s/2) must exceed the sum of adverse selection and inventory costs for the trade to be profitable. The green shaded region indicates the zone where the spread is sufficient to generate net profit, while the red region indicates a loss.

Order Book Mechanics

To understand market making, you need to understand the limit order book, the fundamental data structure that organizes trading in modern electronic markets. As we'll explore in more detail in the upcoming chapter on Market Microstructure, the order book aggregates all outstanding buy and sell orders at various price levels. Think of the order book as a ledger that records every participant's willingness to trade at specific prices, continuously updated as orders arrive, execute, or are cancelled.

Limit Order Book

A limit order book is an electronic record of all outstanding orders to buy or sell a security at specific prices. Buy orders (bids) are sorted from highest to lowest price, while sell orders (asks) are sorted from lowest to highest.

There are two primary order types that interact with this book structure:

  • Limit orders specify a maximum price (for buys) or minimum price (for sells) and wait in the book until matched. You primarily use limit orders to post your quotes. These orders provide liquidity to the market: they sit passively in the book, waiting for someone else to trade against them. You don't know when or if your order will execute, but you do know the price you will receive if it does.
  • Market orders execute immediately against the best available limit orders in the book. Traders demanding immediacy use market orders. These orders consume liquidity: they remove resting limit orders from the book. The market order submitter knows their order will execute immediately, but they don't control the exact price they will receive.
In[3]:
Code
import numpy as np
import pandas as pd

# Simulate a simple order book snapshot
np.random.seed(42)

# Generate bid and ask levels around a mid price of 100
mid_price = 100.0
tick_size = 0.01

# Create price levels
bid_prices = np.arange(mid_price - 0.10, mid_price, tick_size)
ask_prices = np.arange(mid_price + 0.01, mid_price + 0.11, tick_size)

# Generate random quantities (typically more volume near the inside)
bid_quantities = np.random.exponential(scale=200, size=len(bid_prices))
bid_quantities = bid_quantities * np.linspace(
    1.5, 0.5, len(bid_prices)
)  # More at inside

ask_quantities = np.random.exponential(scale=200, size=len(ask_prices))
ask_quantities = ask_quantities * np.linspace(1.5, 0.5, len(ask_prices))

# Create order book DataFrame
order_book = pd.DataFrame(
    {
        "bid_price": np.round(bid_prices[::-1], 2),
        "bid_qty": np.round(bid_quantities[::-1]).astype(int),
        "ask_price": np.round(ask_prices, 2),
        "ask_qty": np.round(ask_quantities).astype(int),
    }
)

# Calculate best quotes and spread for display
best_bid = order_book["bid_price"].iloc[0]
best_ask = order_book["ask_price"].iloc[0]
spread = best_ask - best_bid
Out[4]:
Console
Order Book Snapshot
==================================================
   Bid Qty        Bid |        Ask    Ask Qty
--------------------------------------------------
     123.0      99.99 |     100.01        6.0
     112.0      99.98 |     100.02      973.0
     291.0      99.97 |     100.03      457.0
      10.0      99.96 |     100.04       56.0
      32.0      99.95 |     100.05       42.0
--------------------------------------------------
Best Bid: 99.99
Best Ask: 100.01
Spread: 0.02

The order book shows the best bid at 99.99andthebestaskat99.99 and the best ask at 100.01, creating a spread of $0.02. When you post at the inside (best bid and ask), your quote has priority over orders further from the mid price. This priority is crucial because orders at the same price typically execute in time priority, meaning the first order to arrive at a given price level executes first. When a market buy order arrives, it executes against the best ask; a market sell order executes against the best bid. This matching process continues until the incoming order is fully filled or the book is exhausted at that price level.

Out[5]:
Visualization
Bar chart showing bid depth on left in blue and ask depth on right in red around mid price.
Snapshot of order book depth showing bid and ask volume at different price levels. Cumulative volume typically increases as distance from the mid-price grows. A market maker must balance the desire for execution priority at the inside quote against the safety of posting liquidity further back in the book.

You must decide at which price levels to place your orders and how much size to offer. This decision involves a fundamental trade-off that shapes all market making strategies. Posting at the inside quote provides the highest probability of execution but the smallest profit per trade. Posting further from the mid price increases profit per trade but reduces execution probability. The optimal choice depends on many factors, including the volatility of the asset, the behavior of other market participants, and your current inventory position.

Inventory Risk

The central challenge in market making is inventory risk, the risk that accumulated positions will lose value before they can be unwound. If you buy more than you sell, you accumulate a long position. If prices then fall, you suffer losses that may exceed the spread revenue earned. This risk is unavoidable because you cannot control the timing or direction of incoming orders. You must quote continuously, but you cannot determine which of your quotes will be executed.

The Inventory Accumulation Problem

Consider a scenario where you start the day flat (no position). Over the course of trading, order arrivals are random. Sometimes more buyers arrive, causing you to sell and become short. Other times more sellers arrive, causing you to buy and become long. Your inventory thus evolves as a consequence of the random sequence of order arrivals, not as a result of any deliberate trading decision.

We can model inventory evolution as a random process. This mathematical framework helps us understand why inventory accumulation is such a persistent concern. Let qtq_t denote your inventory at time tt. Each trade changes inventory by ±1\pm 1 (assuming unit trade sizes):

qt+1=qt+Δqtq_{t+1} = q_t + \Delta q_t

where:

  • qtq_t: current inventory level
  • Δqt\Delta q_t: change in inventory (+1 for buy, -1 for sell)
  • qt+1q_{t+1}: inventory level after the trade

If a market sell order arrives, you buy (Δqt=+1\Delta q_t = +1). If a market buy order arrives, you sell (Δqt=1\Delta q_t = -1). Notice that you take the opposite side of each incoming order, which is what it means to "make a market." The inventory change is thus determined by the arriving order, not by your preference.

In[6]:
Code
import numpy as np

# Simulate inventory paths for a market maker
np.random.seed(123)

n_steps = 1000
n_paths = 5

# Simulate multiple inventory paths
# Assume 50% probability of buy vs sell order, with slight imbalance
buy_prob = 0.50  # Probability that incoming order is a sell (market maker buys)

inventory_paths = np.zeros((n_paths, n_steps + 1))

for i in range(n_paths):
    for t in range(n_steps):
        # Random order arrival: +1 if market maker buys, -1 if sells
        if np.random.random() < buy_prob:
            inventory_paths[i, t + 1] = inventory_paths[i, t] + 1
        else:
            inventory_paths[i, t + 1] = inventory_paths[i, t] - 1
Out[7]:
Visualization
Line chart showing five random walk inventory paths diverging from zero over time.
Simulated inventory paths over 1,000 trades. Even with perfectly balanced order arrival probabilities, inventory levels follow a random walk and can drift significantly from zero. This unmanaged drift exposes a market maker to directional price risk.

The simulation reveals a crucial insight about market making dynamics. Even with perfectly balanced order flow, where buys and sells arrive with equal probability, inventory follows a random walk and can drift far from zero. This is a mathematical consequence of the random walk properties: the expected position is zero, but the variance of the position grows with the number of trades. This drift is dangerous because it exposes you to directional price risk, exactly the risk you don't want to take. You aim to profit from the spread, not from predicting price direction. Yet without active management, you inevitably accumulate positions that bet on price direction.

Quantifying Inventory Risk

To understand how to manage inventory risk, we must first quantify it precisely. The P&L impact of inventory comes from price changes. If you hold inventory qq and the price changes by ΔP\Delta P, the inventory P&L is:

Inventory P&L=q×ΔP\text{Inventory P\&L} = q \times \Delta P

where:

  • qq: current inventory position
  • ΔP\Delta P: change in asset price

This formula captures the essence of the risk: your profit or loss depends on how much inventory you hold and how much the price moves. A long position profits when prices rise and loses when prices fall; a short position has the opposite exposure.

The variance of inventory P&L over a short time interval Δt\Delta t can be derived from the price variance. This derivation connects our understanding of price dynamics to the specific risk you face. Assuming the asset price PP follows a random walk with percentage volatility σ\sigma:

Var[Inventory P&L]=Var[qΔP]=q2Var[ΔP](properties of variance)=q2(Pσ)2Δt(substitute price variance)\begin{aligned} \text{Var}[\text{Inventory P\&L}] &= \text{Var}[q \Delta P] \\ &= q^2 \text{Var}[\Delta P] && \text{(properties of variance)} \\ &= q^2 (P \sigma)^2 \Delta t && \text{(substitute price variance)} \end{aligned}

where:

  • qq: inventory size
  • ΔP\Delta P: change in asset price
  • PP: current asset price
  • σ\sigma: annualized volatility (percentage)
  • Δt\Delta t: time interval

This result reveals a critical insight: inventory risk grows quadratically with position size. Holding twice the inventory creates four times the variance, not twice the variance. This non-linear relationship means that large positions are disproportionately risky. If you hold 100 shares, you face four times the risk variance of holding 50 shares, even though you hold only twice as many shares. This quadratic scaling provides strong incentive to keep positions small.

In[8]:
Code
import numpy as np

# Demonstrate the relationship between inventory and risk
sigma = 0.02  # 2% daily volatility
dt = 1 / 252  # One trading day
price = 100

# Calculate P&L variance for different inventory levels
inventory_levels = np.arange(-100, 101, 10)
pnl_std = np.abs(inventory_levels) * sigma * np.sqrt(dt) * price
Out[9]:
Visualization
Line chart showing V-shaped relationship between inventory and P&L standard deviation.
Inventory risk profile showing P&L standard deviation vs inventory size. The linear relationship between position size and risk deviation implies that variance (risk) grows quadratically. This convex risk profile creates a strong incentive for market makers to actively manage inventory toward zero.

This risk profile motivates a key market making principle: inventory mean reversion. You should actively adjust your quotes to encourage trades that reduce inventory. When long, you lower your ask price to attract buyers; when short, you raise your bid price to attract sellers. This quote adjustment technique is called "skewing" and represents the primary tool you use to control their exposure. By making it more attractive for the market to trade in the direction that reduces your position, you can systematically pull your inventory back toward zero without waiting passively for favorable order flow.

Adverse Selection

Beyond inventory risk, you face adverse selection, the risk of trading against informed counterparties who know more about future prices than you do. This concept, which we touched on in our discussion of liquidity risk Part V, is fundamental to understanding why market making spreads exist. While inventory risk can be managed through quote adjustment and hedging, adverse selection represents a more insidious challenge because it systematically extracts value from you.

Information Asymmetry

Consider the market for a stock about to report earnings. Some traders may have superior analysis or even inside information about the announcement. When these informed traders buy, it signals that the stock is likely undervalued, meaning you are selling too cheaply. When informed traders sell, you are buying something that will soon be worth less. You cannot distinguish between informed and uninformed traders at the moment of the trade; you only discover afterward, when prices move, whether your counterparty was informed.

Adverse Selection

Adverse selection occurs when one party to a transaction has information that the other lacks. In market making, informed traders profit at your expense by trading when they have superior knowledge of future price movements.

The famous Glosten-Milgrom model formalizes this intuition, providing a theoretical foundation for understanding how adverse selection affects market making. Suppose a fraction α\alpha of traders are informed and know the true value VV of the asset, while the remaining (1α)(1-\alpha) are uninformed liquidity traders who trade for reasons unrelated to the asset's fundamental value. You must set quotes knowing that:

  • When an informed trader buys, V>AskV > \text{Ask} (you sold too cheaply)
  • When an informed trader sells, V<BidV < \text{Bid} (you bought too expensively)
  • Uninformed traders buy and sell randomly

The key insight is that you face a winner's curse problem. You are more likely to execute trades precisely when those trades are unfavorable. Informed traders only trade when your quotes are mispriced relative to true value, while uninformed traders provide a mixture of profitable and unprofitable trades. Your break-even spread must compensate for losses to informed traders:

s=α(VHVL)s^* = \alpha (V^H - V^L)

where:

  • ss^*: break-even spread
  • α\alpha: probability that the next trader is informed
  • VHVLV^H - V^L: difference between the asset's high and low values (value of private information)

Higher informed trading probability α\alpha requires wider spreads to compensate for the greater expected losses to informed counterparties. Similarly, when the value of private information is larger (the gap between VHV^H and VLV^L is greater), spreads must widen because each trade against an informed counterparty generates larger losses.

Out[10]:
Visualization
Break-even spread vs informed trader fraction ($\alpha$). Spreads widen linearly as the fraction of informed traders increases to compensate for adverse selection.
Break-even spread vs informed trader fraction ($\alpha$). Spreads widen linearly as the fraction of informed traders increases to compensate for adverse selection.
Break-even spread vs value of private information. For a fixed informed fraction, the required spread increases with the value of private information, compensating for the winner's curse.
Break-even spread vs value of private information. For a fixed informed fraction, the required spread increases with the value of private information, compensating for the winner's curse.

Detecting Informed Flow

You use various signals to detect potentially informed order flow. These detection methods are imperfect, but they can help you adjust your quotes or trade sizes when the risk of adverse selection appears elevated:

  • Order size: Large orders may signal informed trading because informed traders have greater incentive to trade large quantities when they possess valuable information
  • Order timing: Trades just before news announcements are more likely to be informed, as traders may have advance knowledge or superior forecasting ability
  • Order toxicity: Metrics like VPIN (Volume-synchronized Probability of Informed Trading) attempt to measure the proportion of informed flow in real time
  • Price impact: Orders that move prices significantly suggest the market is updating its beliefs in response to perceived information content
In[11]:
Code
import numpy as np

# Simulate adverse selection impact
np.random.seed(456)

# Simulate 1000 trades
n_trades = 1000
informed_fraction = 0.15  # 15% of trades are informed
true_move_size = 0.005  # Informed traders know price will move 0.5%

# Generate trade labels: informed (1) or uninformed (0)
is_informed = np.random.random(n_trades) < informed_fraction

# Generate trade directions: 1 for buy, -1 for sell
# Uninformed: random direction
# Informed: always trade in direction of known move (assume positive move)
trade_direction = np.where(is_informed, 1, np.random.choice([-1, 1], n_trades))

# Simulate subsequent price moves
# If trade was informed buy, price tends to go up
price_moves = np.where(
    is_informed,
    trade_direction * true_move_size + np.random.normal(0, 0.003, n_trades),
    np.random.normal(0, 0.003, n_trades),
)

# Market maker's P&L per trade (negative of trade direction times price move)
# Market maker takes opposite side, so if trader buys, MM sells
spread_earned = 0.001  # 10 bps spread
mm_pnl_per_trade = spread_earned / 2 - trade_direction * price_moves

# Separate P&L for informed vs uninformed trades
pnl_vs_informed = mm_pnl_per_trade[is_informed]
pnl_vs_uninformed = mm_pnl_per_trade[~is_informed]

# Calculate aggregate statistics
n_uninformed = (~is_informed).sum()
avg_pnl_uninformed = pnl_vs_uninformed.mean() * 100  # per $100 notional
total_pnl_uninformed = pnl_vs_uninformed.sum() * 100

n_informed = is_informed.sum()
avg_pnl_informed = pnl_vs_informed.mean() * 100
total_pnl_informed = pnl_vs_informed.sum() * 100

avg_pnl_overall = mm_pnl_per_trade.mean() * 100
total_pnl_overall = mm_pnl_per_trade.sum() * 100
Out[12]:
Console
Market Maker P&L Analysis by Counterparty Type
==================================================

Vs Uninformed Traders (851 trades):
  Average P&L: $0.0388 per $100 notional
  Total P&L:   $32.99

Vs Informed Traders (149 trades):
  Average P&L: $-0.4991 per $100 notional
  Total P&L:   $-74.36

Overall:
  Average P&L: $-0.0414 per $100 notional
  Total P&L:   $-41.37

The simulation clearly shows the adverse selection problem in action. We earn money against uninformed traders but lose even more against informed traders. The overall P&L depends critically on the fraction of informed flow and the size of the information edge. This is why you should obsess over order flow quality: a venue or counterparty that generates predominantly uninformed flow is enormously valuable, while flow with high informed content can quickly destroy profitability.

Out[13]:
Visualization
Two overlapping histograms showing P&L distributions for informed and uninformed counterparties.
Distribution of market maker P&L per trade against informed versus uninformed counterparties. Trades against uninformed participants (green) center around positive values due to the spread, while trades against informed participants (red) result in losses as prices move against the market maker. The aggregate profitability depends on the ratio of uninformed to informed flow.

Optimal Market Making: The Avellaneda-Stoikov Model

The foundational quantitative model for market making was developed by Marco Avellaneda and Sasha Stoikov in 2008. This model provides optimal bid and ask quotes that balance the trade-off between earning spread and managing inventory risk. The model produces closed-form solutions that capture the essential economics of market making while remaining tractable enough for real-time implementation.

Model Setup

The Avellaneda-Stoikov model builds on several key assumptions that simplify the market making problem while preserving its essential features:

  • The asset price follows arithmetic Brownian motion: dSt=σabsdWtdS_t = \sigma_{\text{abs}} dW_t
  • You have a terminal time horizon TT and seek to maximize expected utility of terminal wealth
  • Order arrivals follow Poisson processes with intensity depending on quote distance from mid price
  • You have exponential utility with risk aversion parameter γ\gamma

Each assumption serves a specific purpose. Arithmetic Brownian motion simplifies the analysis while capturing the essential feature that prices fluctuate unpredictably. The terminal horizon reflects the fact that you cannot hold positions indefinitely and must eventually flatten your books. The Poisson arrival process captures the random nature of order flow, while exponential utility provides a tractable way to model risk aversion.

Let δa\delta^a and δb\delta^b be the distances of the ask and bid from the mid price. The order arrival intensity (rate) at a distance δ\delta from the mid price is modeled as:

λ(δ)=Aekδ\lambda(\delta) = A e^{-k\delta}

where:

  • λ(δ)\lambda(\delta): arrival intensity
  • δ\delta: distance from mid price
  • AA: baseline arrival rate
  • kk: order flow sensitivity to price

This exponential decay function captures the fundamental trade-off in limit order trading: increasing the spread (larger δ\delta) yields more profit per trade but exponentially reduces the probability of execution. The parameter kk controls how sensitive order flow is to price. A high value of kk means that traders are very price-sensitive, so moving quotes even slightly away from the mid price dramatically reduces execution probability. A low value of kk indicates price-insensitive traders who will trade across a wider range of prices. Understanding the value of kk in a particular market is crucial for calibrating the model.

Out[14]:
Visualization
Order arrival intensity decay functions for varying price sensitivities ($k$). As the quote distance from the mid-price increases, execution probability drops exponentially. Higher values of $k$ represent more price-sensitive markets where even small deviations from the inside quote result in a sharp drop in order flow.
Order arrival intensity decay functions for varying price sensitivities ($k$). As the quote distance from the mid-price increases, execution probability drops exponentially. Higher values of $k$ represent more price-sensitive markets where even small deviations from the inside quote result in a sharp drop in order flow.

Optimal Quotes

The model yields closed-form optimal quotes through a dynamic programming argument. In this model, you maximize expected utility of terminal wealth, accounting for the uncertain arrival of orders and the risk of holding inventory. The solution introduces a fundamental concept called the reservation price, which represents your private valuation accounting for inventory:

r=Sqγσabs2(Tt)r = S - q\gamma\sigma_{\text{abs}}^2(T-t)

where:

  • rr: reservation price
  • SS: current mid price
  • qq: current inventory
  • γ\gamma: risk aversion parameter
  • σabs\sigma_{\text{abs}}: absolute volatility of the asset (in price units, distinct from percentage volatility)
  • TtT-t: time remaining until the terminal horizon

The key insight is that the reservation price shifts away from the mid price based on inventory. This shift quantifies how your risk changes your perception of fair value:

  • When long (q>0q > 0), the reservation price is below the mid price, encouraging selling
  • When short (q<0q < 0), the reservation price is above the mid price, encouraging buying

To understand why this makes sense, consider being long 50 shares. You face the risk that prices will fall, causing you to lose money on your position. From your perspective, a "fair" price for buying more shares is lower than the market mid price because additional purchases increase your risk. Conversely, a fair price for selling is also lower than the mid price because you are eager to reduce your risk exposure. The reservation price formula captures this adjustment precisely.

The optimal spread around this reservation price is:

δ=γσabs2(Tt)+2γln(1+γk)\delta^* = \gamma\sigma_{\text{abs}}^2(T-t) + \frac{2}{\gamma}\ln\left(1 + \frac{\gamma}{k}\right)

where:

  • δ\delta^*: optimal spread
  • γ\gamma: risk aversion parameter
  • σabs\sigma_{\text{abs}}: absolute volatility
  • TtT-t: time remaining
  • kk: order flow sensitivity

The spread consists of two terms, each with a distinct economic interpretation:

  1. γσabs2(Tt)\gamma\sigma_{\text{abs}}^2(T-t): a risk premium that widens with volatility and time horizon. This term reflects the inventory risk component. Higher volatility means greater potential for adverse price moves while holding inventory, so you demand a wider spread as compensation. Similarly, a longer time horizon means more time during which prices could move adversely.

  2. 2γln(1+γ/k)\frac{2}{\gamma}\ln(1 + \gamma/k): a liquidity premium driven by the elasticity of order flow (kk). This term captures the trade-off between profit per trade and execution probability. When order flow is very price-sensitive (high kk), this term is small because you cannot afford to quote wide spreads without losing all your order flow. When order flow is price-insensitive (low kk), you can extract larger spreads.

Out[15]:
Visualization
Risk premium component of the optimal spread vs volatility. The risk premium expands with volatility, reflecting the cost of holding inventory in turbulent markets.
Risk premium component of the optimal spread vs volatility. The risk premium expands with volatility, reflecting the cost of holding inventory in turbulent markets.
Liquidity premium component vs order flow sensitivity ($k$). The premium dominates when flow is inelastic (low $k$) and shrinks when flow is competitive.
Liquidity premium component vs order flow sensitivity ($k$). The premium dominates when flow is inelastic (low $k$) and shrinks when flow is competitive.

This leads to optimal bid and ask prices:

Pbid=rδ2=Sqγσabs2(Tt)δ2Pask=r+δ2=Sqγσabs2(Tt)+δ2\begin{aligned} P^{\text{bid}} &= r - \frac{\delta^*}{2} = S - q\gamma\sigma_{\text{abs}}^2(T-t) - \frac{\delta^*}{2} \\[10pt] P^{\text{ask}} &= r + \frac{\delta^*}{2} = S - q\gamma\sigma_{\text{abs}}^2(T-t) + \frac{\delta^*}{2} \end{aligned}

where:

  • Pbid,PaskP^{\text{bid}}, P^{\text{ask}}: optimal bid and ask quotes
  • rr: reservation price
  • δ\delta^*: optimal spread
  • SS: current mid price
  • qq: current inventory
  • γ\gamma: risk aversion parameter
  • σabs\sigma_{\text{abs}}: absolute volatility
  • TtT-t: time remaining until horizon

These formulas show that both the bid and ask prices shift together based on inventory. The spread between them remains constant at δ\delta^*, but the center of that spread, the reservation price, moves to encourage inventory-reducing trades.

In[16]:
Code
import numpy as np
import pandas as pd


def avellaneda_stoikov_quotes(S, q, gamma, sigma, T_minus_t, k):
    """
    Calculate optimal bid and ask quotes using Avellaneda-Stoikov model.

    Parameters:
    -----------
    S : float - Current mid price
    q : float - Current inventory
    gamma : float - Risk aversion parameter
    sigma : float - Volatility (annualized)
    T_minus_t : float - Time to horizon (in years)
    k : float - Order book parameter (sensitivity of fill rate to price)

    Returns:
    --------
    tuple: (bid_price, ask_price, reservation_price, optimal_spread)
    """
    # Reservation price - shifts based on inventory
    reservation = S - q * gamma * sigma**2 * T_minus_t

    # Optimal spread
    spread = gamma * sigma**2 * T_minus_t + (2 / gamma) * np.log(1 + gamma / k)

    # Optimal bid and ask
    bid = reservation - spread / 2
    ask = reservation + spread / 2

    return bid, ask, reservation, spread


# Example parameters
S = 100.0  # Current price
sigma = 0.3  # Absolute volatility (annualized)
gamma = 0.1  # Risk aversion
k = 1.5  # Order book parameter
T_minus_t = 1 / 252  # One day to horizon

# Calculate quotes for different inventory levels
inventory_range = np.linspace(-50, 50, 101)
quotes_data = []

for q in inventory_range:
    bid, ask, res, spd = avellaneda_stoikov_quotes(
        S, q, gamma, sigma, T_minus_t, k
    )
    quotes_data.append(
        {
            "inventory": q,
            "bid": bid,
            "ask": ask,
            "reservation": res,
            "spread": spd,
        }
    )

quotes_df = pd.DataFrame(quotes_data)
Out[17]:
Visualization
Line chart showing bid, ask, and mid price lines shifting based on inventory position.
Optimal quote adjustment based on inventory position. As inventory becomes positive (long), both bid and ask prices shift downward relative to the mid-price to attract sellers and deter buyers, facilitating inventory reduction. The reservation price (green dashed line) tracks the private valuation adjusted for inventory risk.

The figure shows the core intuition of inventory-based quoting. When you are flat (inventory = 0), quotes are centered around the mid price. As inventory grows positive (long position), both quotes shift down to encourage buying from you. The reservation price, what you consider fair value given your position, drops to reflect the risk of holding long inventory. The magnitude of this shift increases with inventory size, reflecting the quadratic growth of inventory risk that we derived earlier.

Parameter Sensitivity

Understanding how model parameters affect optimal quotes helps you calibrate the model to real market conditions. Each parameter captures a different aspect of the market environment, and knowing their effects enables informed adjustment:

In[18]:
Code
import pandas as pd

# Analyze sensitivity to key parameters
param_analysis = []

# Vary risk aversion
for gamma_test in [0.05, 0.1, 0.2, 0.5]:
    bid, ask, res, spd = avellaneda_stoikov_quotes(
        100, 0, gamma_test, 0.3, 1 / 252, 1.5
    )
    param_analysis.append(
        {
            "parameter": "gamma",
            "value": gamma_test,
            "spread": ask - bid,
            "spread_bps": (ask - bid) / 100 * 10000,
        }
    )

# Vary volatility
for sigma_test in [0.1, 0.2, 0.3, 0.5]:
    bid, ask, res, spd = avellaneda_stoikov_quotes(
        100, 0, 0.1, sigma_test, 1 / 252, 1.5
    )
    param_analysis.append(
        {
            "parameter": "sigma",
            "value": sigma_test,
            "spread": ask - bid,
            "spread_bps": (ask - bid) / 100 * 10000,
        }
    )

# Vary time horizon
for T_test in [1 / 252, 5 / 252, 21 / 252, 63 / 252]:
    bid, ask, res, spd = avellaneda_stoikov_quotes(
        100, 0, 0.1, 0.3, T_test, 1.5
    )
    param_analysis.append(
        {
            "parameter": "T",
            "value": T_test * 252,  # Convert to days
            "spread": ask - bid,
            "spread_bps": (ask - bid) / 100 * 10000,
        }
    )

sensitivity_df = pd.DataFrame(param_analysis)
Out[19]:
Console
Optimal Spread Sensitivity Analysis
============================================================

Risk Aversion (gamma):
  gamma = 0.05: spread = $1.3116 (131.2 bps)
  gamma = 0.10: spread = $1.2908 (129.1 bps)
  gamma = 0.20: spread = $1.2517 (125.2 bps)
  gamma = 0.50: spread = $1.1509 (115.1 bps)

Volatility (sigma):
  sigma = 0.10: spread = $1.2908 (129.1 bps)
  sigma = 0.20: spread = $1.2908 (129.1 bps)
  sigma = 0.30: spread = $1.2908 (129.1 bps)
  sigma = 0.50: spread = $1.2909 (129.1 bps)

Time Horizon (T in days):
  T = 1 days: spread = $1.2908 (129.1 bps)
  T = 5 days: spread = $1.2909 (129.1 bps)
  T = 21 days: spread = $1.2915 (129.2 bps)
  T = 63 days: spread = $1.2930 (129.3 bps)
Out[20]:
Visualization
Spread sensitivity to risk aversion (γ). Higher risk aversion necessitates wider spreads to compensate for inventory risk.
Spread sensitivity to risk aversion (γ). Higher risk aversion necessitates wider spreads to compensate for inventory risk.
Spread sensitivity to volatility (σ). Higher volatility increases inventory risk, requiring wider spreads.
Spread sensitivity to volatility (σ). Higher volatility increases inventory risk, requiring wider spreads.
Spread sensitivity to time horizon ($T$). A longer time horizon increases the variance of terminal wealth, leading to wider protective spreads.
Spread sensitivity to time horizon ($T$). A longer time horizon increases the variance of terminal wealth, leading to wider protective spreads.

The sensitivity analysis reveals several important relationships that guide practical implementation:

  • Higher risk aversion leads to wider spreads; more cautious traders demand greater compensation for bearing inventory risk. If you have limited capital or strict risk limits, you would appropriately use a higher gamma value.
  • Higher volatility requires wider spreads to compensate for greater inventory risk. This explains why spreads widen during periods of market stress and narrow during calm periods. You dynamically adjust σ\sigma estimates based on recent realized volatility.
  • Longer horizons allow wider spreads because there's more time for adverse price moves. This is counterintuitive at first: one might think that more time allows prices to revert. However, the model captures the fact that more time means more uncertainty, and you must be compensated for bearing this uncertainty.

Implementing a Market Making Simulation

Let's build a complete simulation to see how these concepts work together in practice. We'll simulate your strategy using the Avellaneda-Stoikov model over a trading day. This simulation will illustrate how the theoretical framework translates into actual trading behavior and outcomes.

In[21]:
Code
import numpy as np
import pandas as pd


class MarketMakingSimulator:
    """
    Simulates a market making strategy using Avellaneda-Stoikov quotes.
    """

    def __init__(
        self,
        initial_price=100,
        volatility=0.3,
        gamma=0.1,
        k=1.5,
        lambda_base=5.0,
        dt=1 / (252 * 6.5 * 60),
    ):
        """
        Parameters:
        -----------
        initial_price : float - Starting mid price
        volatility : float - Annualized volatility
        gamma : float - Risk aversion
        k : float - Order book parameter
        lambda_base : float - Base arrival rate (orders per minute)
        dt : float - Time step (fraction of year)
        """
        self.S0 = initial_price
        self.sigma = volatility
        self.gamma = gamma
        self.k = k
        self.lambda_base = lambda_base
        self.dt = dt

    def simulate(self, n_steps, T_total):
        """
        Run simulation for n_steps.

        Parameters:
        -----------
        n_steps : int - Number of time steps
        T_total : float - Total time horizon in years

        Returns:
        --------
        dict with simulation results
        """
        np.random.seed(789)

        # Initialize tracking arrays
        prices = np.zeros(n_steps + 1)
        inventories = np.zeros(n_steps + 1)
        cash = np.zeros(n_steps + 1)
        bids = np.zeros(n_steps)
        asks = np.zeros(n_steps)
        trades = []

        prices[0] = self.S0

        for t in range(n_steps):
            S = prices[t]
            q = inventories[t]
            T_remaining = T_total - t * self.dt

            # Calculate optimal quotes
            bid, ask, _, _ = avellaneda_stoikov_quotes(
                S, q, self.gamma, self.sigma, max(T_remaining, 1e-6), self.k
            )

            bids[t] = bid
            asks[t] = ask

            # Calculate fill probabilities
            delta_bid = S - bid
            delta_ask = ask - S

            # Poisson arrival probabilities for this time step
            lambda_bid = (
                self.lambda_base
                * np.exp(-self.k * delta_bid)
                * self.dt
                * 252
                * 6.5
                * 60
            )
            lambda_ask = (
                self.lambda_base
                * np.exp(-self.k * delta_ask)
                * self.dt
                * 252
                * 6.5
                * 60
            )

            # Simulate arrivals (simplify to at most one order per time step)
            bid_fill = np.random.random() < lambda_bid
            ask_fill = np.random.random() < lambda_ask

            # Update inventory and cash
            inventories[t + 1] = inventories[t]
            cash[t + 1] = cash[t]

            if bid_fill:  # Market maker buys
                inventories[t + 1] += 1
                cash[t + 1] -= bid
                trades.append(
                    {"time": t, "side": "buy", "price": bid, "qty": 1}
                )

            if ask_fill:  # Market maker sells
                inventories[t + 1] -= 1
                cash[t + 1] += ask
                trades.append(
                    {"time": t, "side": "sell", "price": ask, "qty": 1}
                )

            # Simulate price movement (arithmetic Brownian motion)
            prices[t + 1] = (
                prices[t] + self.sigma * np.sqrt(self.dt) * np.random.randn()
            )

        # Calculate final P&L
        final_value = cash[-1] + inventories[-1] * prices[-1]
        initial_value = 0

        return {
            "prices": prices,
            "inventories": inventories,
            "cash": cash,
            "bids": bids,
            "asks": asks,
            "trades": pd.DataFrame(trades),
            "final_pnl": final_value - initial_value,
            "final_inventory": inventories[-1],
        }
In[22]:
Code
# Run simulation for a full trading day (6.5 hours, ~23,400 seconds)
# Using minute-level granularity
n_minutes = 390  # 6.5 hours
T_day = 1 / 252  # One trading day

simulator = MarketMakingSimulator(
    initial_price=100,
    volatility=0.3,
    gamma=0.1,
    k=1.5,
    lambda_base=2.0,  # Average 2 orders per minute at fair price
)

results = simulator.simulate(n_minutes, T_day)
Out[24]:
Console
Market Making Simulation Results - Full Trading Day
============================================================

Trading Activity:
  Total trades: 591
  Buys: 290
  Sells: 301

Position:
  Final inventory: -11 shares
  Max long: 4 shares
  Max short: -15 shares

P&L:
  Final P&L: $381.37
  Final inventory value: $-1100.14
  Cash position: $1481.51
Out[25]:
Visualization
Two panel chart with price and quotes on top, inventory over time on bottom.
Simulation of Avellaneda-Stoikov market making dynamics showing mid-price and quotes. The optimal bid and ask quotes fluctuate around the mid-price (black) to manage inventory risk.
Simulation of Avellaneda-Stoikov market making dynamics showing mid-price and quotes. The optimal bid and ask quotes fluctuate around the mid-price (black) to manage inventory risk.
Simulation of Avellaneda-Stoikov market making dynamics showing mid-price and quotes. The optimal bid and ask quotes fluctuate around the mid-price (black) to manage inventory risk.

The simulation illustrates how the market making strategy works in practice. The top panel shows the mid price evolution with your bid and ask quotes, while trade executions are marked with triangles. The bottom panel shows how inventory evolves over time. Notice how it tends to fluctuate around zero because the quotes encourage mean reversion. When inventory drifts positive, the lower quotes attract sellers to buy from you, pulling inventory back down. When inventory drifts negative, the higher quotes attract buyers to sell to you.

Key Parameters

The key parameters for the market making simulation are:

  • S0: Initial mid price. The starting point for the asset price simulation. This anchors all price calculations and determines the dollar value of positions.
  • σ: Annualized volatility (sigma). Higher volatility increases inventory risk and widens spreads. This parameter must be estimated from recent market data and updated regularly.
  • γ: Risk aversion parameter (gamma). Controls how aggressively you adjust quotes to manage inventory. Higher values lead to more aggressive inventory management but potentially less competitive quotes.
  • k: Order book density. Determines how quickly the probability of a fill decays as quotes move away from the mid price. This parameter captures market-specific characteristics of order flow.
  • A: Baseline arrival rate (lambda_base). The expected number of orders per unit time when quoting at the mid price. This sets the overall activity level of the simulation.

Risk Management for Market Makers

Successful market making requires sophisticated risk management beyond the theoretical models. Here we examine practical considerations that you must address in real-world implementation. The Avellaneda-Stoikov model provides a valuable framework, but you must augment it with additional safeguards and monitoring systems.

Position Limits and Circuit Breakers

You should typically impose hard limits on inventory positions. These limits serve as a safety mechanism to prevent catastrophic losses when models fail or market conditions become extreme. When inventory approaches these limits, you must take defensive action:

In[26]:
Code
def apply_risk_limits(bid, ask, inventory, max_inventory=100):
    """
    Apply risk management limits to quotes.

    Parameters:
    -----------
    bid, ask : float - Proposed quotes
    inventory : float - Current inventory
    max_inventory : float - Maximum allowed position

    Returns:
    --------
    tuple: (adjusted_bid, adjusted_ask)
    """
    # If at max long, don't bid (refuse to buy more)
    if inventory >= max_inventory:
        bid = None  # Pull bid

    # If at max short, don't offer (refuse to sell more)
    if inventory <= -max_inventory:
        ask = None  # Pull ask

    # Scale back size as approaching limits
    bid_size = max(0, (max_inventory - inventory) / max_inventory)
    ask_size = max(0, (max_inventory + inventory) / max_inventory)

    return bid, ask, bid_size, ask_size


# Example: inventory approaching limit
test_inventories = [-100, -50, 0, 50, 100]
limit_results = []

for inv in test_inventories:
    _, _, bid_sz, ask_sz = apply_risk_limits(
        99.95, 100.05, inv, max_inventory=100
    )
    status = ""
    if bid_sz == 0:
        status = " (bid pulled)"
    elif ask_sz == 0:
        status = " (ask pulled)"

    limit_results.append(
        {
            "inventory": inv,
            "bid_size_pct": bid_sz,
            "ask_size_pct": ask_sz,
            "status": status,
        }
    )
Out[27]:
Console
Risk-Adjusted Quote Sizes:
--------------------------------------------------
Inventory -100: Bid size 200%, Ask size 0% (ask pulled)
Inventory  -50: Bid size 150%, Ask size 50%
Inventory    0: Bid size 100%, Ask size 100%
Inventory   50: Bid size 50%, Ask size 150%
Inventory  100: Bid size 0%, Ask size 200% (bid pulled)
Out[28]:
Visualization
Risk management logic for quote sizing. Quote sizes are linearly reduced as inventory approaches the defined limits (dashed lines). This soft limit mechanism prevents you from breaching hard constraints while maintaining continuity in providing liquidity.
Risk management logic for quote sizing. Quote sizes are linearly reduced as inventory approaches the defined limits (dashed lines). This soft limit mechanism prevents you from breaching hard constraints while maintaining continuity in providing liquidity.

Volatility Regime Detection

You must adjust your behavior during high-volatility periods. Building on our discussion of volatility modeling in Part III, we can use realized volatility to dynamically adjust spreads. When volatility spikes, the Avellaneda-Stoikov model naturally suggests wider spreads, but the adjustment may need to be more aggressive than the model alone would suggest:

In[29]:
Code
import numpy as np


def dynamic_spread_multiplier(
    recent_returns, base_vol=0.01, max_multiplier=3.0
):
    """
    Calculate spread multiplier based on recent volatility regime.

    Parameters:
    -----------
    recent_returns : array - Recent return observations
    base_vol : float - Baseline expected volatility
    max_multiplier : float - Maximum spread widening factor

    Returns:
    --------
    float: Multiplier to apply to base spread
    """
    realized_vol = np.std(recent_returns)
    vol_ratio = realized_vol / base_vol

    # Linear scaling with cap
    multiplier = min(max_multiplier, max(1.0, vol_ratio))

    return multiplier, realized_vol


# Simulate different volatility regimes
np.random.seed(321)

# Normal regime
normal_returns = np.random.normal(0, 0.01, 60)  # 1% daily vol

# High volatility regime (e.g., during news)
high_vol_returns = np.random.normal(0, 0.025, 60)  # 2.5% daily vol

# Calculate multipliers
normal_mult, normal_vol = dynamic_spread_multiplier(normal_returns)
high_mult, high_vol = dynamic_spread_multiplier(high_vol_returns)

# Calculate example spreads
base_spread = 0.02
normal_spread_ex = base_spread * normal_mult
high_vol_spread_ex = base_spread * high_mult
Out[30]:
Console
Dynamic Spread Adjustment by Volatility Regime
==================================================

Normal Regime:
  Realized volatility: 0.91%
  Spread multiplier: 1.00x

High Volatility Regime:
  Realized volatility: 2.53%
  Spread multiplier: 2.53x

Example (base spread = 2%):
  Normal: spread = 2.00%
  High vol: spread = 5.05%

End-of-Day Risk

You face particular risk at the end of the trading day. Any remaining inventory must either be carried overnight (exposure to overnight gaps) or liquidated (potentially at unfavorable prices). The Avellaneda-Stoikov model captures this through the TtT - t term; as time to horizon shrinks, the urgency to flatten inventory increases. This urgency manifests as more aggressive quote adjustment: you become increasingly willing to trade at unfavorable prices to avoid the even greater risk of holding overnight positions.

In[31]:
Code
import numpy as np

# Simulate quote evolution for a market maker with 20-share long position
inventory = 20
S = 100

minutes_to_close = np.linspace(
    390, 1, 50
)  # From market open to 1 minute before close
T_remaining = minutes_to_close / (390 * 252)  # Convert to fraction of year

bids_eod = []
asks_eod = []

for T in T_remaining:
    bid, ask, _, _ = avellaneda_stoikov_quotes(S, inventory, 0.1, 0.3, T, 1.5)
    bids_eod.append(bid)
    asks_eod.append(ask)
Out[32]:
Visualization
Line chart showing bid-ask spread narrowing and shifting as time approaches market close.
End-of-day inventory liquidation dynamics. As the trading session nears its close (time flows right to left), holding a long position results in aggressive lowering of both bid and ask quotes. This behavior prioritizes inventory reduction over spread capture to avoid overnight gap risk.

The figure shows how you, holding 20 shares long, would adjust quotes throughout the day. As the close approaches, both bid and ask drop dramatically; you are increasingly willing to sell at lower prices to avoid overnight risk. The speed of this adjustment accelerates as the close nears, reflecting the non-linear increase in urgency as time runs out.

The Role of Speed and Technology

Modern market making is as much about technology as about models. The concepts we've discussed in this chapter assume you can update quotes instantaneously, but in practice there's always latency, the time between deciding to change a quote and that change taking effect in the market. This latency creates vulnerability to adverse selection, as faster participants can trade against stale quotes.

The Speed Arms Race

When prices move, you must cancel or update your stale quotes. If you are slow to cancel a sell order when prices rise, you risk being "picked off," having your order executed at a now-unfavorable price. This creates intense competition on latency, where firms invest enormous resources to gain even microsecond advantages:

  • Co-location: Placing servers in the same data center as the exchange minimizes the physical distance that signals must travel
  • FPGA/ASIC: Custom hardware for sub-microsecond processing bypasses the overhead of general-purpose computing
  • Microwave links: Faster-than-fiber communication between exchanges exploits the physics of electromagnetic wave propagation
  • Optimized code: Every nanosecond matters, driving investment in highly optimized, often hardware-specific software

The upcoming chapter on High-Frequency Trading and Latency Arbitrage will explore these technological considerations in depth.

Quote Lifetime and Fill Rates

Your speed determines how long your quotes are "live" and vulnerable to adverse selection. We can estimate the expected adverse price movement during a latency period Δt\Delta t using the square-root-of-time rule for diffusion processes. This rule follows from the properties of Brownian motion: the standard deviation of price changes grows with the square root of time.

E[ΔP]σmsΔtE[\Delta P] \approx \sigma_{\text{ms}} \sqrt{\Delta t}

where:

  • E[ΔP]E[\Delta P]: expected price move magnitude
  • σms\sigma_{\text{ms}}: price volatility per millisecond
  • Δt\Delta t: latency in milliseconds

Assuming a portion of these moves (e.g., 50%) result in you being "picked off" by faster traders, we can quantify the cost of latency.

In[33]:
Code
import numpy as np


def adverse_selection_by_latency(latencies_ms, price_vol_per_ms=0.001):
    """
    Estimate adverse selection cost for different latencies.

    Parameters:
    -----------
    latencies_ms : array - Latencies in milliseconds
    price_vol_per_ms : float - Expected price volatility per millisecond

    Returns:
    --------
    array: Expected adverse selection cost in price units
    """
    # Adverse selection proportional to price move during quote lifetime
    # Simplified model: informed traders can pick off quotes that are
    # stale by more than the current price move

    expected_move = price_vol_per_ms * np.sqrt(latencies_ms)
    adverse_cost = expected_move * 0.5  # Assume 50% of moves are exploitable

    return expected_move, adverse_cost


latencies = np.array([0.001, 0.01, 0.1, 1.0, 10.0, 100.0])  # 1 μs to 100 ms
moves, costs = adverse_selection_by_latency(latencies, price_vol_per_ms=0.0005)

# Format data for display
latency_data = []
for lat, move, cost in zip(latencies, moves, costs):
    latency_data.append(
        {"latency_val": lat, "move_bps": move * 10000, "cost_bps": cost * 10000}
    )
Out[34]:
Console
Adverse Selection Cost by Quote Latency
=======================================================
Latency         Expected Move        Adverse Cost   
-------------------------------------------------------
1 μs            0.16 bps           0.08 bps
10 μs           0.50 bps           0.25 bps
100 μs          1.58 bps           0.79 bps
1 ms            5.00 bps           2.50 bps
10 ms           15.81 bps           7.91 bps
100 ms          50.00 bps           25.00 bps
Out[35]:
Visualization
Estimated adverse selection cost as a function of system latency. The cost scales with the square root of time, following the diffusion of asset prices. While the absolute cost reduction diminishes at lower latencies, the relative competitive advantage of microsecond-level speed remains critical in winner-take-all liquidity provision.
Estimated adverse selection cost as a function of system latency. The cost scales with the square root of time, following the diffusion of asset prices. While the absolute cost reduction diminishes at lower latencies, the relative competitive advantage of microsecond-level speed remains critical in winner-take-all liquidity provision.

This simplified analysis shows why latency matters: if you are slower, you face higher adverse selection costs. A 100-millisecond delay might seem negligible to human perception, but in fast-moving markets, it can mean significant losses to faster traders who exploit stale quotes. The economics of latency explain why market making has become dominated by technology-intensive firms with the resources to compete at the frontier of speed.

Limitations and Impact

Quantitative market making models provide valuable frameworks for decision-making, but you must understand their constraints. This section examines both the theoretical limitations of the models and the practical challenges of real-world implementation.

Model Limitations

The quantitative models we've examined provide valuable intuition but have important limitations that you must understand:

The Avellaneda-Stoikov model assumes continuous trading, Gaussian price dynamics, and specific functional forms for order arrival rates. Real markets feature discrete ticks, fat-tailed distributions, and complex order flow dynamics. The model also treats all trades as uninformed, ignoring the adverse selection problem. This means the model's optimal quotes may be too aggressive in markets with significant informed trading.

Parameter estimation presents a significant challenge. The model requires values for γ\gamma (risk aversion) and kk (order book sensitivity), which are difficult to measure directly. You often calibrate these to historical data, but parameters may vary significantly across market conditions. A model calibrated to calm market conditions may perform poorly during periods of stress.

Market impact is largely ignored in basic models. Your own trading can move prices, especially in less liquid instruments. More sophisticated models incorporate this feedback effect, but doing so substantially increases complexity.

Real market making also involves multiple instruments traded simultaneously. Inventory in one stock may be partially hedged by positions in correlated stocks, ETFs, or futures. Multi-asset inventory management is considerably more complex than single-instrument models suggest, requiring portfolio-level optimization.

Practical Challenges

Beyond model limitations, real-world market making faces operational challenges that can be as important as theoretical considerations:

  • Technology costs: Building and maintaining a competitive trading infrastructure requires substantial investment in hardware, software, and connectivity. These fixed costs create barriers to entry and economies of scale.
  • Regulatory requirements: You might have affirmative obligations to provide liquidity and maintain orderly markets, particularly if you hold designated market maker status. These obligations can force you to provide liquidity even when it is unprofitable.
  • Inventory financing: Carrying positions requires capital and creates balance sheet exposure. The cost of capital affects the profitability threshold for market making activities.
  • Model risk: Systematic errors in models can lead to persistent losses before detection. Robust monitoring and model validation processes are essential.

Impact on Markets

Quantitative market making has fundamentally transformed financial markets over the past two decades. Bid-ask spreads have compressed dramatically, from eighths of a dollar to pennies or fractions of pennies. This benefits investors through lower transaction costs. A retail investor buying 100 shares of stock today pays a fraction of the transaction costs that would have applied in the 1990s.

However, the benefits come with trade-offs. Flash crashes, like the May 2010 event where the Dow Jones dropped nearly 1,000 points in minutes, revealed how quickly liquidity can evaporate when automated market makers simultaneously pull quotes. The reliance on speed has created barriers to entry, concentrating liquidity provision among a handful of highly capitalized technology firms.

The ongoing evolution of market structure, including changes to maker-taker fee models, minimum tick sizes, and transparency requirements, continues to reshape the competitive landscape for you. Understanding these dynamics is essential for anyone involved in quantitative trading or market design.

Summary

This chapter explored the economics, models, and risks of quantitative market making:

Core economics: You earn the bid-ask spread by providing immediacy to other traders. You buy at the bid and sell at the ask, capturing the difference. Profitability depends on managing inventory risk and avoiding adverse selection.

Inventory risk: Accumulated positions expose you to directional price risk. The Avellaneda-Stoikov model provides optimal quotes that shift based on inventory, encouraging mean reversion. When long, quotes shift lower; when short, quotes shift higher.

Adverse selection: Informed traders profit at your expense by trading when they have superior information. Spreads must be wide enough to compensate for losses to informed flow while remaining competitive for uninformed flow.

The Avellaneda-Stoikov model: This foundational framework produces optimal bid and ask quotes based on current inventory, volatility, risk aversion, and time horizon. The model introduces the reservation price concept: your private valuation adjusted for inventory risk.

Risk management: Practical market making requires position limits, volatility regime detection, and end-of-day protocols. The model parameters require careful calibration and ongoing monitoring.

Technology and speed: Modern market making is a technology race. Latency determines adverse selection exposure; if you are slower, you face higher costs from stale quotes being picked off. The upcoming chapter on High-Frequency Trading will explore these technological dimensions further.

Market making sits at the intersection of financial theory and engineering practice. The models provide a theoretical foundation, but successful implementation requires mastering the operational and technological challenges that define modern electronic markets.

Quiz

Ready to test your understanding? Take this quick quiz to reinforce what you've learned about market making and liquidity provision.

Loading component...

Reference

BIBTEXAcademic
@misc{marketmakingliquidityprovisionoptimalquotingmodels, author = {Michael Brenndoerfer}, title = {Market Making & Liquidity Provision: Optimal Quoting Models}, year = {2025}, url = {https://mbrenndoerfer.com/writing/market-making-liquidity-provision-optimal-quoting-strategies}, organization = {mbrenndoerfer.com}, note = {Accessed: 2025-01-01} }
APAAcademic
Michael Brenndoerfer (2025). Market Making & Liquidity Provision: Optimal Quoting Models. Retrieved from https://mbrenndoerfer.com/writing/market-making-liquidity-provision-optimal-quoting-strategies
MLAAcademic
Michael Brenndoerfer. "Market Making & Liquidity Provision: Optimal Quoting Models." 2026. Web. today. <https://mbrenndoerfer.com/writing/market-making-liquidity-provision-optimal-quoting-strategies>.
CHICAGOAcademic
Michael Brenndoerfer. "Market Making & Liquidity Provision: Optimal Quoting Models." Accessed today. https://mbrenndoerfer.com/writing/market-making-liquidity-provision-optimal-quoting-strategies.
HARVARDAcademic
Michael Brenndoerfer (2025) 'Market Making & Liquidity Provision: Optimal Quoting Models'. Available at: https://mbrenndoerfer.com/writing/market-making-liquidity-provision-optimal-quoting-strategies (Accessed: today).
SimpleBasic
Michael Brenndoerfer (2025). Market Making & Liquidity Provision: Optimal Quoting Models. https://mbrenndoerfer.com/writing/market-making-liquidity-provision-optimal-quoting-strategies