Lesson 12 of the course "Build your own arbitrage bot". This closes World 3. Your bot has a scanner, a size calculator and a reality check. Today you make it safe to run.
The head-on way
The blunt way to start: put your keys in the script, hardcode every number (fee 0.001, size 1, and so on inside the functions), and run it with real money to "see what happens". It works exactly until the first surprise. Then three questions have no good answer. Which number did I change? How much did I lose? Why did it keep trading?
This lesson fixes those three, one tool each.
Tool 1: paper trading
Paper trading means the whole bot runs on live prices, takes decisions, and sends its orders to a fake account that only records what would have happened. No money moves. You learn how the bot behaves, and every bug costs nothing.
The trick that makes it useful: the fake account has the same method as the real exchange client, here called trade. The bot does not know which one it talks to. When you switch to the real one later, nothing else changes.
A paper account also lies a little, as lesson 11 showed: it fills at the book prices you passed in, with no delay and no other bots. Treat its profit as an upper bound. Real results will be lower.
Tool 2: a config file
Every number that you may want to change goes into one place, read at the start. In the code below it is JSON text inside the program so that you can run it as it is. In your bot it becomes a file config.json.
- You change a number without touching the logic.
- You can save a config next to each run and know later which settings produced which result.
- A mistake in a number is one line to find.
Keys and secrets never go into the config file in the repository. They go into environment variables.
Tool 3: risk limits
A risk limit is a rule that stops the bot when something looks wrong, whatever the bot believes. Four basic ones:
- Maximum size of one trade. A bug that asks for 1000 coins instead of 1 is stopped here.
- Minimum edge: do not trade when the planned profit is below a safety margin (lesson 11 explained why).
- Maximum trades in a period. A loop that goes wild will hit this.
- Maximum daily loss. When the loss passes the limit the bot stops, and only a human restarts it. This is the kill switch.
The code
import json
CONFIG = json.loads("""{
"fee": 0.001,
"min_edge": 0.05,
"max_size": 1.0,
"max_trades": 20,
"max_daily_loss": 0.50
}""")
def walk(levels, amount):
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
def plan_profit(asks, bids, q, fee):
"""Profit after fees for size q, or None if a book is too thin."""
paid, got_a = walk(asks, q)
received, got_b = walk(bids, q)
if got_a < q or got_b < q:
return None
return received * (1 - fee) - paid * (1 + fee)
class PaperAccount:
"""A fake account. A real exchange client would have the same trade() method."""
def __init__(self):
self.pnl, self.trades = 0.0, 0
def trade(self, asks, bids, q, fee):
profit = plan_profit(asks, bids, q, fee)
self.pnl += profit
self.trades += 1
return profit
class Guard:
def __init__(self, cfg):
self.cfg, self.stopped = cfg, False
def check(self, account, size, edge):
if self.stopped:
return "stopped"
if account.pnl <= -self.cfg["max_daily_loss"]:
self.stopped = True
return "daily loss limit"
if account.trades >= self.cfg["max_trades"]:
return "trade limit"
if size > self.cfg["max_size"]:
return "size above limit"
if edge is None or edge < self.cfg["min_edge"]:
return "edge below minimum"
return "ok"
asks = [(100.00, 2), (100.20, 3)]
bids = [(100.30, 1), (99.90, 5)]
account, guard = PaperAccount(), Guard(CONFIG)
for size in [1.0, 1.0, 1.5, 0.2]:
edge = plan_profit(asks, bids, size, CONFIG["fee"])
verdict = guard.check(account, size, edge)
profit = account.trade(asks, bids, size, CONFIG["fee"]) if verdict == "ok" else None
print(f"size {size:<4} edge {edge:+.4f} -> {verdict:20} profit {profit if profit is None else round(profit, 4)}")
print("pnl", round(account.pnl, 4), "trades", account.trades)
print("loss streak:")
account, guard = PaperAccount(), Guard(CONFIG)
for i in range(6):
account.pnl -= 0.20
print(i, round(account.pnl, 2), guard.check(account, 1.0, 0.1))
Output:
size 1.0 edge +0.0997 -> ok profit 0.0997
size 1.0 edge +0.0997 -> ok profit 0.0997
size 1.5 edge -0.0502 -> size above limit profit None
size 0.2 edge +0.0199 -> edge below minimum profit None
pnl 0.1994 trades 2
loss streak:
0 -0.2 ok
1 -0.4 ok
2 -0.6 daily loss limit
3 -0.8 stopped
4 -1.0 stopped
5 -1.2 stopped
Read the output.
- The first two trades pass every check and are recorded: 0.0997 each, total 0.1994. You can check the 0.0997 by hand from lesson 10.
- Size 1.5 is stopped by the size limit (it would also lose money: -0.0502).
- Size 0.2 has a positive edge of 0.0199, but below the minimum edge 0.05, so the bot skips it. A tiny profit is not worth the risk from lesson 11.
- In the loss streak the bot loses 0.20 per step. The guard fires at the third step, when the total is -0.6. The limit is 0.50, and the loss overshot it by 0.10, because the check runs before each trade and sees only the losses already made. A limit stops the bleeding, but it cannot stop the last trade. Set it lower than the loss you can really afford.
- After "daily loss limit" the guard answers "stopped" forever. Only a restart by a person clears it. That is on purpose.
Before you go live
A sensible order, with a rule that you move to the next step only when the previous one is boring:
- Paper trading for at least a week, with logs. Compare the profit it claims with lesson 11's estimate.
- Real exchange, tiny size (the exchange minimum), a loss limit of a few dollars. The goal is to test the connection, the orders and the fees, not to earn.
- Slowly raise size only while the real results match the paper results.
If real results are much worse than paper results, the difference is your real costs: delay, failed legs, other bots. Find which one before you raise the size.
Let an AI agent write and test it
A vague request:
Add safety to my bot.
A request with checks:
Write safe.py with plan_profit, PaperAccount and Guard from lesson 12, reading the settings from config.json (fee 0.001, min_edge 0.05, max_size 1.0, max_trades 20, max_daily_loss 0.50). Test with asks [(100.00, 2), (100.20, 3)] and bids [(100.30, 1), (99.90, 5)]: sizes 1.0, 1.0, 1.5, 0.2 must give ok, ok, size above limit, edge below minimum, and a final pnl of 0.1994. Test 2: a loss of 0.20 per step must print "daily loss limit" at step 2 and "stopped" after. Read API keys only from environment variables and never print them. Do not connect to any exchange.
Check the pnl 0.1994 on a calculator before you accept the agent's work.
Your exercise
- Run the code and compare with the output above.
- Move the config into a real
config.jsonfile and read it withjson.load. Changemin_edgeto 0.10 and see which trades are skipped. - Make the guard check the loss after each trade, so that it stops at the first trade that crosses the limit. Does the overshoot disappear? (Not completely: the last trade still happens.)
- Add a limit "maximum open position": if the bot holds more than 2 coins at once after a failed leg, it stops.
What comes next
World 3 is done: you have a whole bot that finds, sizes, simulates, and protects. World 4 makes it fast and wider: lesson 13 starts with speed, why Python becomes slow and what to do about it, and how Rust enters the picture.