Reality Check: What Breaks Between the Plan and the Money

Lesson 11 of the arbitrage course. Latency, partial fills, a failed second leg and stale prices, with a small simulation that shows how a profitable plan turns into an average loss. Short Python code you can run and change.

Lesson 11 of the course "Build your own arbitrage bot". The bot now knows what to trade and how much. Today you test the assumption hiding under all of it: that the plan happens as written.

The head-on assumption

So far the bot computed a plan: buy 1 coin on A at 100.00, sell 1 coin on B at 100.30, profit 0.0997 after fees (lesson 10). The blunt way to run it is to assume both orders fill instantly at exactly those prices. This is how a first simulation is written, and it is the reason first bots look great on paper and lose money live. The rest of the lesson takes that assumption apart.

Five things that break the plan

  1. Delay. From the moment you read the prices to the moment the order arrives at the exchange, time passes: your program, the network, the exchange queue. Tens or hundreds of milliseconds. Prices move in that time.
  2. Two legs, two moments. You buy first and sell second. After the first fill, the second price can change. Until the second leg is done, you hold a coin and carry the price risk. This is called leg risk.
  3. Partial fills. You ask for 1 coin and get 0.6. The other leg must shrink to match, or you are left with an unhedged remainder.
  4. A failed leg. The second order can be rejected: no balance on that exchange, a price limit, the exchange is down, you hit a rate limit. Now you hold a coin you did not want, and you must sell it at whatever the market gives.
  5. Other bots. The gap you see is visible to everyone. Faster bots take the top level of the book before you. When your order arrives, the level you counted on is gone. You get filled mostly when the price is moving against you, which means the trades you win are worse than average.

A simulation you can run

We keep the plan from lesson 10 and add two effects. First, the bid on B moves randomly by the time the sell happens (a normal random number with a chosen spread, in price units). Second, with some chance the sell leg fails and you dump the coin on the market at 99.80.

import random
random.seed(11)
FEE = 0.001
ASK_A, BID_B = 100.00, 100.30        # plan: buy 1 coin on A, sell 1 coin on B

def one_trade(move_sigma, fail_chance, dump_price):
    paid = ASK_A * (1 + FEE)
    if random.random() < fail_chance:               # the sell leg fails: dump the coin
        return dump_price * (1 - FEE) - paid
    bid_now = BID_B + random.gauss(0, move_sigma)   # the bid moved while we were buying
    return bid_now * (1 - FEE) - paid

def run(move_sigma, fail_chance, n=10000):
    r = [one_trade(move_sigma, fail_chance, 99.80) for _ in range(n)]
    return sum(r) / n, sum(x < 0 for x in r) / n

print("plan profit", round(BID_B * (1 - FEE) - ASK_A * (1 + FEE), 4))
for sig, fail in [(0, 0), (0.05, 0), (0.2, 0), (0.2, 0.1), (0.2, 0.3)]:
    m, l = run(sig, fail)
    print(f"move {sig:<5} fail {fail:<4} avg profit {m:+.4f}  losing trades {l*100:.0f}%")

Output:

plan profit 0.0997
move 0     fail 0    avg profit +0.0997  losing trades 0%
move 0.05  fail 0    avg profit +0.0994  losing trades 2%
move 0.2   fail 0    avg profit +0.0994  losing trades 31%
move 0.2   fail 0.1  avg profit +0.0516  losing trades 38%
move 0.2   fail 0.3  avg profit -0.0512  losing trades 52%

Read it line by line.

  • No movement, no failure: the plan, +0.0997.
  • A random move of 0.05 (half the profit): the average barely changes, but 2 trades in 100 already lose money. Random noise with no bias averages out. That is the optimistic case.
  • A random move of 0.2: the average is the same, but 31 trades in 100 lose money. Your profit per trade is small, so even harmless-looking noise makes a third of trades losers.
  • A 10 percent chance that the second leg fails: the average halves, to +0.0516. One failed trade costs about 0.40 (you pay 100.10 and get 99.70), which takes back the profit of four good ones.
  • A 30 percent failure chance: the average is negative, −0.0512. The strategy loses money while every individual plan looks profitable.

Check the failure cost by hand: you paid 100.00 × 1.001 = 100.10. You dump at 99.80 × 0.999 = 99.7002. Loss: 0.3998. With a 30 percent chance of failure, the expected profit is 0.7 × 0.0997 − 0.3 × 0.3998 = −0.0501, which is close to the simulated −0.0512 (the difference is random noise from 10 000 trials).

The simulation is simple on purpose and has one important flaw in the optimistic direction: it moves the price with no bias. In a real market, the fills you get are not a fair sample. When the price moves in your favor, other bots take it before you. When it moves against you, you get filled. So the real result is worse than the line "move 0.2, fail 0".

What a bot does about each problem

  • Delay. Measure it. Log the time of each step. Keep the bot near the exchanges (a server in the same region), and do the cheapest checks first.
  • Leg risk. Prefer designs with a smaller exposed window: hold balance on both exchanges and sell and buy at the same time instead of moving coins between them. Send both orders together, not one after the other.
  • Partial fills. After the first leg, read how much really filled, and size the second leg to that amount.
  • A failed leg. Have a plan for the leftover coin: sell at market, with a price limit, and count the cost in your results. Add the failure rate to the profit estimate, as the simulation did.
  • Other bots. Take a safety margin: trade only when the expected profit is well above zero, not just above zero. How far above is a number you set from your own measurements.

Let an AI agent write and test it

A vague request:

Make the arbitrage simulation more realistic.

A request with a check built in:

Write reality.py from the lesson 11 code: one_trade(move_sigma, fail_chance, dump_price) and run(...). Use random.seed(11). The first line of output must be "plan profit 0.0997". The line for move 0.2, fail 0.3 must show a negative average profit. Add one parameter, min_edge: a trade is skipped when the planned profit is below min_edge. Print the average profit per planned trade for min_edge 0.05 and 0.15 and explain in two sentences why the numbers differ. Do not change the plan numbers.

Recompute the failure cost by hand (0.3998) and compare it with what the agent prints for a single failed trade.

Your exercise

  1. Run the code and check that your output matches the table.
  2. Find the failure chance at which the average profit is exactly zero. By hand: 0.0997 × (1 − f) = 0.3998 × f gives f = 0.0997 / 0.4995, about 0.20. Check it in code with fail 0.2.
  3. Change the dump price from 99.80 to 99.95. How does the break-even failure chance change? What does that say about the value of selling the leftover coin quickly and near the market?
  4. Make the move one-sided: subtract 0.05 from every bid_now (a fill bias). Run again and compare. This is closer to a real market.

What comes next

You now know the plan is only the start, and that failures decide the average result. Lesson 12 builds the safety around it: paper trading (the full bot on live prices with fake money), a config file for every number we used, and risk limits that stop the bot before one bad hour can hurt you.