On 10 August, at 10:14:27, I switched one of my strategies from live to paper in the dashboard. At 10:15:00 it entered a real trade anyway. That trade lost ₹15,947.
Nothing crashed. No error was logged. The dashboard showed the strategy as paper the whole time. This post is about how that happened, and the second, worse bug I found while fixing it.
How the bot decides between paper and live
Every strategy has a mode — live or paper — that I can flip from the dashboard. On top of that sits a live gate: a set of written rules, per strategy, that describe which trades are allowed to go to the broker. For example, a rule might allow a strategy to trade live only on one side of the market, and send everything else to paper.
The routing code sits in one small wrapper around two engines, one live and one paper. For each entry, it asks the gate for a decision and picks an engine.
The intent was clear in my head: the gate is an allowlist that narrows a live strategy. It can take trades away from live. It should never add them.
What actually happened
When I flipped the strategy to paper, the code re-pointed its fallback engine to paper. But the fallback was only used when the gate returned "no rule applies". This strategy had a rule, and the next signal matched it. The gate returned "live, gated", the wrapper took the gated branch straight to the live engine, and my switch was never read.
In other words, the gate was behaving as a grant that could promote a paper strategy back to live. Thirty-three seconds after I papered it, the rule did exactly that.
The second bug: a paper exit on the real broker
While reviewing the fix I found something worse.
The wrapper remembers which engine it used for the last entry, and routes exits through that same engine. When the bot starts up, that memory is seeded with the live engine. So consider a restart while a strategy holds an open paper position: if the exit fires before any new entry, the wrapper closes it through the live engine. That is a real sell order for a position that only ever existed on paper — and on a real account, selling something you don't hold opens a short.
It had been harmless while the wrapper only wrapped live strategies. It became reachable the day I started wrapping paper-by-default strategies too. No trade was affected, but only because I found it in review.
A related near-miss in the same file: an earlier idea was to shrink trade size for strategies "on probation" by scaling the buy quantity inside the wrapper. But exits take their quantity from the strategy, which would still believe it held full size. The exit would have sold more than was bought. I moved that sizing into the strategy itself, so buy, sell and logging all agree by construction.
The fix
Three changes, each small:
- A manual switch to paper outranks everything. It is checked first, before the gate, before the allocator, before any rule. No rule, however specific, can route a manually-papered strategy to live.
- The paper choice is recorded, not just returned. When the switch forces paper, the wrapper also updates its memory of the active engine, so the matching exit closes on paper too.
- Exits fall back safely after a restart. Until the wrapper has made a routing decision in this session, exits use the strategy's configured default rather than whatever the engine was seeded with.
Each one is pinned by a regression test named after the date of the incident. The test sets the manual switch, feeds a trade that matches a live rule, and asserts it goes to paper.
The rules I follow now
- Routing code is where money is lost silently. Changes to the gate, the engines, or the kill switch are only made in a session where I read the diff myself — never by an automated assistant working unattended.
- Fail closed. If a rule references data the bot can't supply right now, the trade goes to paper.
- Operator intent wins. A human saying "stop" must be the strongest signal in the system. Anything that can override it is a bug.
- Exits follow entries. A position closes through the engine that opened it, full stop.
- Fixes aren't live until the process restarts. I say that out loud every time I touch the running bot, because a fix sitting on disk has saved nobody.
If you run a bot with both paper and live modes, try this today: switch a live strategy to paper, then force a signal that matches one of its live rules, and watch where the order goes. It takes five minutes, and it is cheaper than ₹15,947.