Taming Inventory Risk: Building a Smarter Crypto Market Maker with Avellaneda–Stoikov | by DolphinDB | MediumSitemapOpen in appSign up<br>Sign in
Medium Logo
Get app<br>Write
Search
Sign up<br>Sign in
Taming Inventory Risk: Building a Smarter Crypto Market Maker with Avellaneda–Stoikov
DolphinDB
5 min read·<br>Mar 17, 2026
Listen
Share
Introduction<br>Market making is one of the most demanding strategies in quantitative trading. A market maker simultaneously quotes both sides of the order book — posting a bid and an ask at all times — and profits from the spread between them. Do it well, and the spread income compounds steadily. Do it poorly, and adverse price moves leave you holding an inventory position you never wanted, at a loss you didn’t price in.<br>Press enter or click to view image in full size
The core challenge is inventory risk . Every filled order skews your position. If you’re not actively adjusting your quotes in response, you accumulate directional exposure that can quickly erode spread profits.<br>The Avellaneda-Stoikov (AS) model , introduced in the 2008 paper High-Frequency Trading in a Limit Order Book by Marco Avellaneda and Sasha Stoikov, is one of the most influential frameworks for addressing exactly this problem. Rather than quoting symmetrically around the mid-price, the model continuously adjusts quotes based on current inventory, market volatility, and the market maker’s risk tolerance — dynamically balancing spread income against inventory risk.<br>This post walks through a full implementation of the AS market-making strategy on the BTC/USDT perpetual contract, backtested at snapshot frequency using DolphinDB’s cryptocurrency backtesting engine.<br>The Avellaneda-Stoikov Model<br>The AS model computes optimal quotes in two steps.<br>Step 1: Reservation Price<br>The market maker first calculates a reservation price — the price at which they are indifferent between holding and trading, given their current inventory:
s: market mid-price<br>q: market maker’s current inventory<br>γ: market maker’s risk aversion coefficient<br>σ: market price volatility<br>T: normalized end time<br>t: current time<br>The intuition is straightforward: the larger the inventory or the higher the volatility, the more the reservation price shifts away from mid, reducing the incentive to accumulate further exposure.<br>Step 2: Optimal Spread<br>The model then computes the optimal bid-ask spread around the reservation price:
δ_a, δ_b: symmetric bid-ask spread<br>γ, σ, T, t: same as the previous formula<br>k: market liquidity<br>This formula shows that a higher risk aversion, higher market volatility, or better market liquidity leads to a larger optimal spread for the market maker, compensating for risk.<br>Final Quotes
Strategy Implementation<br>Initialization<br>The initialize callback runs once when the engine starts. It sets the AS model parameters and initializes state variables for tracking prices and generating daily trade summaries:<br>def initialize(mutable context){<br>print("initialize")<br>Backtest::setUniverse(context["engine"], context.Universe)<br>// Onsnapshot Parameters<br>context["sigma"] = 0.025 // market volatility<br>context["gamma"] = 0.1 // inventory risk aversion parameter<br>context["k"] = 1.5 // order book liquidity parameter<br>context["amount"] = 0.001 // order amount<br>//...<br>context["lastprice"] = NULL<br>// Daily Trade Summary<br>context['dailyReport'] = table(1000:0,<br>[`SecurityID,`tradeDate,`BuyVolume,`BuyAmount,`SellVolume,`SellAmount,`transactionCost,`closePrice,`rev],<br>[SYMBOL,DATE,DOUBLE,DOUBLE,DOUBLE,DOUBLE,DOUBLE,DOUBLE,DOUBLE])<br>}Snapshot Callback<br>The core strategy logic runs in onSnapshot, which fires on every incoming order book snapshot. The logic follows seven steps:<br>Use orderInterval as the order frequency—for example, check, calculate, and place orders every second.<br>Cancel existing orders before placing new ones.<br>Calculate the current market mid-price from the best bid and ask.<br>Retrieve the current long and short positions.<br>Compute quotes based on the AS model. Note the decimal precision of the order price — for BTC/USDT, the minimum price increment is 0.1.<br>Submit buy and sell orders.<br>Manage inventory risk and close positions in a timely manner.<br>def onSnapshot(mutable context, msg, indicator){<br>istock = context["istock"][0]<br>if(context["lastprice"]0){<br>Backtest::cancelOrder(context["engine"],istock)<br>// 3. Calculate Mid Price<br>askPrice0 = msg[istock]["offerPrice"][0]<br>bidPrice0 = msg[istock]["bidPrice"][0]<br>midPrice = (askPrice0+bidPrice0)/2<br>// 4. Get Position<br>pos = Backtest::getPosition(context["engine"],istock,"futures")<br>longPos = pos['longPosition']<br>shortPos = pos['shortPosition']<br>netPos = nullFill(pos['longPosition']-pos['longPosition'],0)<br>if(count(netPos) == 0){<br>netPos = 0<br>// 5. Calculate order price<br>gamma = context["gamma"]<br>sigma = context["sigma"]<br>k = context["k"]<br>amount = context["amount"]<br>endTime = timestamp(context["tradeDate"])<br>timeToEnd = (endTime-t)\86400000<br>reservePrice = midPrice - netPos * gamma * square(sigma) * timeToEnd<br>spread = gamma * square(sigma)...