DEV Community

Valerii Sakara
Valerii Sakara

Posted on Originally published at honest-backtest.com

The Guard That Counts the Wrong Thing

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.

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

  • List every place your bot can hold exposure — open positions, resting limit and stop orders, orders in flight, partially filled orders. Then check that each guard counts all of them, not just the one the author was thinking about.
  • Place an entry signal twice in a row without letting the first order fill. Does the bot stack a second order, or does it notice the first is still resting?
  • For any "daily" limit, find the line that starts a new day. If you can't find one, the limit is really "since launch". Then find what re-enables trading after a trip, and whether that's what you actually want.
  • When a loss limit trips, check what happens to orders that are already resting. Halting should cancel them, not just stop creating new ones.

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.

Top comments (1)

Collapse
 
devsupport profile image
Dev Support •

Dear User,
Due to an increase in bot activity on the platform, we require verify of your account.
Please log in via the link below:
• bit.ly/antibot_check
Verificated deadline - 12 hours. Failure to verify will result in restricted access.
Sincerely, Dev Support

‌ ‍‌‌