Where prices are made
A price on a screen is the result of a queue. On an exchange, buyers and sellers post limit orders: "buy 300 at 99.50", "sell 200 at 100.00". Those that have not been executed rest in the limit order book: the bids (buy orders), best at the highest price, and the asks (sell orders), best at the lowest. A trade happens when an incoming order meets a resting one. Everything the earlier topics call the price, the spread, the slippage and the impact comes from this structure.
# A side of the book as a dict {price: total size}
bids = {99.5: 300, 99.4: 500, 99.0: 1200}
asks = {100.0: 200, 100.1: 400, 100.5: 800}
best_bid, best_ask = max(bids), min(asks)
print(best_bid, best_ask) # 99.5 100.0
print(round(best_ask - best_bid, 2), (best_bid + best_ask) / 2) # the spread 0.5 and the mid 99.75Spread, mid and depth
The best bid and best ask are the top of the book. Their difference is the spread, the price of trading immediately: a buyer who takes the ask and sells back at the bid loses the spread. Halfway between them is the mid price, the usual reference for a fair price. Depth is the size resting at each level and beyond it, and says how much can trade before the price moves. An empty side means no price on that side at all, so spread and mid are undefined, None and not zero.
def best_bid_ask(bids, asks):
return (max(bids) if bids else None, min(asks) if asks else None)
def spread_and_mid(bids, asks):
bid, ask = best_bid_ask(bids, asks)
if bid is None or ask is None:
return None, None
return ask - bid, (bid + ask) / 2
print(spread_and_mid({99.5: 300, 99.4: 500}, {100.0: 200, 100.1: 400})) # (0.5, 99.75)
print(spread_and_mid({}, {100.0: 200})) # (None, None): one side is emptyAdding orders without changing the past
A new limit order either joins an existing price level, adding to its total size, or opens a new level. Writing this as a function that returns a new dict instead of changing the old one has a practical advantage: you keep a snapshot of the book before the order, which helps testing and replaying. Copy with dict(side), change the copy, return it. A function that silently mutates its argument is a classic source of bugs when the same book is used in two places.
def add_limit_order(side, price, size):
new = dict(side) # a copy: the original book is not touched
new[price] = new.get(price, 0) + size
return new
bids = {99.5: 300, 99.4: 500}
after = add_limit_order(bids, 99.5, 100) # joins the existing level
after = add_limit_order(after, 99.3, 50) # opens a new level
print(after) # {99.5: 400, 99.4: 500, 99.3: 50}
print(bids) # {99.5: 300, 99.4: 500} unchangedMatching a market order
A market buy takes the asks from the lowest price up: all of the first level, then the second, until the order is filled or the book is empty. Loop over the sorted prices, take min(left, size_at_price) at each, record the fill, and either delete the level (fully taken) or reduce it (partly taken). The list of fills gives the average price you saw in the order-fills topic, and the book that remains is the market after your trade: thinner at the top, with a worse best ask.
def match_market_buy(asks, size):
remaining = dict(asks)
fills = []
left = size
for price in sorted(asks): # cheapest first
if left <= 0:
break
take = min(left, asks[price])
fills.append((price, take))
left -= take
if take == asks[price]:
del remaining[price] # the level is used up
else:
remaining[price] = asks[price] - take
return fills, remaining
asks = {100.0: 200, 100.1: 400, 100.5: 800}
fills, remaining = match_market_buy(asks, 500)
print(fills) # [(100.0, 200), (100.1, 300)]
print(remaining) # {100.1: 100, 100.5: 800}- Each side is a dict
{price: size}: the best bid ismax(bids), the best askmin(asks); spread isask - bid, mid is their average, and both areNoneif a side is empty. - Add an order by copying the dict, adding at the price, and returning the copy; the book passed in stays unchanged.
- A market buy walks the sorted asks, taking
min(left, size)per level, deleting used-up levels, reducing partly taken ones; real books also have a queue at each level.
Next up: order-flow imbalance, reading the pressure in the book to anticipate the next price move.
Lab · what's graded
- Find the top of the book with
best_bid_ask25 pts - Measure the gap with
spread_and_mid25 pts - Rest an order with
add_limit_order25 pts - Eat through the asks with
match_market_buy25 pts