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.
| Term | Meaning |
|---|---|
| half-spread | The distance of each quote from the mid price |
| inventory | The position the market maker holds: positive when long, negative short |
| spread earned | The sum of the distances between each fill and the mid at that moment |
| inventory P&L | Gains and losses on the position as the price moves |
| adverse selection | Being traded against mostly by people who know more than you |
| skew | Shifting 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:
where is the inventory held while the mid moves by . 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.
- Spread earned
- 0.00
- 0 trades
- Inventory P&L
- 0.00
- Total P&L
- 0.00
- Largest position
- 0
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 flow | Half-spread | Average P&L | Spread of P&L across runs |
|---|---|---|---|
| none | 1¢ | 16.90 | 1.94 |
| none | 3¢ | 28.66 | 2.35 |
| none | 6¢ | 24.18 | 2.62 |
| 5% of steps | 1¢ | 7.65 | 3.17 |
| 5% of steps | 3¢ | 21.86 | 3.65 |
| 5% of steps | 6¢ | 19.85 | 4.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:
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.

