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:
- The mid price moves randomly, as Brownian motion with volatility
σ. - Buyers and sellers arrive at random. The further my quote sits from the mid, the less often it fills: the arrival rate is
λ(δ) = A·e^(−kδ). - The market maker is risk-averse, with a risk-aversion parameter
γ.
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 settings | A-S | Naive |
|---|---|---|
| Mean final PnL | 62.7 | 60.2 |
| Sharpe ratio | 8.6 | 6.6 |
| Mean max drawdown | −1.6 | −3.3 |
| Inventory std. dev. | 1.4 | 2.2 |
| Paths where A-S earns more | 58% | |
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:
| Setting | Sharpe A-S / naive | Drawdown A-S / naive | A-S wins |
|---|---|---|---|
| Calm market (σ = 1) | 9.5 / 8.7 | −1.2 / −1.5 | 64% |
| Default (σ = 2, γ = 0.1) | 8.6 / 6.6 | −1.6 / −3.3 | 58% |
| Volatile market (σ = 3) | 7.5 / 4.8 | −1.9 / −5.2 | 59% |
| More risk-averse (γ = 0.5) | 6.3 / 4.2 | −1.1 / −3.7 | 71% |
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:
- No adverse selection. Here, whoever trades with me knows nothing about where the price is going. In real markets, informed traders are exactly the ones who fill your quotes right before the price moves against you.
- My trades don't move the price. The mid evolves independently of my orders.
- Constant volatility and fill rates. Real markets change their behavior during the day.
06What I'd do next
- Add adverse selection, so fills are correlated with the next price move, and see how much of the edge survives.
- Calibrate
σ,Aandkfrom real order book data instead of choosing them. - Run the strategy against my own C++ order book, so its quotes compete with other orders in an actual book.