Home › Guides › Trading bots & code
Common trading bot mistakes: 10 bugs that make a bot look better than it is
Most bot mistakes don't crash anything. The code runs, the backtest looks great, and the bug stays hidden until real orders go out. Here are 10 of the usual ones, and how to catch each.
Why bot bugs are sneaky
A typo in normal code usually breaks something. A typo in trading code often makes the results look better.
That's the trap. A bug that hurts results gets found. A bug that helps gets trusted.
So the best habit is suspicion. When a backtest looks great, hunt for the reason before you believe it. We'll show the two biggest bugs running in code, on made-up prices, so you can see them happen.
Example code for learning — test it on paper first; it is not a ready-made trading system.
Practice data for the demos
import numpy as np
import pandas as pd
# 4,000 fake candles from a random walk. Made-up data, not a real market.
rng = np.random.default_rng(seed=1)
close = 100 + rng.normal(0, 0.5, 4000).cumsum()
open_ = np.r_[close[0], close[:-1]]
wiggle = np.abs(rng.normal(0, 0.3, 4000))
df = pd.DataFrame({
"open": open_,
"high": np.maximum(open_, close) + wiggle,
"low": np.minimum(open_, close) - wiggle,
"close": close,
})
These prices are random. Nobody — no person, no bot — can have a real edge on them. So any rule that "wins" here is showing you a bug or luck. Keep that in mind: results on synthetic data mean nothing about real markets.
Mistake 1: Look-ahead (using the future)
The bot uses information it couldn't have had yet. The classic: deciding on a candle's close, but buying at that same candle's open.
Watch it happen. The rule: "if a candle closes green (up), buy at its open and sell at its close."
candle = (df["high"] - df["low"]).rolling(14).mean().shift(1) # average candle size
move = (df["close"] - df["open"]) / candle # each candle's move, in candle sizes
green = df["close"] > df["open"]
cheat = move[green].mean() # knows the candle is green before buying it
honest = move[green.shift(1, fill_value=False)].mean() # acts on the NEXT candle instead
print(f"With look-ahead: {cheat:+.2f} per trade")
print(f"Without: {honest:+.2f} per trade")
With look-ahead, every single trade "wins" — on random data. Shift the signal by one candle and the magic vanishes, to about zero. And that's before costs.
The fix: shift every signal forward one candle with shift(1), and fill at the next candle's open. Then run your bot on random data. If it wins there, look for a peek.
Mistake 2: Overfitting (tuning to noise)
Try enough settings and one will look great on the past — by luck. That's called overfitting: the rule fits the noise, not a real pattern.
Here we try 30 pairs of moving averages and keep the best. Results are in R, one unit of risk (the amount lost if the stop is hit), with fees included and signals shifted.
def total_r(df, fast, slow, rr=2.0, fee=0.05):
"""Crossover rule, stop at 2 candle sizes, target 2R. Returns total R."""
f, s = df["close"].rolling(fast).mean(), df["close"].rolling(slow).mean()
enter = ((f > s) & (f.shift(1) <= s.shift(1))).shift(1, fill_value=False).to_numpy()
risk_all = (2 * (df["high"] - df["low"]).rolling(14).mean()).shift(1).to_numpy()
o, h, l, c = (df[k].to_numpy() for k in ("open", "high", "low", "close"))
total, i, n = 0.0, 0, len(df)
while i < n:
if not enter[i] or not risk_all[i] > 0:
i += 1
continue
entry, risk, result = o[i], risk_all[i], None
for j in range(i, n):
if l[j] <= entry - risk: # stop first: the cautious choice
result = -1.0
break
if h[j] >= entry + rr * risk:
result = rr
break
if result is None:
j = n - 1
result = (c[j] - entry) / risk
total += result - fee / risk
i = j + 1
return total
tuning, untouched = df.iloc[:3000], df.iloc[3000:]
scores = {(f, s): total_r(tuning, f, s)
for f in (5, 10, 15, 20, 25) for s in (30, 40, 50, 60, 80, 100)}
best = max(scores, key=scores.get)
print("Best pair on the tuning part:", best, f"{scores[best]:+.1f}R")
print("Same pair on the untouched part:", f"{total_r(untouched, *best):+.1f}R")
On our run, the "best" pair came out well ahead on the tuning part. On the part it never saw, it lost. Nothing was ever there — we just picked the luckiest of 30 tries.
The fix:
- Keep a slice of data you never touch until the very end.
- Use few settings, chosen for a reason, not by search.
- Prefer rules that work "okay" across many settings over one that's great at a single setting.
- Then forward-test on paper. See paper trading a bot.
Mistake 3: No costs
Spread, commission and Slippage come off every trade. A rule that trades often can look fine before costs and poor after. Always take a cost off each trade in the backtest, in units of risk, as in how to backtest in Python.
Mistake 4: Trading the forming candle
The last row of live data is often a candle that hasn't closed yet. Its numbers keep changing. A signal on it can appear and then vanish.
live = pd.DataFrame({"close": [100.0, 100.4, 100.9, 100.2]}) # last row is still forming
finished = live.iloc[:-1] # drop it before deciding
print("Deciding on close:", finished["close"].iloc[-1]) # 100.9, a finished candle
The fix: decide only on finished candles. Check your data source's docs — some include the forming candle, some don't. Same idea as our body-close rule: only a closed candle counts.
Mistake 5: Rounding the size up
Brokers and exchanges only accept sizes in fixed steps (for example 0.001 of a coin, or whole shares). Round the wrong way and you risk more than planned — or the order gets rejected.
import math
def round_down(qty, step):
"""Round a size DOWN to the allowed step, so risk never goes above plan."""
return math.floor(qty / step) * step
print(round_down(0.123456, 0.001)) # 0.123
print(round_down(57.9, 1)) # 57 whole shares
Always round down. Libraries often have helpers for this — ccxt, for example, has amount_to_precision. Check the size math with the position size calculator.
Mistake 6: Retries that double the order
The order goes through, the reply gets lost, the bot "tries again" — and now holds twice the size. The fix: make your own order ID once and reuse it on every retry. The full code is in trading bot risk controls.
Mistake 7: The stop only lives in the bot
If the bot holds the stop in memory and crashes, there is no stop. The fix: send the Stop loss to the broker with the entry, as a Bracket order, so it survives a crash.
Mistake 8: Mixed-up time zones
The exchange uses one time zone, your computer another, the data a third. The "daily" loss limit resets at the wrong hour, or the bot trades into the open. The fix: store every time in UTC and convert only for display.
Mistake 9: No limits on a bad day
A bot keeps firing. A choppy day can mean signal after signal. The fix: a daily loss limit (we use −2R: two full losses and done), a cap on trades per day and a kill switch.
Mistake 10: Keys in the code, and trust in strangers
Keys pasted into scripts leak. Bots bought from strangers hide what they do. The fix: keys in environment variables, withdrawals off, and never hand a key to a "bot seller". See API key security for trading bots.
A quick pre-launch checklist
| Check | How |
|---|---|
| No look-ahead | Run on random data; it should not win on average |
| Not overfitted | Settings chosen on one slice, checked once on another |
| Costs in | Fee and slippage taken off every trade, in R |
| Closed candles only | Drop the forming candle before deciding |
| Size rounds down | Test with awkward numbers |
| Stops at the broker | Kill the bot mid-trade; is the stop still there? |
| Limits work | Trigger the daily limit and kill switch on purpose |
| Paper first | Weeks on paper, every order explained |
Treat a great backtest as a bug report. Before you celebrate, ask: what could make this look better than it is? Then check mistakes 1 to 3 first.
Questions
Why does my trading bot work in backtests but not live?
The usual causes are look-ahead in the backtest, settings tuned to past noise, missing costs, and live fills being worse than assumed. Check those first, then forward-test on paper.
What is overfitting in a trading bot?
It's when settings are tuned until they match past prices, including the random parts. The rule looks great on that data and falls apart on data it hasn't seen.
How do I know if my backtest has look-ahead bias?
Run it on random, made-up prices. No rule should win there on average. If yours does, it's almost certainly using information it couldn't have had yet.
Should I buy a trading bot instead of building one?
Be very careful. You can't see what a bought bot does, and "guaranteed returns" claims are a classic scam sign. Whatever you use, test it on paper and never give anyone withdrawal access.
Want the next step? We'll walk you through it.
Four free lessons by email, one every few days — each with a 10-minute exercise on a real chart.
Send Me the Free Lessons 4 short emails · No spam · Unsubscribe anytimeOr see the whole method, step by step:
See What’s Inside the Course Try everything free first. Decide when you're ready. · Open the free chart tool (works in the TradingView phone app)One chart idea a day, in plain English. Follow along: