Market Making·Advanced Techniques·Advanced
Multi Level Quote Engine
Build a multi-level quoting engine that simultaneously posts limit orders at several price levels away from the mid-price on both sides of the book with a configurable size distribution curve, capturing spreads at multiple depth layers of the limit order book simultaneously.
advanced-techniquesmarket-making
Multi-Level Quote Engine — Market Making
Category: Market Making | Subcategory: Advanced
What This Notebook Does
Rather than posting a single bid and ask, sophisticated market makers post quotes at multiple price levels simultaneously (a 'ladder'). This approach has several advantages:
- Captures fills at different price points without manual intervention
- Each deeper level provides worse pricing but larger size (compensates for taking further-away price risk)
- Naturally provides liquidity across the full order book depth
- Better handles large incoming orders that would otherwise sweep through a single level
This notebook:
- Defines a multi-level quote ladder structure (price and size per level)
- Implements inventory-skewed pricing across all levels simultaneously
- Calculates exponentially increasing spreads for deeper levels (compensates for price risk)
- Simulates the full ladder running over synthetic order flow with level-by-level fill tracking
- Measures fill distribution across levels and adjusts ladder parameters
- Exports quote ladder state and fill history
Quote Ladder Design Principles
| Level | Distance from Mid | Size | Spread Multiple | Purpose |
|---|---|---|---|---|
| Level 1 (best) | Tight (2-5 bps) | Small | 1× | Capture most fills, earn rebates |
| Level 2 | Moderate (5-10 bps) | Medium | 2× | Better fills, slightly more exposure |
| Level 3 | Wide (10-20 bps) | Large | 4× | Large sweeps, high spread revenue |
| Level 4 (deepest) | Very wide (20+ bps) | Largest | 8× | Extreme moves only, very high reward |
[1]
!pip install numpy pandas matplotlib seaborn --quiet[2]
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from dataclasses import dataclass, field
from typing import List, Dict, Tuple
import warnings
warnings.filterwarnings('ignore')
%matplotlib inline
plt.rcParams['figure.figsize'] = (13, 5)
plt.rcParams['axes.spines.top'] = False
plt.rcParams['axes.spines.right'] = False
sns.set_palette('husl')
print('Imports ready.')Imports ready.
Section 2 — Configuration
[3]
N_LEVELS = 4 # number of bid levels and ask levels each
LEVEL_1_SPREAD = 0.0004 # level 1 half-spread (4 bps from mid each side)
SPREAD_MULTIPLIER = 2.0 # each deeper level's spread = previous × this
LEVEL_SIZES = [0.05, 0.10, 0.20, 0.40] # BTC size per level (increasing depth = bigger size)
MAX_INVENTORY = 2.00 # hard inventory limit
SKEW_FACTOR = 0.5 # how much to shift quotes when inventory is non-zero
SIMULATION_STEPS = 4_000
START_PRICE = 50_000.0
MAKER_REBATE_BPS = 2.0Section 3 — Quote Ladder Engine
[4]
@dataclass
class QuoteLevel:
"""
Represents a single resting quote on one side of the ladder.
Attributes
----------
level_num : int
Level index (1 = best/tightest, N = deepest/widest).
side : str
'bid' or 'ask'.
price : float
Posted price for this level.
size : float
Posted size in base asset.
spread_from_mid : float
Distance from mid price as a decimal fraction.
"""
level_num: int
side: str
price: float
size: float
spread_from_mid: float
def build_quote_ladder(
mid: float,
inventory: float,
n_levels: int,
level_1_spread: float,
spread_multiplier: float,
level_sizes: List[float],
max_inventory: float,
skew_factor: float
) -> List[QuoteLevel]:
"""
Build a complete bid-ask ladder with inventory skew applied.
Parameters
----------
mid : float
Current market mid-price.
inventory : float
Current net inventory in base asset.
n_levels : int
Number of levels on each side.
level_1_spread : float
Half-spread for level 1 (decimal fraction).
spread_multiplier : float
Factor by which each deeper level's spread increases.
level_sizes : list of float
Quote size in base asset per level.
max_inventory : float
Hard inventory limit for size capping.
skew_factor : float
Skew shift per unit of normalized inventory (shifts entire ladder).
Returns
-------
list of QuoteLevel
All bid and ask levels in the current ladder.
Notes
-----
The reservation price (mid adjusted for inventory) shifts the entire ladder:
positive inventory → shift both bid and ask lower (want to sell)
negative inventory → shift both bid and ask higher (want to buy back)
This is the key insight from Avellaneda-Stoikov: quote around the
'reservation price', not the raw mid-price.
"""
# Reservation price: adjust mid for inventory risk
inv_norm = inventory / max_inventory # normalize to [-1, 1]
reservation_price = mid * (1 - skew_factor * inv_norm * level_1_spread)
levels = []
current_spread = level_1_spread
for i in range(n_levels):
raw_size = level_sizes[i] if i < len(level_sizes) else level_sizes[-1]
# Cap bid side when long, cap ask side when short
inv_fraction = abs(inventory) / max_inventory
if inventory > 0:
bid_size_cap = raw_size * max(0, 1 - inv_fraction * (i + 1) / n_levels)
ask_size = raw_size
else:
bid_size_cap = raw_size
ask_size = raw_size * max(0, 1 - inv_fraction * (i + 1) / n_levels)
bid_price = reservation_price * (1 - current_spread)
ask_price = reservation_price * (1 + current_spread)
if bid_size_cap > 0.001:
levels.append(QuoteLevel(i + 1, 'bid', bid_price, bid_size_cap, current_spread))
if ask_size > 0.001:
levels.append(QuoteLevel(i + 1, 'ask', ask_price, ask_size, current_spread))
current_spread *= spread_multiplier
return levels
# Quick test
test_ladder = build_quote_ladder(
50000, 0.5, N_LEVELS, LEVEL_1_SPREAD, SPREAD_MULTIPLIER,
LEVEL_SIZES, MAX_INVENTORY, SKEW_FACTOR
)
print('Sample ladder (inventory=0.5 BTC):')
for lvl in test_ladder:
print(f' Level {lvl.level_num} {lvl.side:4s}: price={lvl.price:.2f} size={lvl.size:.3f} spread={lvl.spread_from_mid*10000:.1f}bps')Sample ladder (inventory=0.5 BTC): Level 1 bid : price=49977.50 size=0.047 spread=4.0bps Level 1 ask : price=50017.50 size=0.050 spread=4.0bps Level 2 bid : price=49957.50 size=0.088 spread=8.0bps Level 2 ask : price=50037.50 size=0.100 spread=8.0bps Level 3 bid : price=49917.50 size=0.163 spread=16.0bps Level 3 ask : price=50077.50 size=0.200 spread=16.0bps Level 4 bid : price=49837.51 size=0.300 spread=32.0bps Level 4 ask : price=50157.49 size=0.400 spread=32.0bps
Section 4 — Simulation
[5]
def simulate_multi_level_mm(
n_steps: int,
start_price: float,
n_levels: int,
level_1_spread: float,
spread_multiplier: float,
level_sizes: List[float],
max_inventory: float,
skew_factor: float,
maker_rebate_bps: float
) -> pd.DataFrame:
"""
Run a full multi-level market making simulation.
At each tick: rebuild the ladder, simulate fills at each level based on
incoming order flow, update inventory and PnL.
Parameters
----------
n_steps : int
Number of simulation ticks.
start_price : float
Initial mid-price.
n_levels : int
Number of quote levels per side.
level_1_spread : float
Tightest half-spread (decimal fraction).
spread_multiplier : float
Spread growth factor per level.
level_sizes : list of float
Base size per level in BTC.
max_inventory : float
Inventory hard limit.
skew_factor : float
Inventory skew strength.
maker_rebate_bps : float
Rebate per maker fill in basis points.
Returns
-------
pd.DataFrame
Simulation history with per-level fill counts and PnL.
"""
np.random.seed(42)
tick_vol = 0.02 / np.sqrt(288)
rebate_dec = maker_rebate_bps / 10_000
mid = start_price
inventory = 0.0
cash = 0.0
level_fills = {f'lvl{i+1}_bid': 0 for i in range(n_levels)}
level_fills.update({f'lvl{i+1}_ask': 0 for i in range(n_levels)})
records = []
for t in range(n_steps):
mid += mid * np.random.normal(0, tick_vol)
mid = max(mid, 1.0)
ladder = build_quote_ladder(
mid, inventory, n_levels, level_1_spread,
spread_multiplier, level_sizes, max_inventory, skew_factor
)
# Fill probability decreases with level distance from mid
for quote in ladder:
fill_prob = 0.15 / (quote.level_num ** 1.5)
if np.random.rand() < fill_prob:
if quote.side == 'bid':
inventory += quote.size
cash -= quote.size * quote.price
cash += quote.size * quote.price * rebate_dec
level_fills[f'lvl{quote.level_num}_bid'] += 1
else:
inventory -= quote.size
cash += quote.size * quote.price
cash += quote.size * quote.price * rebate_dec
level_fills[f'lvl{quote.level_num}_ask'] += 1
mtm_pnl = cash + inventory * mid
record = {'tick': t, 'mid': mid, 'inventory': inventory,
'mtm_pnl': mtm_pnl, 'cash': cash}
record.update(dict(level_fills))
records.append(record)
return pd.DataFrame(records)
sim = simulate_multi_level_mm(
SIMULATION_STEPS, START_PRICE, N_LEVELS,
LEVEL_1_SPREAD, SPREAD_MULTIPLIER, LEVEL_SIZES,
MAX_INVENTORY, SKEW_FACTOR, MAKER_REBATE_BPS
)
print(f'Simulation complete. Final MtM PnL: ${sim["mtm_pnl"].iloc[-1]:.2f}')
fill_cols = [c for c in sim.columns if c.startswith('lvl')]
print('Total fills per level:')
print(sim[fill_cols].iloc[-1].to_string())Simulation complete. Final MtM PnL: $13070.10 Total fills per level: lvl1_bid 579 lvl2_bid 209 lvl3_bid 105 lvl4_bid 81 lvl1_ask 604 lvl2_ask 239 lvl3_ask 97 lvl4_ask 74
Section 5 — Visualization
[6]
def plot_ladder_analysis(sim: pd.DataFrame, n_levels: int) -> None:
"""
Four-panel visualization of multi-level quote engine performance.
Parameters
----------
sim : pd.DataFrame
Simulation output.
n_levels : int
Number of ladder levels.
"""
fig, axes = plt.subplots(2, 2, figsize=(15, 10))
# Panel 1: MtM PnL
axes[0, 0].plot(sim['tick'], sim['mtm_pnl'], color='gold', linewidth=1.0)
axes[0, 0].axhline(0, color='black', linewidth=0.8, linestyle='--')
axes[0, 0].set_title('Mark-to-Market PnL')
axes[0, 0].set_ylabel('PnL (USD)')
# Panel 2: Inventory
axes[0, 1].plot(sim['tick'], sim['inventory'], color='steelblue', linewidth=0.8)
axes[0, 1].axhline(0, color='black', linewidth=0.8, linestyle='--')
axes[0, 1].axhline( MAX_INVENTORY, color='red', linewidth=0.8, linestyle=':')
axes[0, 1].axhline(-MAX_INVENTORY, color='red', linewidth=0.8, linestyle=':')
axes[0, 1].set_title('Inventory')
axes[0, 1].set_ylabel('BTC')
# Panel 3: Fill distribution by level
fill_cols = [c for c in sim.columns if c.startswith('lvl')]
fill_totals = sim[fill_cols].iloc[-1]
colors = ['green' if 'bid' in c else 'red' for c in fill_totals.index]
axes[1, 0].bar(range(len(fill_totals)), fill_totals.values, color=colors, alpha=0.7, edgecolor='white')
axes[1, 0].set_xticks(range(len(fill_totals)))
axes[1, 0].set_xticklabels(fill_totals.index, rotation=45, ha='right', fontsize=8)
axes[1, 0].set_title('Total Fills by Level and Side')
axes[1, 0].set_ylabel('Fill Count')
# Panel 4: Sample ladder visualization
sample_mid = sim['mid'].iloc[-1]
sample_inv = sim['inventory'].iloc[-1]
sample_ladder = build_quote_ladder(
sample_mid, sample_inv, n_levels, LEVEL_1_SPREAD,
SPREAD_MULTIPLIER, LEVEL_SIZES, MAX_INVENTORY, SKEW_FACTOR
)
bid_levels = [(l.price, l.size) for l in sample_ladder if l.side == 'bid']
ask_levels = [(l.price, l.size) for l in sample_ladder if l.side == 'ask']
if bid_levels:
prices, sizes = zip(*sorted(bid_levels, reverse=True))
axes[1, 1].barh(range(len(prices)), sizes, color='green', alpha=0.7, label='Bids')
axes[1, 1].set_yticks(range(len(prices)))
axes[1, 1].set_yticklabels([f'${p:.0f}' for p in prices])
if ask_levels:
prices_a, sizes_a = zip(*sorted(ask_levels))
offset = len(bid_levels) if bid_levels else 0
axes[1, 1].barh(range(offset, offset + len(prices_a)), sizes_a, color='red', alpha=0.7, label='Asks')
axes[1, 1].set_yticks(range(offset + len(prices_a)))
axes[1, 1].axhline(len(bid_levels) - 0.5, color='black', linewidth=1.0, linestyle='--')
axes[1, 1].set_title(f'Sample Ladder (inventory={sample_inv:.3f} BTC)')
axes[1, 1].set_xlabel('Quote Size (BTC)')
axes[1, 1].legend()
plt.tight_layout()
plt.show()
plot_ladder_analysis(sim, N_LEVELS)Section 6 — Export
[7]
def export_ladder_results(sim: pd.DataFrame) -> None:
"""
Export multi-level simulation results.
Parameters
----------
sim : pd.DataFrame
Simulation history.
"""
sim.to_csv('multi_level_quote_simulation.csv', index=False)
print(f'Exported multi_level_quote_simulation.csv ({len(sim)} ticks)')
export_ladder_results(sim)Exported multi_level_quote_simulation.csv (4000 ticks)
Summary & Next Steps
Key Takeaways
- Multi-level quoting significantly increases fill rate compared to a single level
- Level 1 (tightest) captures the most fills but at the smallest spread per fill
- Deeper levels rarely fill but when they do, the spread captured is much larger
- Inventory skew applied to the reservation price naturally rebalances position across all levels
- Fill distribution analysis reveals whether the ladder is too tight (Level 1 dominates) or too wide (no fills at outer levels)