Market making: the strategy exchanges literally pay you to run

How market makers earn the spread, why inventory is the real enemy, and a minimal quoting loop built on watchOrderBook, postOnly orders, and hard risk limits.

By CCXT Team

Every trade needs a counterparty, and most of the time the counterparty is a market maker: someone continuously quoting both a buy price and a sell price, earning the gap between them. Exchanges want that liquidity so badly they charge makers lower fees than takers — often zero, sometimes negative (a rebate). This post explains the mechanics and builds a minimal quoting loop with CCXT — a teaching skeleton, not a production system, and market making with real size is a genuinely hard, competitive business.

The business model

A market maker places a limit buy below the current price and a limit sell above it. When both fill — someone sells into your bid, someone else buys from your ask — you've bought low and sold high, capturing the spread, and you're back to flat. Repeat thousands of times.

Three numbers govern the P&L:

  • Spread captured per round trip (your quoted spread, minus adverse price movement).
  • Maker fees — negotiate the tier; makers live and die on fee schedules. Check exchange.markets[symbol]['maker'].
  • Inventory losses — the reason this isn't free money, and the subject of most of the literature.

Inventory: the real enemy

If price trends down, your bids keep filling and your asks don't — you accumulate the asset all the way down. This is adverse selection: the people trading with you know something (or are simply the front edge of a move), and you're systematically on the wrong side of it.

Every serious market-making model is at heart an inventory-control policy. The classic reference is Avellaneda–Stoikov (2008): quote around a reservation price that leans away from your inventory — long inventory pushes both quotes down (eager to sell, reluctant to buy), short inventory pushes both up. Our skeleton below uses the simplest version of that idea: a linear skew.

A minimal quoting loop

import ccxt from 'ccxt';

const SYMBOL = 'BTC/USDT';
const SPREAD = 0.002;            // 20 bps quoted spread, fees-inclusive
const ORDER_SIZE = 0.001;
const MAX_INVENTORY = 0.005;     // hard position bound, in base currency
const REFRESH = 5000;            // ms between re-quotes

const exchange = new ccxt.pro.binance({ apiKey: '...', secret: '...' });
exchange.setSandboxMode(true);   // learn on the testnet
await exchange.loadMarkets();
let inventory = 0;               // track via watchMyTrades in real code
try {
    while (true) {
        const book = await exchange.watchOrderBook(SYMBOL);
        const mid = (book.bids[0][0] + book.asks[0][0]) / 2;
        // lean quotes against inventory (poor man's Avellaneda-Stoikov)
        const skew = -(inventory / MAX_INVENTORY) * (SPREAD / 2);
        const bid = exchange.priceToPrecision(SYMBOL, mid * (1 - SPREAD / 2 + skew));
        const ask = exchange.priceToPrecision(SYMBOL, mid * (1 + SPREAD / 2 + skew));
        await exchange.cancelAllOrders(SYMBOL);
        if (inventory < MAX_INVENTORY) {
            await exchange.createOrder(SYMBOL, 'limit', 'buy', ORDER_SIZE, bid, { postOnly: true });
        }
        if (inventory > -MAX_INVENTORY) {
            await exchange.createOrder(SYMBOL, 'limit', 'sell', ORDER_SIZE, ask, { postOnly: true });
        }
        await exchange.sleep(REFRESH);
    }
} finally {
    await exchange.cancelAllOrders(SYMBOL);   // never exit with quotes resting
    await exchange.close();
}

What the CCXT pieces are doing:

  • watchOrderBook streams the book over WebSockets (see the WebSocket post) — quoting off polled REST data means quoting off stale prices.
  • postOnly: true guarantees your order rests as a maker. If it would cross the spread and execute as a taker (paying taker fees and taking the wrong side), the exchange rejects it instead. Non-negotiable for a maker strategy.
  • cancelAllOrders + re-place is the simple re-quote. Where the exchange supports it (check exchange.has['editOrder']), editOrder amends price in place — fewer round trips, and you often keep queue position on exchanges that preserve it for size-down amendments.
  • priceToPrecision because quotes at invalid ticks are rejected — see mistake #1.

What's deliberately missing: real inventory tracking (subscribe to watchMyTrades and update inventory on every fill), volatility-aware spread sizing, order-book-imbalance signals, and multi-level quoting. Those are your roadmap, in that order.

Risk controls that are not optional

  • Hard inventory bounds — the skeleton stops quoting one side at the bound. Real systems also actively unwind back toward flat.
  • A kill switch you have tested — one command that cancels everything and flattens. cancelAllOrders exists on the unified API for exactly this. Some exchanges also support dead-man's-switch semantics (auto-cancel on disconnect) via exchange-specific parameters — worth wiring up where available.
  • Spread floors during volatility. When the book thins out or trades start printing through levels, widen or pull quotes. The makers who survive news events are the ones who weren't quoting through them.
  • Fee-aware spread. Your quoted spread must exceed round-trip fees plus expected adverse selection. If your maker fee is 0.02% per side, a 0.1% spread nets 0.06% before adverse selection — which will claim most of it.

Where this goes next

Run the skeleton on a testnet against a liquid pair and watch what happens to inventory during a trend — that lesson is worth more than any parameter tuning. Then read Avellaneda–Stoikov and the queue-position literature, and look at how the open-source market-making bots structure the same loop. The plumbing — books, quotes, cancels, fills — is the same six-language unified API throughout, which means your first market maker can be built in the language your team already speaks.