A bot's internal variable and the real state of the world are two different things, connected only by whatever code keeps them in sync. Most of the time nobody notices the gap, because most of the time nothing forces the two to diverge. The two bugs below both come from the same root cause: a number that lives only in a running process's memory, with nothing writing it anywhere durable and nothing pushing it back out to the exchange once it changes.
Case 1: the daily loss limit a routine restart wipes out
A live bot I reviewed — real orders hitting a real exchange, not a backtest — ran under a process supervisor configured to restart it automatically on failure, with an explicit crash-loop guard in the unit file. That's a reasonable, even mature, piece of operational hygiene: unattended bots should recover from crashes rather than sit dead until someone notices.
The daily loss limit told a different story. The running total that a loss-limit check compared against its threshold was set once, in the initialization code, and never persisted anywhere — no database row, no file, nothing that survives the process exiting. A separate function existed to recompute the day's real losses from exchange history on a fresh start, correctly handling the exchange's own midnight rollover. It was never called. Every restart — crash, deploy, or the supervisor's own routine recovery — silently reset the day's tracked losses to zero, regardless of how close to the limit the bot actually was five seconds before the restart.
The bot's own logs never show this as a bug, because from the process's point of view nothing went wrong: it started up clean, initialized its state per its own code, and carried on. The gap only exists between what the exchange's account history says happened that day and what the bot currently believes — and nothing in the bot ever compares the two.
Case 2: the stop-loss that only moves in a Python variable
A different bot, trading options on one exchange, ran a trailing stop meant to lock in profit as a position moved favorably. The logic that decided when and how far to trail the stop was correct — it recalculated a new stop level on every price update and stored it on the in-memory trade object.
What it never did was tell the exchange. The execution module that this bot used to open and
close positions had no order-amend or order-edit call anywhere in it — only submit-new and
cancel. The trailing logic updated trade.stop_loss, which fed the bot's own dashboard
and logs, and stopped there. The actual protective order resting on the exchange's books stayed at
the price it was first submitted at, for the lifetime of the position. A trader watching the
dashboard would see the stop "trailing up" in real time while the order that would actually close
the position on adverse movement never moved at all.
This is one failure shape out of several I check for. The full checklist — entry-price path and exit/risk-limit path together — is on one page, or I can walk through it against your bot directly.
Get the safety-net checklist →Why "it worked in testing" doesn't catch this
Both bugs pass a normal test suite without any trouble, because a unit test constructs its own world and never restarts the process or checks the exchange's own order book — it just calls the function and inspects the in-memory result, which is exactly where each of these bugs' state lives. The daily-loss-limit function computes the right answer given a correct running total; it's simply never fed one after the first restart. The trailing-stop function computes the right new stop price; it just never reaches an API call that would move the real order. Neither bug is a logic error inside the function you'd naturally go read — it's a missing wire between a correct calculation and the one place that calculation needed to land.
What to check in your own bot
- For any risk counter that's supposed to persist across a day (daily P&L, loss streak, drawdown) — kill the process and restart it mid-session. If the counter comes back at zero, or the recovery function that should rebuild it from exchange history is defined but unused, you've found this bug.
- For any stop-loss or take-profit that's meant to trail or adjust, confirm the update path ends in an actual order-amend/cancel-and-replace call to the exchange — not just an assignment to an in-memory field that happens to also drive your dashboard.
- Treat "my process supervisor auto-restarts on crash" as a reason to specifically re-check every piece of state that's supposed to survive a restart, not just a reliability win. The two goals pull in opposite directions unless someone deliberately reconciled them.
- When in doubt, trust the exchange's own account/order history over your bot's internal variables — the bugs above are exactly the case where the two quietly stop agreeing and nothing in the bot itself would ever tell you.
Both bots were live, with real capital, when I found these. Neither is exotic — a process supervisor and a trailing stop are both perfectly ordinary things to build. The failure isn't in wanting either feature; it's in the seam between two pieces of code that each work correctly on their own but were never actually connected. I've written about a related shape before — a stop-loss that logs "closed" without telling the exchange — this is the same lesson again: what a bot's internal state says and what's actually true in the market are two separate claims, and only one of them is checkable from the outside.