Lesson 4 of the course "Build your own arbitrage bot". Today stage 1 of your bot starts to work on real data.
What we build
Lesson 1 compared two exchanges for one coin. Stage 1 of a real bot compares every pair of exchanges and many coins, and it reports only gaps that remain positive after fees. Today it covers five exchanges and one coin, and you will see how few gaps survive.
Three traps that make a gap fake
Before the code, three things that produce gaps that do not exist.
Trap 1: fees. Every exchange takes a percentage of each trade, called the taker fee when you take an existing offer. Suppose the buy-side fee is 0.1 percent and the sell-side fee is 0.26 percent. Then the gap must be above 0.36 percent just to break even.
Trap 2: quote currency. A price is written in some currency. BTC/USDT is priced in the dollar-pegged coin USDT. BTC/USD is priced in real dollars. These two are not the same price. USDT trades a little above or below one dollar, and sometimes much more. If you compare BTC/USDT on one exchange with BTC/USD on another without converting, the "gap" may only be the difference between USDT and the dollar. Rule: compare prices in the same quote currency.
Trap 3: units. One exchange may quote the price of one coin, another the price of a thousand, and a few quote the price of a contract with its own size. Always check what one unit means.
The formula
For buying on exchange X and selling on exchange Y:
gap in percent = (bid on Y − ask on X) / ask on X × 100
net gap = gap − fee on X − fee on Y
If the net gap is above zero, the pair survives stage 1.
Worked example. Ask on X is 100.00 and fee on X is 0.10 percent. Bid on Y is 100.50 and fee on Y is 0.10 percent. Gap = (100.50 − 100.00) / 100.00 × 100 = 0.50 percent. Net gap = 0.50 − 0.10 − 0.10 = 0.30 percent. This pair survives.
Same prices with fee on Y of 0.40 percent: net gap = 0.50 − 0.10 − 0.40 = 0.00. It does not survive.
The program
The program asks five exchanges for the best bid and ask of BTC/USDT, tries every ordered pair, and prints the five best.
This is the blunt, head-on way: two loops, everyone against everyone, one web request per exchange each time you run it. We write it this way on purpose. It is the shortest program that shows the problem, and you will feel its limits in the next section. Do not run this design in production.
import json, urllib.request
def get(url):
req = urllib.request.Request(url, headers={"User-Agent": "course-scanner"})
with urllib.request.urlopen(req, timeout=10) as r:
return json.load(r)
def binance(s): d = get(f"https://api.binance.com/api/v3/ticker/bookTicker?symbol={s}USDT"); return float(d["bidPrice"]), float(d["askPrice"])
def kraken(s): d = next(iter(get(f"https://api.kraken.com/0/public/Ticker?pair={s}USDT")["result"].values())); return float(d["b"][0]), float(d["a"][0])
def okx(s): d = get(f"https://www.okx.com/api/v5/market/ticker?instId={s}-USDT")["data"][0]; return float(d["bidPx"]), float(d["askPx"])
def bitstamp(s): d = get(f"https://www.bitstamp.net/api/v2/ticker/{s.lower()}usdt/"); return float(d["bid"]), float(d["ask"])
def coinbase(s): d = get(f"https://api.exchange.coinbase.com/products/{s}-USDT/ticker"); return float(d["bid"]), float(d["ask"])
# name: (function, taker fee in percent). The fees are examples: check each exchange's fee page.
VENUES = {"binance": (binance, 0.10), "kraken": (kraken, 0.26), "okx": (okx, 0.10),
"bitstamp": (bitstamp, 0.30), "coinbase": (coinbase, 0.40)}
def scan(symbol):
quotes = {}
for name, (fn, fee) in VENUES.items():
try:
quotes[name] = fn(symbol)
except Exception as e:
print("skip", name, "-", type(e).__name__)
rows = []
for buy, (_, ask) in quotes.items():
for sell, (bid, _) in quotes.items():
if buy == sell:
continue
gap = (bid - ask) / ask * 100
fees = VENUES[buy][1] + VENUES[sell][1]
rows.append((gap - fees, gap, buy, sell))
rows.sort(reverse=True)
for net, gap, buy, sell in rows[:5]:
print(f"buy {buy:9} sell {sell:9} gap {gap:+.3f}% after fees {net:+.3f}%")
scan("BTC")
Save it as scan5.py and run python3 scan5.py.
What result you should see
Five lines. A test run printed this:
buy binance sell okx gap +0.004% after fees -0.196%
buy okx sell binance gap -0.004% after fees -0.204%
buy binance sell kraken gap +0.003% after fees -0.357%
Your numbers will differ, but the picture will be the same: the gaps are tiny, a few thousandths of a percent, and fees are two orders of magnitude larger, so every net gap is negative. This is the honest first result of stage 1 on a big coin at big exchanges. If you see a net gap above zero, do not celebrate yet. Check three things: the quote currency, the unit, and whether the quotes are fresh (a venue that has not updated for a minute shows an old price).
If an exchange is skipped, it probably blocks your country. Remove it from VENUES or replace it.
Why you must not do it this way in production
The head-on program works, and for five exchanges and one coin it is fine. Now count what it costs when the bot grows.
- Pairs to compare: with N exchanges there are N × (N − 1) ordered pairs. For 5 exchanges that is 20. For 30 exchanges it is 870. For each coin.
- Requests: the program downloads fresh quotes on every run, one web request per exchange per coin. 30 exchanges and 200 coins make 6000 requests per scan. Exchanges limit requests and will block you, and 6000 requests take minutes, while a gap can live for seconds.
- Stale prices: by the time the last request returns, the first quote is old, so the comparison mixes different moments.
What a production bot does instead, in short:
- It keeps a live connection to each exchange (a websocket) and receives every price change as it happens, so it never polls.
- It stores the best bid and best ask per exchange per coin in memory.
- For one coin it needs only the highest bid among all exchanges and the lowest ask: one pass over N quotes, not N × (N − 1) comparisons. If the best bid minus the best ask does not beat the fees even for the cheapest pair, nothing else needs checking.
You will not build all of this today. The point is to know what the simple program hides, so that when your bot is slow or blocked you know which part to replace. Lessons 9 to 12 return to this when the bot becomes a whole.
Where gaps really appear
Gaps are bigger for small, rarely traded coins, on small exchanges, and during fast price moves. They are also bigger when the fees are lower. Many exchanges give lower fees to people who trade a lot or hold the exchange's own token. A professional bot works at fee levels you cannot reach on day one. This is one reason the competition on obvious pairs is so hard.
Let an AI agent write and extend it
A vague request:
Compare prices on many exchanges.
A request with the details that matter:
Extend scan5.py. Add a function that skips any quote older than 5 seconds if the exchange gives a timestamp. Put the fees and the coin list in config.py. Scan BTC, ETH and SOL. Print only pairs with net gap above 0, and if there are none, print the best three pairs with the word NEAR. Do not add any trading code. Show me the output.
Then check by hand: take one printed row, compute (bid − ask) / ask × 100 with a calculator from the two prices the program shows, and compare it with the gap column. It must match to three decimals.
Your exercise
- Run
scan5.pyfive times, a minute apart. How many times was any net gap above zero? - Change the fees in
VENUESto 0.02 percent on both sides (the level of a very large trader). Does any pair survive now? What does this tell you about who can profit from this kind of gap? - Add one more coin, such as ETH. Is its best gap larger or smaller than BTC's?
- Save a copy of your file as
scan5_v1.py. This is your saved version for the lesson.
What comes next
Lesson 4 compared exchanges. In lesson 5 we change the question: what if the gap is not between two exchanges but around a circle of currencies on one exchange? For that, the prices need to be drawn as a graph.