Skip to main content
QuantDXB

Markets and trading · 14 min read

Market making

Quoting both sides for a living: the spread you earn, the inventory you carry, the informed traders who pick you off, and how skew keeps risk in check.

Before you start

  • The limit order book (this track)
  • Random walks and Brownian motion

By the end you'll be able to

  • Split a market maker's P&L into spread earned and inventory P&L
  • Explain the trade-off in choosing a spread
  • Explain adverse selection and how market makers measure it
  • Use inventory skew to control risk, and simulate it

A market maker stands ready to buy and to sell at the same time, quoting a bid slightly below the price and an ask slightly above. If buyers and sellers arrive evenly, it buys low and sells high all day and keeps the spread. Two things get in the way: the position it builds up while waiting for the other side, and traders who know where the price is going. This lesson simulates both and shows how quoting decisions trade one risk against another.

TermMeaning
half-spreadThe distance of each quote from the mid price
inventoryThe position the market maker holds: positive when long, negative short
spread earnedThe sum of the distances between each fill and the mid at that moment
inventory P&LGains and losses on the position as the price moves
adverse selectionBeing traded against mostly by people who know more than you
skewShifting both quotes to push inventory back toward zero

How a market maker earns

Suppose the mid is 100.00 and the market maker quotes 99.97 bid and 100.03 ask, a half-spread of 3 cents. A seller hits the bid: the market maker buys at 99.97, three cents below the mid. Later a buyer lifts the ask: it sells at 100.03. If the mid hasn't moved, the round trip earned 6 cents.

So the P&L splits into two parts:

P&L=∑fills∣fill price−mid∣⏟spread earned+∑tqt Δmidt⏟inventory P&L,\text{P\&L} = \underbrace{\sum_{\text{fills}} \lvert \text{fill price} - \text{mid} \rvert}_{\text{spread earned}} + \underbrace{\sum_{t} q_t \,\Delta \text{mid}_t}_{\text{inventory P\&L}},

where qtq_t is the inventory held while the mid moves by Δmidt\Delta \text{mid}_t. The spread part is almost always positive. The inventory part is where the risk lives.

The model

The chart simulates 2,000 steps of a stylised market:

  • The mid price follows a random walk.
  • Uninformed traders arrive on both sides. The closer the quote to the mid, the more likely a trade: the chance per step falls off exponentially with distance.
  • Optionally, some steps bring an informed trader who knows the next price move (a jump of 10 cents) and trades just before it happens.
  • With skew on, both quotes shift against the inventory: when long, quote lower, so selling is more likely and buying less.
3¢
Skew quotes to inventory
Informed traders
Spread earned
0.00
0 trades
Inventory P&L
0.00
Total P&L
0.00
Largest position
0
The market maker quotes a bid and an ask around the mid. Tight quotes trade often but earn little each time; wide quotes earn more per trade but trade rarely. Turn skew off and the position wanders, and so does the P&L. Add informed traders and tight quotes get picked off.

Things to try:

  • Spread width. At 1 cent the market maker trades constantly but earns little per trade. At 8 cents it earns a lot per trade but almost nobody trades. The total is highest in between.
  • Skew off. The inventory wanders far from zero, and the total P&L line drifts away from the dashed spread line: the market maker has become a speculator by accident.
  • Informed traders on. Every informed trade is a loss: the market maker sells just before a rise or buys just before a fall. Tight quotes suffer most, because informed traders can profit from any quote closer than the jump.

Key idea. The spread pays for two risks: holding inventory while the price moves, and trading with people who know more. Quote too tight and you pay for both; quote too wide and you don't trade.

What the numbers say

Running the same model 200 times for each setting, with skew on (the code is below):

Informed flowHalf-spreadAverage P&LSpread of P&L across runs
none1¢16.901.94
none3¢28.662.35
none6¢24.182.62
5% of steps1¢7.653.17
5% of steps3¢21.863.65
5% of steps6¢19.854.44

The best half-spread is in the middle. Informed flow cuts profits at every width, and more than halves them at the tightest one, where every informed trade is most profitable for the informed trader.

Skew changes the risk more than the return. At a 3-cent half-spread with no informed flow, turning skew off raises average P&L slightly, from 28.66 to 30.18, but the variability of P&L across runs jumps from 2.35 to 18.66 and the largest position from about 8 units to 33. Nearly all the extra risk is price risk on inventory, for almost no extra return.

Key idea. A market maker's edge is the spread, not price direction. Skewing quotes to keep inventory near zero gives up a little expected profit to remove most of the risk.

Adverse selection in the real world

The informed trader here is a caricature, but the effect is real. A market maker never knows which counterparties know something: a large fund about to buy for days, a trader reacting faster to news, or a firm with a better short-term model. Fills that are immediately followed by a move against the market maker are the signature, and measuring them ("markouts": P&L a few seconds or minutes after each fill) is one of the main jobs of a market-making desk.

Market makers respond by widening spreads when risk rises (around news, in volatile markets), by quoting less size, and by learning which flow is informed. That is why spreads widen and depth thins exactly when markets are stressed, as the order book lesson noted.

In code

The whole model fits in one function:

python
import numpy as np

def simulate(half_spread, skew, informed_rate, steps=2000, seed=0,
             vol=0.02, jump=0.10, arrival=0.6, decay=30.0):
    rng = np.random.default_rng(seed)
    mid, inventory, cash, max_inventory = 100.0, 0, 0.0, 0
    for _ in range(steps):
        centre = mid - skew * inventory  # skew: quote lower when long, higher when short
        bid, ask = centre - half_spread, centre + half_spread
        if rng.random() < arrival * np.exp(-decay * max(0.0, ask - mid)):
            inventory, cash = inventory - 1, cash + ask  # someone buys from us at the ask
        if rng.random() < arrival * np.exp(-decay * max(0.0, mid - bid)):
            inventory, cash = inventory + 1, cash - bid  # someone sells to us at the bid
        move = vol * rng.standard_normal()
        if rng.random() < informed_rate:  # a trader who knows the next jump
            up = rng.random() < 0.5
            move += jump if up else -jump
            if up and ask < mid + jump:
                inventory, cash = inventory - 1, cash + ask
            if not up and bid > mid - jump:
                inventory, cash = inventory + 1, cash - bid
        mid += move
        max_inventory = max(max_inventory, abs(inventory))
    return cash + inventory * mid, max_inventory  # mark the position to the final mid

def average(**kwargs):
    runs = [simulate(seed=s, **kwargs) for s in range(200)]
    pnl = np.array([r[0] for r in runs])
    return pnl.mean(), pnl.std(), np.mean([r[1] for r in runs])

for informed in (0.0, 0.05):
    for half_spread in (0.01, 0.03, 0.06):
        mean, sd, inv = average(half_spread=half_spread, skew=0.002, informed_rate=informed)
        print(f"informed {informed:.0%}, half-spread {half_spread:.2f}: P&L {mean:6.2f} ± {sd:5.2f}, max inventory {inv:4.1f}")

for skew in (0.0, 0.002):
    mean, sd, inv = average(half_spread=0.03, skew=skew, informed_rate=0.0)
    print(f"skew {skew}: P&L {mean:6.2f}, P&L sd {sd:5.2f}, max inventory {inv:4.1f}")

Its output is the table above, plus the skew comparison. This model is deliberately simple. The standard academic version, by Avellaneda and Stoikov, derives the optimal quotes and skew from the market maker's risk aversion; it makes a good next project.

Where this shows up in quant work

  • Market-making firms run this trade-off across thousands of instruments, with quotes updated in microseconds. Quant roles there build the fair-value models, the skew logic and the markout analysis.
  • Execution. Anyone who trades pays the spread to someone. Understanding why it is that wide tells you when trading is cheap and when to wait.
  • A portfolio project. Extending this simulator with an inventory limit, a fair value from order-book imbalance, and markout statistics is a strong, self-contained project.

Exercises

  • Set the half-spread to 2 cents with informed traders on. Roughly what fraction of the spread earned is lost to inventory P&L?
  • In the Python code, add a hard inventory limit: stop quoting on one side when the position reaches ±10 units. How does it compare with skewing?
  • Compute markouts: for each fill, record the mid 10 steps later and the P&L of that fill against it. How do the average markouts differ between uninformed and informed fills?

Key takeaways

  • A market maker's P&L is spread earned plus inventory P&L; the first is reliable, the second is risk.
  • Tight quotes trade often for little; wide quotes trade rarely for more; the best is in between.
  • Informed traders pick off quotes before moves; tight spreads suffer most.
  • Skewing quotes against inventory keeps positions small and cuts risk sharply for little cost.