Reading and Walking an Order Book

Lesson 9 of the arbitrage course. What an order book is, how to read bids and asks with sizes, how to walk it to find the real price of a trade, and a short Python function you can check by hand.

Lesson 9 of the course "Build your own arbitrage bot". World 3 starts here: stage 2 of the bot, the fill simulation. Until now we used one price per side. Today you learn why one price is not enough.

The market stall

Imagine a market stall that sells apples. The seller has a few crates, and each crate has its own price. The first crate holds 2 kg at 100 coins per kg. The second holds 3 kg at 100.20 per kg. If you want 2 kg, you pay 100 per kg. If you want 5 kg, the first 2 kg cost 100 and the next 3 kg cost 100.20. Your average price is higher than the first price.

An order book is this stall for a coin. It has two sides.

  • The ask side lists sellers. Each row says: "I sell this amount at this price". Rows are sorted from the lowest price (best ask) upward. When you buy, you eat these rows from the top.
  • The bid side lists buyers. Each row says: "I buy this amount at this price". Rows are sorted from the highest price (best bid) downward. When you sell, you eat these rows from the top.

A row is called a level. Real books have thousands of levels, but the top few matter most.

A tiny book:

Ask priceAsk size
100.203
100.002
Bid priceBid size
99.905
99.804

(The asks are written with the worst at the top and the best at the bottom, the way many exchanges print them, so the best ask and best bid sit next to each other in the middle.)

Best ask 100.00, best bid 99.90, spread 0.10.

The head-on way: one price times the amount

The blunt method multiplies the best price by the amount: 3 coins at 100.00 cost 300.00. This is what the scanner of lessons 1 to 4 implicitly assumed. The real cost below is 300.20, so the blunt method is wrong by 0.20 on a trade of 3 coins, and the error grows with size. For an arbitrage margin of 0.10 that is the difference between profit and loss. Use the blunt number only for a quick first look, never for a decision.

Walking the book

Walking the book means: to find the cost of a trade, start at the best level, take as much as that level has (or as much as you still need), then move to the next level, and keep going until you have the whole amount.

Example. You buy 3 coins on the book above (the ask side).

  • Level 1: 100.00, size 2. Take 2 coins. Cost 2 × 100.00 = 200.00. You still need 1.
  • Level 2: 100.20, size 3. Take 1 coin. Cost 1 × 100.20 = 100.20.
  • Total cost: 300.20. Average price 300.20 / 3 = 100.0667.

You wanted to trade at 100.00 and your average is 100.0667. The extra cost 0.0667 per coin is called slippage. It grows with the trade size.

Selling works the same way on the bid side. If you sell 3 coins into bids of 1 coin at 100.30 and 5 coins at 99.90: 1 × 100.30 + 2 × 99.90 = 300.10. Average 100.0333.

The arbitrage that vanished

In lesson 3 the screen saw ask 100.00 on exchange A and bid 100.30 on exchange B, a gap of 0.30. Walk both books for 3 coins: you pay 300.20 on A, you receive 300.10 on B. Net −0.10. The gap that stage 1 reported does not exist for a 3-coin trade. It exists for 1 coin, because the top level of B has size 1: buying 1 coin on A costs 100.00 and selling 1 coin on B gives 100.30, profit 0.30. So the gap was real for 1 coin and false for 3.

The question for stage 2 is therefore not "is there a gap?" but "for which size is the profit positive, and what is the best size?"

The code

def walk(levels, amount):
    """levels: [(price, size), ...], best price first.
    Returns (money, filled): money spent or received, and how much was actually filled."""
    money, left = 0.0, amount
    for price, size in levels:
        take = min(size, left)
        money += take * price
        left -= take
        if left <= 0:
            break
    return money, amount - left

asks_A = [(100.00, 2), (100.20, 3)]      # you buy here
bids_B = [(100.30, 1), (99.90, 5)]       # you sell here

paid, got_a = walk(asks_A, 3)
received, got_b = walk(bids_B, 3)
print("paid", paid, "for", got_a, "coins")
print("received", received, "for", got_b, "coins")
print("net", round(received - paid, 2))
print("average buy price", round(paid / got_a, 4), "average sell price", round(received / got_b, 4))
print(walk(asks_A, 10))

Output:

paid 300.2 for 3 coins
received 300.1 for 3 coins
net -0.1
average buy price 100.0667 average sell price 100.0333
(500.6, 5)

The last line shows what happens when the book is too thin: you asked for 10 coins, the book only has 5, so you get (500.6, 5): 5 coins for 500.60. Always check that filled equals the amount you wanted.

A real book

You can download a real order book. This reads the top five levels of Bitcoin on a public exchange. No account or key is needed:

import json, urllib.request
req = urllib.request.Request("https://api.binance.com/api/v3/depth?symbol=BTCUSDT&limit=5",
                             headers={"User-Agent": "course"})
d = json.load(urllib.request.urlopen(req, timeout=10))
asks = [(float(p), float(q)) for p, q in d["asks"]]
bids = [(float(p), float(q)) for p, q in d["bids"]]
print("asks", asks[:3])
print("bids", bids[:3])

A test run printed asks starting (75694.8, 4.63912) and bids starting (75694.79, 3.97826). Notice two facts. The spread is one cent, which is tiny. And the top level holds about 4 coins, while the next levels hold much less than a coin. A trade of 10 coins would reach levels with worse prices. Your numbers will differ.

Two honest warnings

  • The book is a snapshot. By the time your order arrives, other people may have taken the top levels. Lesson 11 covers this.
  • Exchanges send the book in different formats: prices as text, sizes in coins or in contracts. Check units again, as in lesson 4.

Let an AI agent write and test it

A vague request:

Add order book support.

A request with the check built in:

Write book.py with walk(levels, amount) that returns (money, filled) by walking levels best price first. Test 1: asks [(100.00, 2), (100.20, 3)], amount 3 must return (300.2, 3). Test 2: bids [(100.30, 1), (99.90, 5)], amount 3 must return (300.1, 3). Test 3: amount 10 on the asks must return (500.6, 5). Then add a function that downloads the top 20 levels from the public Binance depth endpoint and walks them for amounts 0.1, 1 and 10 BTC, printing the average price for each. Do not trade anything.

Check the three test numbers on a calculator. The agent's code is correct only if you recompute them.

Your exercise

  1. Run the code and make sure the first output is exactly the one above.
  2. By hand, build a book with three ask levels of your own choice. Compute the cost of buying a size that uses all three levels and the average price. Then check it with the function.
  3. Run the real book function for 0.1, 1 and 10 BTC. How much does the average price rise as the size grows?
  4. Save your code as a new version.

What comes next

You can now compute the true cost of a trade on one book. A cycle has several legs, each with its own book, and each leg has a fee. Lesson 10 combines them and finds the size where the profit is best.