Market Making and Liquidity Provision › Risk Management for Market Makers

Michael BrenndoerferDecember 31, 20256 min read
Quantitative FinanceAll chapters
Part 12 of 15Market Making and High-Frequency Trading
Chapter 65Market Making and Liquidity Provision › Risk Management for Market Makers
Reading section 9 of 13 · Risk Management for Market Makers · 6 minSection 9 of 13 · 6 min

Book contents

Section 9 of 13 · 6 min read

Market Making and Liquidity Provision › Risk Management for Market MakersOne section · Sections in this chapter9 / 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

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
Line chart showing bid and ask quote sizes as a percentage decreasing linearly toward zero as inventory approaches the maximum long or short limit.
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 horizon-dependent inventory risk through the T−tT - t term. In the baseline finite-horizon solution, however, the inventory adjustment shrinks as the horizon approaches; the model does not create a last-minute liquidation order by itself. A strategy that must finish flat needs an explicit terminal inventory penalty or execution rule.

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, 5.0, T, 1.5)
    bids_eod.append(bid)
    asks_eod.append(ask)
Out[32]:
Visualization
Line chart showing the center of the bid and ask quotes returning toward the mid-price as minutes to market close decline, while the quoted spread remains positive.
Finite-horizon Avellaneda-Stoikov quotes for a long inventory position. As the close approaches (time flows right to left), the inventory adjustment and risk-premium term shrink, so the center of the quoted spread returns toward the mid-price while a liquidity-premium spread remains. Forced end-of-day liquidation requires an additional terminal inventory penalty or execution rule.

The figure exposes an important limitation of the basic finite-horizon solution. With 20 shares long, the inventory adjustment is largest when more time remains and fades as the horizon approaches, while the spread narrows toward its liquidity-premium floor. The center of the quotes therefore returns toward the mid-price near the close, but the quotes do not automatically become liquidation orders. A production strategy that must finish flat needs an explicit terminal inventory penalty, market-order schedule, or hard liquidation rule in addition to the baseline model.