Python implementation of the Avellaneda-Stoikov (2008) optimal market-making strategy, with Poisson and Hawkes self-exciting fill models, head-to-head Monte Carlo against two baselines, and a γ-sensitivity sweep that recovers the expected interior-optimum Sharpe ratio.
Avellaneda, M. & Stoikov, S. (2008). High-frequency trading in a limit order book. Quantitative Finance 8(3), 217-224.
$ pytest tests/ -> 15 passed in 0.48s
$ python examples/run_sim.py (200 Monte Carlo paths per strategy)
Poisson (memoryless) fills
AvellanedaStoikov PnL = +57.31 +/- 5.85 Sharpe = 9.80 <|q|> = 1.03
Symmetric (q-blind) PnL = +60.97 +/- 10.86 Sharpe = 5.61 <|q|> = 4.21
Constant spread PnL = +60.97 +/- 11.83 Sharpe = 5.15 <|q|> = 4.37
Hawkes (self-exciting) fills
AvellanedaStoikov PnL = +215.29 +/- 12.35 Sharpe = 17.43 <|q|> = 0.78
Symmetric (q-blind) PnL = +220.89 +/- 29.30 Sharpe = 7.54 <|q|> = 10.12
Constant spread PnL = +232.16 +/- 26.08 Sharpe = 8.90 <|q|> = 9.56
AS trades a few cents of expected P&L for ~2x the Sharpe by skewing quotes away from accumulating inventory. Under self-exciting Hawkes fills (realistic order-flow clustering) the gap widens to >2x.
A market maker continuously quotes a bid S_t^b and ask S_t^a on a
risky asset with mid-price S_t evolving as a Brownian motion:
dS_t = σ · dW_t
The trader's cash x_t and inventory q_t evolve when their quotes are
hit. Fills arrive as point processes with intensity that decays
exponentially with quote distance from mid:
λ^a(δ^a) = A · exp(-k · δ^a), λ^b(δ^b) = A · exp(-k · δ^b)
Wealth at terminal T is W_T = x_T + q_T · S_T. The maker maximizes
the CARA-utility of terminal wealth:
max E[ -exp(-γ · W_T) ]
with risk aversion γ > 0.
Standard dynamic programming yields the Hamilton-Jacobi-Bellman equation
for the value function u(s, x, q, t). Avellaneda & Stoikov derive an
approximation valid in the high-frequency limit (the value function is
well-approximated by an exponential ansatz). Two closed-form quantities
fall out:
(1) The reservation price — the maker's indifference price given they
currently hold q shares of inventory:
r(s, q, t) = s − q · γ · σ² · (T − t) [eq. 9 in the paper]
The skew is linear in inventory and shrinks to zero as t → T (no future
inventory risk to hedge).
(2) The total quoted spread (ask side + bid side together):
δ^a + δ^b = γ · σ² · (T − t) + (2/γ) · ln(1 + γ/k) [eq. 10]
Decomposed into:
- Diffusion / risk-aversion term
γσ²(T−t): vanishes at terminal time - Liquidity / competition term
(2/γ)·ln(1 + γ/k): scales with how quickly fill intensity decays — denser quotes ifklarge
In the high-frequency approximation, quotes are placed symmetrically
around the reservation price r (not around the mid s):
bid_price = r − (δ^a + δ^b)/2 ask_price = r + (δ^a + δ^b)/2
All inventory skew enters through r. If the maker is long, r < s, so
their midpoint of quotes is below the public mid — they're trying to
unload by inviting buyers (ask near mid, bid far below).
| Limit | Predicted behavior |
|---|---|
q → 0 |
r → s, symmetric quotes about mid |
q > 0 (long) |
r < s, quotes skew DOWN |
q < 0 (short) |
r > s, quotes skew UP |
t → T |
diffusion term → 0, spread minimum |
σ → ∞ |
spread widens linearly |
k → ∞ |
liquidity term → 0, spread tightens |
λ(δ) = A · exp(-k · δ), memoryless. This is the AS paper's assumption
and yields a clean closed-form analysis.
Self-exciting:
λ(t, δ) = A · exp(-k · δ) · (1 + Σᵢ α · exp(-β · (t − tᵢ)))
Each fill bumps the intensity by α, which then decays at rate β. This
captures the empirical fact that order arrivals cluster in time
(Bacry, Mastromatteo & Muzy 2015). Under Hawkes fills the AS strategy's
inventory-control advantage becomes more important, because clustered
fills generate larger inventory excursions.
| Strategy | Inventory-aware? | Vol-aware? | Description |
|---|---|---|---|
AvellanedaStoikovMM |
yes | yes | Closed-form HJB-optimal (the paper) |
SymmetricMM |
no | yes | AS spread but no inventory skew |
ConstantSpreadMM |
no | no | Fixed half-spread around mid |
All implement the same quote(s, q, t, T, sigma) -> (bid, ask) interface,
so the simulator runs them through an identical sequence of mid-price
innovations and fill events.
Same fill model, same RNG seeds, same parameters across strategies.
| Strategy | Poisson Sharpe | Hawkes Sharpe | Poisson <|q|> | Hawkes <|q|> |
|----------------------|---------------:|--------------:|----------------:|---------------:|
| AvellanedaStoikov| 9.80 | 17.43 | 1.03 | 0.78 |
| Symmetric (q-blind) | 5.61 | 7.54 | 4.21 | 10.12 |
| Constant spread | 5.15 | 8.90 | 4.37 | 9.56 |
Reading the table: AS more than doubles the risk-adjusted return by constraining inventory variance to 1-2 shares instead of 4-10 shares.
examples/sensitivity.py sweeps γ over four orders of magnitude:
gamma=0.005 Sharpe= 6.5 <|q|>=3.3 (too risk-neutral, inventory swings)
gamma=0.089 Sharpe=10.1 <|q|>=1.2 <-- optimum on this fill model
gamma=5.000 Sharpe= 2.9 <|q|>=0.04 (too risk-averse, quotes too wide, miss fills)
The Sharpe-vs-γ curve has a clean interior optimum. Too little risk aversion → unconstrained inventory; too much → quotes so skewed the maker misses fills. The sweet spot lives at γ ≈ 0.09 for this fill model.
examples/plot_paths.py reproduces the AS Figure 2 layout: a single
sample path of mid + (bid, ask) quotes + inventory + cumulative P&L.
pip install -r requirements.txt
pytest tests/ # 15 passed in 0.48s
python examples/run_sim.py # head-to-head Monte Carlo
python examples/plot_paths.py # AS paper Figure 2 replication
python examples/sensitivity.py # γ sweep with optimum localizationas_mm/
model.py - reservation_price, optimal_spread, optimal_quotes
simulator.py - Simulator + PoissonFillModel + HawkesFillModel
strategies.py - AS / Symmetric / ConstantSpread interfaces
analytics.py - summarize() across MC paths
tests/
test_model.py - 15 tests for closed-form limit behavior
examples/
run_sim.py - head-to-head comparison
plot_paths.py - single-path visualization
sensitivity.py - γ-Sharpe curve
pytest tests/ -> 15/15 passing. Coverage of the closed-form formulas:
- reservation at
q=0equals mid - long inventory lowers reservation, short inventory raises it
- skew scales linearly with inventory (eq. 9)
- skew shrinks to zero at terminal
- spread decomposes additively (diffusion + liquidity)
- spread monotonic in
σ, monotonic-inverse ink - invalid inputs raise
ValueError - quotes symmetric about reservation (the high-frequency approximation)
- Inventory penalty extension — Cartea-Jaimungal-Penalva 2015 add a
running quadratic penalty
-φ · q²to the running utility; closed form exists. - Stochastic intensity calibration — fit
A,kto actual LOB market-by-order data (Bouchaud-Farmer-Lillo Empirical Market Microstructure). - Multi-asset — joint hedging across correlated names; the HJB becomes vector-valued but stays tractable.
- Discrete-tick variant — quotes restricted to the tick grid; small but nonzero impact on the optimal-spread formula.
- Avellaneda, M. & Stoikov, S. (2008). High-frequency trading in a limit order book. Quantitative Finance 8(3): 217-224.
- Cartea, A., Jaimungal, S. & Penalva, J. (2015). Algorithmic and High-Frequency Trading. Cambridge UP. Ch. 10 expands AS with inventory penalty + alpha signals.
- Bacry, E., Mastromatteo, I. & Muzy, J.-F. (2015). Hawkes processes in finance. Market Microstructure and Liquidity 1(1).
- Guéant, O., Lehalle, C.-A. & Fernandez-Tapia, J. (2013). Dealing with the inventory risk: a solution to the market making problem. — finite-horizon refinement of AS.