Market Making and Liquidity Provision › Risk Management for Market Makers
Quantitative FinanceAll chapters 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:
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,
}
)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)

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:
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_multDynamic 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 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.
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)
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.