σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
uantCraft AcademySkills
Ctrl K
AO
+1.2k Learners
LoginJoin ↗
Skills
AO
+1.2k Learners
Login
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Python skill›Module 11 · Production systems & frontier models›Topic 11.6·~1 hr 30 minExpert

Market making

Skill positionModule 11 of 11 · Topic 6 of 13
11.6 Market making
—Lab score— / 100Total XP—Streak—

Earning the spread, carrying the risk

A market maker posts a bid and an ask and waits. When a seller hits the bid and a buyer lifts the ask, the maker buys low and sells high and keeps the spread. That is the income. The cost is inventory risk: between the two trades the maker holds a position whose value moves with the price, and the maker is more often filled by someone who knows more (adverse selection) than by someone who does not. Good market making is a balance between quoting tight enough to be hit often and wide enough to be paid for the risk, while keeping the inventory near zero.

# The income: buy at the bid, sell at the ask, keep the spread
bid, ask, size = 99.95, 100.05, 100
cash = -bid * size + ask * size
print(round(cash, 2))                                # 10.0    a round trip of 100 shares earned the 0.10 spread

# The risk: between the two trades the price moves against the inventory
inventory, mid_after = 100, 99.80
print(round(inventory * (mid_after - bid), 2))       # -15.0   the position lost more than the spread would have earned

Avellaneda-Stoikov: where to quote

The Avellaneda-Stoikov model gives a closed-form answer. First, the reservation price r = mid - q * gamma * sigma^2 * tau is where the maker would be happy to trade given the inventory q: a long maker (q > 0) should value the asset below the mid and quote lower to attract buyers, a short maker should quote higher. Risk aversion gamma, volatility sigma and time left tau set how hard to lean. Second, the optimal spread gamma * sigma^2 * tau + (2 / gamma) * ln(1 + gamma / k) has two parts: a charge for the risk of holding the position until the horizon, and a term for the order flow, where k says how quickly fills dry up as you quote farther from the mid. The quotes are r - spread/2 and r + spread/2.

# The reservation price leans against inventory: long -> quote lower, short -> quote higher
def reservation_price(mid, inventory, gamma, sigma, tau):
    return mid - inventory * gamma * sigma ** 2 * tau

for inventory in (-50, 0, 50):
    print(inventory, round(reservation_price(100.0, inventory, 0.1, 0.2, 1.0), 4))
# -50 100.2   short: quote higher to buy back
#   0 100.0   flat: centred on the mid
#  50  99.8   long: quote lower to sell
# The optimal spread and the two quotes around the reservation price
import math

def optimal_spread(gamma, sigma, tau, k):
    return gamma * sigma ** 2 * tau + (2 / gamma) * math.log(1 + gamma / k)

def make_quotes(mid, inventory, gamma, sigma, tau, k):
    centre = mid - inventory * gamma * sigma ** 2 * tau
    half = optimal_spread(gamma, sigma, tau, k) / 2
    return centre - half, centre + half

print(round(optimal_spread(0.1, 0.2, 1.0, 1.5), 4))                       # 1.2948
bid, ask = make_quotes(100.0, 0, 0.1, 0.2, 1.0, 1.5)
print(round(bid, 4), round(ask, 4))                  # 99.3526 100.6474   symmetric around the mid when flat
bid, ask = make_quotes(100.0, 50, 0.1, 0.2, 1.0, 1.5)
print(round(bid, 4), round(ask, 4))                  # 99.1526 100.4474   long 50: both quotes move down by 0.2

Inventory, fills and the P&L

Track the state as two numbers, inventory and cash. When the bid is hit you bought: inventory rises and cash falls by price * quantity; when the ask is hit you sold: the reverse. Your wealth at any moment is cash plus inventory marked at the mid, cash + inventory * mid, and the change in it is your P&L. That P&L comes from the spread captured and from the price moves while holding inventory; quoting around the reservation price pulls inventory back to zero because the side that reduces it becomes more attractive. Limits matter in practice: a hard cap on inventory, a pause in fast markets, and a check on every quote that the bid is below the ask.

# Booking fills and marking the book to market
def apply_fill(state, side, price, quantity):
    inventory, cash = state
    if side == "bid":
        return inventory + quantity, cash - price * quantity
    return inventory - quantity, cash + price * quantity

state = (0, 0.0)
state = apply_fill(state, "bid", 99.95, 100)         # our bid is hit: we buy 100
print(state)                                         # (100, -9995.0)
state = apply_fill(state, "ask", 100.05, 100)        # our ask is hit: we sell 100
print(state)                                         # (0, 10.0)   flat again, and the spread is the profit
inventory, cash = apply_fill(state, "bid", 99.9, 50)
print(round(cash + inventory * 99.9, 2))             # 10.0   marked at the price we just paid, the equity is unchanged
A model is a starting pointThe model assumes a random-walk mid price, constant volatility and a smooth exponential order-arrival curve. Real markets have queues, tick sizes, fees and rebates, jumps, and informed traders who pick off stale quotes. Treat the formulas as a way to reason about the direction of each effect (long inventory: quote lower; more volatility: wider), then calibrate and test on recorded data in a simulator before risking money, with hard limits that no model can override.
  • A market maker earns the spread but carries inventory risk and adverse selection; the aim is tight enough to trade, wide enough to be paid, and inventory near zero.
  • Reservation price mid - q * gamma * sigma^2 * tau leans against inventory; the spread gamma * sigma^2 * tau + (2/gamma) ln(1 + gamma/k) grows with risk aversion-weighted volatility and time.
  • Track inventory and cash, mark to the mid, enforce hard inventory limits, and test the strategy in a simulator on recorded data before using it live.

Next up: the Heston model, volatility that is itself random.

Lab · what's graded

  • Lean against inventory with reservation_price25 pts
  • Set the width with optimal_spread25 pts
  • Quote both sides with make_quotes25 pts
  • Book a fill with apply_fill25 pts
Explorer · lab-135-1
▾ lab-135-1
…
bash · in-browser
learner@quantcraft:~/lab-135-1$
Python not loaded yetPythonLn 1, Col 1Spaces: 4UTF-8

Reset this topic?

Everything in lab-135-1 goes back to the starter files and the terminal is cleared. Your current solution will be lost — this can't be undone.

Will be reset
  • main.py → starter code
  • Terminal history and test output
  • Unsubmitted changes
Will be kept
  • Best lab score
  • Your XP and streak
  • Lecture progress and notes

Log in to run your code.

Every lecture is free to read. Create an account or log in to run code, run tests and submit this lab.

  • In-browser IDE with a bash terminal — nothing to install
  • Instant grading, XP and your place on the skill
Create an accountLog in
OR
Continue with GitHub