Davina Titus
Write-up · Python · Stochastic control

Testing a market-making model

A market maker earns the spread but carries inventory risk. I implemented the classic model for balancing the two, then tried to see if it holds up.

Code ↗Live demo ↗Avellaneda & Stoikov (2008)

01The problem

A market maker posts a bid and an ask at the same time and profits from the gap between them. The catch is inventory. If I buy at my bid and the price then drops, I lose more than the spread I earned. Quote too wide and nobody trades with me; quote too tight and I take on risk for almost nothing.

In 2008, Marco Avellaneda and Sasha Stoikov solved this as an optimal control problem and got a closed-form answer. I wanted to implement it and check whether it actually beats the obvious strategy.

02The model

The setup has three pieces:

Solving the resulting Hamilton-Jacobi-Bellman equation gives two formulas:

# reservation price: where I'd be indifferent, given my inventory q
r  = s − q·γ·σ²·(T − t)

# optimal total spread around r
δ* = γ·σ²·(T − t) + (2/γ)·ln(1 + γ/k)

bid = r − δ*/2      ask = r + δ*/2

The key idea: when I'm holding inventory, I shift both quotes so the market is more likely to take it off my hands.

If I'm long (q > 0), the reservation price drops below the mid, so my ask gets cheaper and my bid less attractive. That nudges my position back toward zero. The (T − t) term also means I care less about inventory risk near the end of the session, so the spread tightens.

03The simulation

I simulate a session of length T = 1 in 1,000 steps. At each step the mid price takes a random step, and each of my quotes fills with probability 1 − e^(−λ(δ)·dt), the exact probability of at least one arrival in that interval. Inventory is capped at ±5.

To make the comparison fair, the naive strategy faces the same price path and the same random order arrivals. It quotes a fixed symmetric spread around the mid and ignores its inventory. One detail matters here: if each strategy's random numbers were drawn only when needed, the two would end up consuming different random streams and the comparison would stop being apples to apples. So every step draws all of them, whether or not they get used.

04Results

One path can be luck, so I ran 300 independent sessions for each setting and averaged. These numbers use the default parameters in my code, with prices normalized to start at 1.

Default settingsA-SNaive
Mean final PnL62.760.2
Sharpe ratio8.66.6
Mean max drawdown−1.6−3.3
Inventory std. dev.1.42.2
Paths where A-S earns more58%

The interesting result isn't that A-S makes more money. It's only slightly ahead on profit, and it loses to naive quoting on about 4 in 10 paths. The real difference is risk: it roughly halves the drawdown and keeps inventory much closer to zero, which is exactly what the model is designed to do.

Changing the parameters showed when that edge matters most:

SettingSharpe A-S / naiveDrawdown A-S / naiveA-S wins
Calm market (σ = 1)9.5 / 8.7−1.2 / −1.564%
Default (σ = 2, γ = 0.1)8.6 / 6.6−1.6 / −3.358%
Volatile market (σ = 3)7.5 / 4.8−1.9 / −5.259%
More risk-averse (γ = 0.5)6.3 / 4.2−1.1 / −3.771%

In a calm market the two strategies are close. As volatility or risk aversion goes up, ignoring inventory gets expensive, and the gap in drawdown widens a lot. Sharpe ratios here are in the simulation's own time units, so they're useful for comparing strategies, not as real-world figures.

05What the model leaves out

Building this made the model's assumptions very concrete:

06What I'd do next