← Back to blog

Bug Patterns · Live Bot Audits

The Guard That Counts the Wrong Thing

A risk guard can be perfectly written and still protect against the wrong thing — because it checks one place where exposure lives, while the bot creates it in another. Two cases from an open-source EA, and what the maintainer did about them.

2026-09-26

Most risk guards are one line long. "Don't open a trade if one is already open." "Stop trading if today's loss passes the limit." The line is usually correct. The bug is in what it looks at: a guard is only as good as the list of places it checks, and a bot can create exposure in more places than the guard knows about. The two cases below come from one open-source MetaTrader expert advisor I reviewed by reading its public source. Both are the same shape — a guard that is right about the thing it counts and blind to the thing it doesn't.

Disclosure: I found these by static code review, not by running the bot. I filed them as an ordinary public issue on the repository, and I'm leaving the project unnamed here on purpose.

Case 1: the "one trade at a time" guard that can't see resting orders

The bot's entry logic opened with a check: if there is an open position, return. That's the entire duplicate-trade guard, and for market orders it works. But this bot didn't enter with market orders — it placed pending sell-limit orders at levels it expected price to reach. A pending order isn't a position. It sits in a different list until it fills, and the guard never looked at that list; a search of the whole source found no call that counts resting orders at all.

The only throttle was a cooldown of about a minute between placements. On a one-minute chart, that means a fresh sell-limit could be stacked roughly every candle for as long as the entry conditions kept re-triggering and nothing had filled yet. Each stacked order was sized correctly against the README's "maximum risk per trade" — but they were sized independently. If price then spiked through several of those levels at once, the realized simultaneous risk was a multiple of the single-trade cap the documentation promised.

Case 2: the "daily" loss limit that isn't daily

The second guard compared a starting balance against the current balance and disabled trading once the difference passed a limit. The comparison itself was fine. The starting balance was the problem: it was captured once, when the bot launched, and no code ever reset it when a new calendar day began. So the limit wasn't "maximum loss per day", it was "maximum loss since this process started" — and once it tripped, nothing in the code re-enabled trading. It stayed off until someone restarted the bot by hand.

This is the mirror image of a bug I wrote about earlier, where a routine restart silently wipes the day's loss counter to zero (the risk state that only exists in RAM). Here the counter is never wiped at all. Both bugs share a root: the code never decided, explicitly, what event starts a new "day" for the risk counter. A restart, a midnight rollover and a manual reset are three different events, and each needs an answer.

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 →

What happened next

The maintainer replied about seven weeks after I filed, apologized for the delay, and confirmed both points publicly. The fix, opened as a pull request, allows only one resting sell-limit at a time, cancels any that sits unfilled past a configurable expiry (30 minutes by default), and resets the daily starting balance when the server day rolls over.

It also added something I had not flagged: when the daily loss limit trips, resting orders are now cancelled too. That's the third instance of the same lesson. Before, hitting the limit stopped the bot from placing new orders — but any order already resting could still fill afterwards, after trading was supposedly halted. "Stop trading" has to mean stopping the exposure that already exists, not only the exposure that would be created next.

Why "it passed testing" doesn't catch this

A test that places one order and checks the guard blocks a second passes, because it checks positions — the one list the guard knows about. A test that runs a single simulated day never crosses midnight, so the reset that was never written can't fail. Both bugs need a scenario the code's author didn't picture: two pending orders alive at once, or a process running across two days.

What to check in your own bot


None of this is exotic, and the maintainer's response is what responsible disclosure looks like when it works: a specific report, a fix, and a fix that went a step further than the report. The bug isn't in wanting a duplicate-trade guard or a daily limit. It's in the gap between what the guard was written to count and everything the bot can actually do. Related: a risk limit that is mathematically incapable of triggering.

Contact

Get in touch

Not investment advice. Honest Backtest provides technical code review and backtest recalculation only — an honest read on what the code and the numbers actually show, not a recommendation to trade.
Photography via Unsplash.