Arena lesson
Market making
Most strategies bet on direction. A market maker doesn't — it quotes both sides of the book and earns the spreadas others trade against it. It's the perfect first agent for the Arena: you'll feel spread capture, the maker rebate, and the two risks that make it hard — inventory and adverse selection — all measurable on a real recorded order book.
The idea
Post a bid a little below the mid and an aska little above it. If a seller hits your bid and, later, a buyer lifts your ask, you've bought low and sold high — pocketing the spread without ever predicting the price. Do this thousands of times and the edge compounds. On exchanges with a maker rebate(you get paid for providing liquidity), you earn that on top. The Arena's real-book season uses a maker rebate and a taker fee, so providing liquidity is rewarded and crossing the spread costs you — exactly like a real venue.
Why it's hard: two risks
- Inventory risk. Every fill leaves you holding a position. If your bid keeps getting hit, you accumulate a long inventory right as the price may be falling — your spread profits get swamped by a directional loss. You must lean against inventory.
- Adverse selection.Your quote gets filled precisely when someone knows something you don't — the market is about to move through your price. Makers are systematically picked off by informed flow; the spread you earn has to compensate for it.
- Queue position. At a given price, fills go first-in-first-out. Re-quoting every tick is simple but sends you to the back of the line; quoting patiently keeps your place. (See how matching works.)
The starter agent
Here's the core of a working market maker (full file on GitHub). It cancels its old quotes, recomputes a bid and ask around the mid, and skews both quotes against its current inventory so fills naturally pull it back toward flat:
from convexpi.arena.client import RemoteAgent, MarketState
class MarketMaker(RemoteAgent):
half_spread_bps = 8.0 # how far each quote sits from the mid
size = 5 # quote size per side
max_pos = 40 # hard inventory cap
max_skew_bps = 8.0 # max quote shift at full inventory
def on_tick(self, state: MarketState) -> list[dict]:
if state.mid is None:
return []
orders = [self.cancel(o["order_id"]) for o in state.my_open_orders]
mid = state.mid
half = mid * self.half_spread_bps / 1e4
# lean against inventory: long -> shift quotes down (sell more, buy less)
skew = (state.position / self.max_pos) * (mid * self.max_skew_bps / 1e4)
if state.position < self.max_pos:
orders.append(self.limit("buy", round(mid - half - skew), self.size))
if state.position > -self.max_pos:
orders.append(self.limit("sell", round(mid + half - skew), self.size))
return ordersThat's the whole strategy. When flat, the quotes are symmetric around the mid; as inventory grows, the skew shifts both quotes to unload it, and the position cap stops either side from running away.
Run it
Install the SDK, then point the agent at the real-order-book competition:
pip install convexpi-arena
python examples/market_maker.py my-handle \
--server wss://arena-production-e3f1.up.railway.appWatch your fills print, and your PnL and maker % climb the board on /compete/arena-book— where you can also see the live depth ladder you're quoting into.
Now make it better
The starter is deliberately naive. Ideas that move the needle:
- • Keep queue priority: only re-quote when the mid moves enough to matter, instead of cancelling every tick.
- • Widen in fast markets: scale your half-spread with recent volatility so you're not picked off during moves.
- • Smarter skew: make the inventory penalty non-linear, or quote different sizes on each side.
- • Respect the fees: your spread must clear the round-trip cost after the maker rebate — quote too tight and you pay to trade.
- • Read the tape: back off when recent trades show one-sided, informed flow.
Go quote the book
Clone the starter, run it against the live book, and tune it. The spread is there to be earned — if your quotes are smart enough to survive the inventory and the informed flow.

