Two more bugs from live trading bot audits — where the protection looked real everywhere except in the one place that mattered.
A stop-loss that fires and a stop-loss that logs "fired" are not the same thing. Neither are a circuit breaker that trips and a circuit breaker that mathematically can't move. Both bugs below share a shape I keep running into: the protective mechanism runs, updates its own bookkeeping, tells the operator it did its job — and never actually reaches the part of the system that would have made it real.
The pattern
Protective code paths — stop-losses, kill switches, daily loss limits, emergency exits — get exercised far less often than the code that opens positions. An entry function runs on every single trade; a kill switch might fire once in weeks of live running, if ever. That asymmetry means bugs in the protective path survive far longer before anyone notices, because nothing forces them to prove themselves the way constant use forces bugs in the hot path to surface.
Two live audits from the last few weeks turned up the same underlying failure, in two different disguises.
Case 1: the exit that logs "closed" and stops there
In one bot I reviewed — real capital, real markets — the entry path was solid: build an order, route it through the exchange's real order-submission client, wait for confirmation, record the actual fill. The automatic exit logic (stop-loss, take-profit, and a maximum-holding-time rule) looked, at a glance, like it followed the same pattern. It didn't.
When an exit condition triggered, the code wrote a trade record tagged SIMULATED, with a dry_run flag hardcoded True regardless of whether the bot was actually running live — then deleted the position from its own tracking table. Nothing in that path built an order. Nothing called the exchange. The function that actually submits and confirms orders — the same one the entry logic used — was never referenced anywhere in the exit code.
The practical effect: the position that was supposed to be closed is still open, on the real exchange, with real exposure — and the bot no longer knows it exists, because it just deleted its own only record of it. Logs, dashboard, and any alerting all say "closed." The one system that would show otherwise — the exchange itself — was never asked.
Worth noting: this bot's documentation advertised a fully-built exit system with six named strategies, including an explicit kill-switch meant to "force exit all positions." That code existed, tested, in the repository — just never imported anywhere outside its own test file. A correct, well-tested kill switch that nothing in the live code path ever calls provides exactly as much protection as no kill switch at all.
Case 2: the risk limit that's mathematically incapable of triggering
A different bot, a different failure mode, same root shape. This one ran a cross-exchange hedge strategy — open a position on one exchange, immediately hedge it on another — with a daily loss limit meant to halt trading if losses crossed a threshold.
The loss-limit check itself was fine: it compared an accumulated running total against a configured maximum and blocked new trades if breached. The problem was one level up. The only place in the entire codebase that updated that running total was a single function call after every successful fill — and that call never passed a PnL argument. The function's own signature gave it a default value of zero. Every single trade, win or loss, added exactly 0 to the total.
The running total was therefore mathematically incapable of ever becoming negative enough to trip the limit — not "unlikely to," but structurally unable to, by construction. The same permanently-zero number also fed the bot's own status display, so the operator-facing dashboard read "today's P&L: 0.00" continuously, whether the bot was flat or bleeding — indistinguishable from the outside. This particular strategy carries a specific extra risk: if one leg of the hedge fails to fill while the other goes through, the position is briefly — or not so briefly — directional and unhedged. That's exactly the scenario the loss limit exists to catch, and exactly the scenario it was structurally blind to.
Why this survives so long
Neither of these bugs is subtle once you're looking at the right ten lines. Both survived because nobody was forced to look at those ten lines under real conditions. A stale-price bug fires on every trade — it's loud, eventually, just by volume. A dead kill-switch or a zeroed-out loss counter is quiet by nature: it only needed to work once, in the one moment things went wrong, and by then it's too late to notice the check that should have stopped it never could have.
Standard unit tests don't reliably catch this either. A unit test that calls the risk function directly with a real loss value would pass fine — the bug isn't in what the function does with a real argument, it's that the one real call site in production never supplies one. That's a wiring problem, not a logic problem, and wiring problems only show up when you trace a value from where it originates to where it's actually used, not when you test each function in isolation.
What to check in your own bot
- For every stop-loss, kill-switch, or emergency-exit path, trace it to an actual order-submission call — the same one your entry logic uses. If the exit path reaches a different, thinner function (or none), that's worth a hard look.
- For every risk counter that's supposed to accumulate over time (daily P&L, loss streak, drawdown), find every call site that updates it and confirm each one actually passes a real, non-default value — not just that the accumulator logic itself is correct in isolation.
- Don't trust "well-tested" as a proxy for "connected." A correctly implemented safety class that's never imported into the live path is just documentation.
- If you can, deliberately trigger your emergency path once in a controlled setting and confirm — from the exchange's own records, not your bot's logs — that an order actually happened. Your bot's own logs aren't an independent witness; if the bug is that they lie, they'll lie convincingly.
Both of these were live when I found them, not abandoned side projects or paper-trading demos. That's usually the case: the protective code doesn't get skipped, it gets written, tested in isolation, wired in mostly right, and then quietly fails to do the one thing it exists for. I wrote about a related pattern — stale prices undermining stop-losses — in an earlier post; this is the same lesson from a different angle. What a bot logs about its own safety and what its safety actually does are two separate claims, and only one of them is checkable from the outside.
Originally published on the Honest Backtest blog.
Top comments (0)