The Hidden Traps I Fell Into When Building My First Trading Bot
Quick context (why you're writing this)
I spent a weekend hacking together a simple market‑making bot for a crypto exchange. The code looked clean, the back‑test showed a shiny 2% daily return, and I was ready to push it live. The first live run? My account was down 0.8% after just a few minutes. I stared at the logs, convinced the exchange was cheating me. Turns out, the problem wasn’t the exchange—it was a handful of subtle assumptions I’d made in the code that looked harmless on paper but blew up in reality. If you’ve ever seen a strategy that works perfectly in simulation and then implodes in the market, you know the feeling. Let’s talk about the three mistakes that got me (and probably you) into trouble.
The Insight
What I learned is that trading systems live at the intersection of math, systems engineering, and human behavior. Tiny oversights—like using floating‑point numbers for money, treating market data as instantly consistent, or assuming an order is filled the moment you send it—can cascade into serious P&L leaks or even regulatory red flags. The fix isn’t about adding more layers of abstraction; it’s about tightening the contract between your strategy and the exchange. When you treat price as an integer number of “ticks” or “cents”, when you serialize order lifecycle events, and when you respect the real‑time nature of the book, you stop fighting phantom bugs and start focusing on edge‑case logic that actually matters.
How (with code)
Mistake #1 – Using float for price and size
It’s tempting to write price = 100.25 and let Python’s float handle the math. In a back‑test that works because you’re usually rounding for display. In live trading, the exchange expects prices in its native tick size (often an integer number of satoshis, cents, or basis points). A tiny binary representation error can send an order that’s rejected, or worse, accepted at a price you didn’t intend.
# ❌ What NOT to do
price = 100.25 # float
size = 0.0037 # float
order = {
"symbol": "BTC-USD",
"price": price,
"size": size,
"side": "buy"
}
# send order via REST/WebSocket
When the exchange parses this JSON, it may round price to 100.24999999999999 and reject it because it doesn’t fall on the allowed tick size (e.g., $0.01).
Fix: Work in the smallest discrete unit the exchange understands. For US equities that’s cents; for crypto it’s often the “base currency” unit (e.g., satoshi for BTC).
# ✅ Safer approach
TICK_SIZE = 0.01 # $0.01 for this exchange
price_ticks = int(round(100.25 / TICK_SIZE)) # 10025 ticks
size_base = int(0.0037 * 1_000_000) # convert to micro‑units if needed
order = {
"symbol": "BTC-USD",
"price": price_ticks * TICK_SIZE, # send as integer ticks or let lib handle conversion
"size": size_base,
"side": "buy"
}
Now the price is guaranteed to land on a valid tick, and you avoid silent rounding surprises.
Mistake #2 – Assuming instant order acknowledgment
A lot of example snippets show a fire‑and‑forget pattern: send an order, immediately read the response, and treat it as filled. In reality, the exchange may accept the order, then later reject it due to price‑time priority, self‑trade prevention, or a sudden market move. If you base your next decision on that premature “filled” flag, you can end up double‑sizing or sending contradictory orders.
# ❌ Risky pattern
resp = client.post_order(order) # HTTP POST, returns 200 OK instantly
if resp.status_code == 200:
position += order["size"] # <-- assumes fill!
The HTTP 200 only means the exchange received the request, not that it was executed.
Fix: Treat order lifecycle as a stream of events. Assign a client‑order ID, listen to the exchange’s order updates (via WebSocket or polling), and only update your position when you receive a filled or partially filled event.
# ✅ Event‑driven handling
client_order_id = str(uuid4())
order["client_order_id"] = client_order_id
# send order
client.post_order(order)
# later, in your message handler
def on_order_update(update):
if update["client_order_id"] != client_order_id:
return
if update["status"] == "filled":
position += update["filled_size"]
avg_price = update["avg_price"]
elif update["status"] == "rejected":
log.warning("Order rejected: %s", update.get("reason"))
Now your strategy reacts to the real state of the order, not to an optimistic guess.
Mistake #3 – Ignoring latency and timestamp ordering
When you pull market data via a REST endpoint, you might get a snapshot that’s already stale by the time you process it. If you then decide to send an order based on that snapshot, you could be acting on a price that’s already moved against you. Even WebSocket feeds can deliver messages out of order if you don’t sort by the exchange’s timestamp or sequence field.
# ❌ Naive snapshot usage
ticker = client.get_ticker("BTC-USD")
mid_price = (ticker["bid"] + ticker["ask"]) / 2
if mid_price < my_target:
client.post_order({"price": mid_price, "size": 0.001, "side": "buy"})
The tick you just read could be from 200 ms ago; in a fast market that’s enough to slip past your target.
Fix: Use the exchange’s monotonic sequence number (or UTC timestamp with millisecond precision) to buffer and sort incoming messages before you act on them. Only process a message when you know you’ve seen all prior updates.
# ✅ Simple sequence guard
last_seq = -1
def on_message(msg):
global last_seq
seq = msg["seq"]
if seq <= last_seq:
# duplicate or out‑of‑order; drop or buffer for reordering
return
# process in order
last_seq = seq
# your strategy logic here
If your transport doesn’t give you a sequence, fall back to the exchange’s timestamp and discard any message whose timestamp is older than the last processed one by more than a few milliseconds—this guards against clock skew and network jitter.
Why This Matters
These three slip‑ups aren’t just academic; they translate directly into lost money, increased risk, and sometimes compliance headaches. A strategy that looks profitable in a notebook can bleed cash when live because the market punishes any mismatch between what you think you’re doing and what the exchange actually sees. By grounding your code in discrete price units, treating orders as stateful events, and respecting the real‑time nature of market data, you turn a fragile prototype into something that can survive the noise of live trading.
The trade‑off? You’ll write a bit more boilerplate, and you’ll need to think harder about ordering and state management. But the payoff is confidence: when the market moves fast, you know your bot isn’t making decisions based on stale or malformed data.
Challenge
Take a look at your own trading or execution code right now. Find one place where you’re using a floating‑point number for price or size, or where you assume an order is filled the moment you send it. Replace it with the pattern we talked about—integer ticks and an event‑driven update loop—and see how it changes your behavior in a paper‑trade or testnet run.
What’s the first assumption you’re going to challenge? Let me know in the comments—I’m curious to hear what you uncover.
Top comments (0)