Inventory Management Using Avellaneda-Stoikov On-Chain
Translates the classic Avellaneda-Stoikov continuous-time market-making model into discrete on-chain quote updates, with a worked Rust implementation for Hyperliquid. Discusses how to calibrate the risk-aversion parameter using realised volatility from a rolling 5-minute OHLCV window.
The Avellaneda-Stoikov (AS) model from 2008 remains the cleanest theoretical framework for a market maker who wants to quote both sides while penalising inventory accumulation. Most implementations live in centralised exchange environments where you reprice every few milliseconds. On Hyperliquid, where you commit quotes to a decentralised order book with finite transaction throughput, the discrete adaptation matters enormously — and the calibration details most write-ups skip are exactly where production bots live or die.
The Model in One Paragraph
AS gives you two quantities: a reservation price and a spread. The reservation price is the mid-price adjusted for your current inventory and the risk you are carrying:
r = s - q * γ * σ² * T
Where s is the current mid, q is your signed inventory (positive = long), γ is risk aversion, σ is volatility, and T is time remaining in the "session" (more on this shortly). The optimal symmetric spread around r is:
δ_bid = δ_ask = γ * σ² * T + (2/γ) * ln(1 + γ/κ)
κ is the order arrival intensity — how aggressively market orders hit at a given distance from mid. In continuous time, both equations fall out of a Hamilton-Jacobi-Bellman equation. In production, you discretise and re-solve on every quote cycle.
Mapping Continuous Time to On-Chain Cycles
The original model assumes you can reprice instantaneously. On Hyperliquid, each cancel-and-replace round trip costs gas and takes at minimum one block. Our bots service is built around a quote-cycle abstraction: a Tokio task wakes every N milliseconds, reads the current state, computes new quotes, and fires a batch cancel/replace if the quotes have drifted beyond a threshold.
The key change from the textbook: T is not time-to-close-of-day. Instead, treat it as a rolling session length — we use 300 seconds (5 minutes) as the horizon. This keeps the spread from collapsing to zero as your arbitrary "session" end approaches. At the start of each 5-minute window, reset T = 300. As the window progresses, T decrements. At reset, carry your inventory forward — do not pretend you are flat.
Concretely, if you are 30 seconds into a 300-second window and you are long 2 ETH with γ = 0.1 and σ freshly computed, the reservation price will shade below mid proportionally. Your ask quote stays near mid; your bid quote moves away from mid. You lean on the side that reduces inventory.
Calibrating γ From Realised Volatility
Most teams hardcode γ and tune it by feel. The better approach is to make γ a function of recent vol so the spread widens automatically in stressed conditions.
We compute σ from a rolling 5-minute OHLCV window pulled from the Hyperliquid info API:
fn realised_vol(candles: &[Ohlcv]) -> f64 {
let log_returns: Vec<f64> = candles
.windows(2)
.map(|w| (w[1].close / w[0].close).ln())
.collect();
let mean = log_returns.iter().sum::<f64>() / log_returns.len() as f64;
let variance = log_returns.iter()
.map(|r| (r - mean).powi(2))
.sum::<f64>() / log_returns.len() as f64;
variance.sqrt() * (12.0_f64).sqrt() // annualise from 5-min bars
}
We target a base spread of roughly 4 bps in calm markets. Working backwards from the spread formula with typical κ values for SOL-PERP, that implies γ in the range 0.05–0.15. Rather than a single value, we use:
let gamma = (BASE_GAMMA * (sigma / SIGMA_TARGET)).clamp(0.05, 0.5);
SIGMA_TARGET is your "normal" annualised vol — set it empirically from 30 days of historical data. When realised vol runs double the target, γ doubles, spreads widen, and inventory risk appetite shrinks. This is adaptive risk aversion, and it has saved our positions on multiple sharp intraday moves.
Order Sizing and Depth
The AS model is silent on size. We layer three price levels per side:
- Level 1 (closest): 30% of max position size, at the AS-derived quote
- Level 2: 50% of max, 1.5× the AS spread further out
- Level 3: 20% of max, 3× the AS spread, essentially a backstop
Max position size is computed from your capital allocation and a Kelly-inspired fraction of expected edge. Do not quote level 3 when inventory already exceeds 60% of your single-side limit — you are not trying to accumulate more of what you are already long.
Practical Failure Modes
σ spikes during low-liquidity periods. A single 2% candle on thin Sunday-morning tape will send γ to its ceiling and widen your spreads so far you stop getting filled. We gate the vol computation: if fewer than 15 trades occurred in the last 5-minute window, we substitute the trailing 1-hour vol instead.
Inventory drift under trending conditions. AS assumes a mean-reverting mid. On a trending day, q accumulates relentlessly on one side. You need a hard inventory cap — we use ±$25k notional per market — beyond which the bot quotes only on the reducing side and cancels the accumulating side entirely. This is not in the model; it is risk management.
Stale candle data. Hyperliquid's info endpoint occasionally returns cached candles. We validate that the most recent candle close timestamp is within 2× the candle interval; otherwise we pause quoting rather than trade on stale vol.
What You Actually Ship
The final Rust struct holding quote state looks roughly like:
struct QuoteState {
mid: f64,
inventory_notional: f64,
sigma: f64,
gamma: f64,
session_remaining_secs: f64,
reservation_price: f64,
half_spread: f64,
}
Each cycle: fetch mid and candles, recompute sigma and gamma, decrement session_remaining_secs, recompute reservation_price and half_spread, diff against live orders, and fire only the cancels and places that actually changed beyond a minimum tick threshold. Unnecessary churn burns gas and increases adverse selection exposure.
The AS model is not magic — it is a disciplined way to encode the inventory-versus-edge trade-off into your quotes rather than managing it reactively after the fact. Get the calibration right, add sensible hard limits, and it holds up in production.
If you want to run a model like this on Hyperliquid or another venue without building the infrastructure from scratch, talk to us — we design, build, and operate these systems end-to-end.
Need a bot like this built?
We design, build and run trading bots on Solana, Hyperliquid and Polymarket.
Start a projectMore from the blog
Public vs Paid RPC: How to Run a Fair Benchmark
A fair comparison of public and paid RPC endpoints must account for quotas, workload, freshness and support guarantees.
Read articleRPC Benchmark Percentiles Explained: p50, p95 and p99
Why averages hide the latency spikes that break trading bots, wallets and production blockchain applications.
Read articleHyperliquid REST vs WebSocket Benchmark: What to Measure
A benchmark plan for Hyperliquid market data that separates request latency from streaming freshness and recovery.
Read article