Tuning Spread and Inventory Skew in a Market-Making Bot
A practical walkthrough of Bayesian optimisation with Optuna to jointly tune bid-ask spread, inventory skew intensity, and order-refresh interval on a Hyperliquid maker strategy — including how to penalise drawdown in the objective function without overfitting to a single regime.
Running a market-making bot on Hyperliquid without systematic parameter tuning is roughly equivalent to trading on feel — you will get lucky in trending regimes and destroyed in choppy ones. Spread, inventory skew, and refresh interval interact in non-obvious ways: tighten spread to capture more flow and your inventory exposure spikes; widen it to reduce risk and you lose queue position. Getting these three levers right, jointly, is where real alpha lives on a maker strategy.
The Parameter Space and Why It Is Not Separable
Most teams tune spread first, then skew, then refresh. That sequential approach misses the coupling. A wider spread tolerates more inventory imbalance before the skew kicks in enough to matter; a faster refresh reduces adverse selection but increases gas and taker-spread costs on Hyperliquid's fee ladder. You are optimising a surface, not three independent lines.
Concretely, the parameters you are tuning look like this:
base_spread_bps— symmetric half-spread posted on each side, typically 2–15 bps on mid-cap perpsskew_intensity— multiplier that shifts bid and ask quotes as a linear or sigmoid function of net inventory (measured in USD notional)refresh_interval_s— how often the bot cancels and resets its resting orders
The joint search space has enough curvature that grid search becomes expensive fast. A 10×10×10 grid means 1,000 backtests. With realistic simulation latency (accounting for fill probability, partial fills, and funding), each run takes 5–15 seconds. That is hours of compute before you even consider walk-forward validation.
Optuna for Bayesian Optimisation
Optuna uses a Tree-structured Parzen Estimator (TPE) by default. TPE builds a probabilistic model of where good parameters are likely to be, rather than sampling blind. In practice this means you converge on a promising region in 80–150 trials rather than 1,000, which matters when your backtest engine is not embarrassingly cheap to run.
import optuna
def objective(trial):
spread = trial.suggest_float("base_spread_bps", 2.0, 15.0)
skew = trial.suggest_float("skew_intensity", 0.1, 3.0)
refresh = trial.suggest_int("refresh_interval_s", 5, 60)
return run_backtest(spread, skew, refresh)
study = optuna.create_study(direction="maximize")
study.optimize(objective, n_trials=120, n_jobs=4)
n_jobs=4 runs trials in parallel across CPU cores. Each worker seeds a different random state so the TPE sampler does not duplicate work. On a modest 8-core machine this halves wall-clock time without compromising the search quality.
Designing the Objective Function
The naive objective is Sharpe ratio over the backtest window. The problem is that Sharpe rewards consistent small gains and can be gamed by a bot that simply refuses to trade in volatile periods — which a refresh interval of 60 seconds effectively achieves during fast moves.
A better objective penalises drawdown explicitly:
def score(pnl_series: pd.Series) -> float:
sharpe = pnl_series.mean() / (pnl_series.std() + 1e-9) * (252 ** 0.5)
max_dd = (pnl_series.cumsum() - pnl_series.cumsum().cummax()).min()
return sharpe + 0.4 * max_dd # max_dd is negative, so this penalises it
The 0.4 weight on drawdown is itself a hyperparameter, but it is best treated as a design choice rather than something you tune — otherwise you risk fitting the penalty weight to the backtest regime. Set it based on your actual risk tolerance and leave it fixed across all trials.
One anti-pattern we see frequently: including the drawdown penalty only during the optimisation window and not during walk-forward. Do both. If your best trial collapses on the out-of-sample window specifically because drawdown was not penalised during live conditions, the bug is in your evaluation harness, not the algorithm.
Inventory Skew Mechanics
Inventory skew shifts your quotes asymmetrically based on net position. If you are long 50k USD notional of SOL-PERP, you want your ask tighter (or even at mid) and your bid wider to mean-revert the position passively. The skew function we use in production is a clamped linear transform:
ask_adjustment = clamp(inventory_usd / max_inventory * skew_intensity, -max_spread, 0)
bid_adjustment = clamp(-inventory_usd / max_inventory * skew_intensity, 0, max_spread)
max_inventory is a hard limit you set — say 100k USD — beyond which the bot stops posting on the side that would increase exposure. Never let the bot skew past the point where one side crosses mid. Posting a bid above mid is a taker order on Hyperliquid's matching engine and you will pay crossing fees immediately.
The Optuna trials will find that skew_intensity values above roughly 2.5 hurt Sharpe significantly in ranging markets because the bot widens quotes so aggressively that it stops filling entirely, accumulates unrealised inventory from open positions drifting, and then takes a large adverse move. The useful range in practice is 0.5–1.8.
Avoiding Regime Overfitting
The single biggest failure mode in parameter search is fitting to one volatility regime. Run your objective function across at least three distinct historical windows — a trending period, a mean-reverting period, and a high-volatility period — and average the scores. Use weighted averaging if you have a view on which regimes are more likely going forward.
Concretely, instead of one run_backtest(...) call, aggregate:
scores = [run_backtest(spread, skew, refresh, window=w) for w in REGIME_WINDOWS]
return np.average(scores, weights=[0.3, 0.4, 0.3])
Walk-forward validation on a held-out 30-day window is non-negotiable before deploying. If your in-sample top-10 trials cluster tightly in parameter space but scatter wildly in out-of-sample performance, your backtest has a data leak — usually look-ahead in the mark price or funding calculation.
The maker strategy architecture we build for clients separates the parameter search harness from the execution engine entirely. Optuna runs against a replay engine, not the live gateway, so there is zero risk of accidentally placing real orders during a tuning run.
If you want these systems built, tested, and running in production rather than spending weeks on the infrastructure, get in touch.
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