DEV Community

Cameron Meese
Cameron Meese

Posted on

My bot rejected every trade for being 'too wide' — and the gate was measuring the wrong thing

This is the third post in an accidental series about my paper-trading bot finding new and creative ways to do nothing. First it ran for 48 hours and rejected every trade. Then it logged hundreds of trades it never made. This time it had a number to hit — 100 completed trades before I'd even consider real money — and it had crawled to 87 and stalled. Days passed. The counter didn't move.

So, again: I dumped the rejection log and bucketed by reason.

$ awk '/rejected/' state/decisions.jsonl | jq -r .reason | sort | uniq -c
    246 wide_spread
Enter fullscreen mode Exit fullscreen mode

Not "mostly." Everything. Every single rejected entry, one reason: the spread was too wide. The bot has a rule — if a market's bid-ask spread is wider than a threshold, don't trade it, because crossing a wide spread is expensive. Reasonable rule. It was now vetoing 100% of opportunities.

My first assumption was the obvious one: the markets I was watching had simply gotten illiquid. Wide spreads are real. Maybe the bot was right to refuse.

It was not right. And the reason why is the most useful thing I've learned about market microstructure all month.

Touch-spread is not fill cost

Here's the thing I had quietly conflated. The "spread" my gate measured was the touch — the gap between the best bid and the best ask:

spread_bps = (best_ask - best_bid) / mid * 10_000
Enter fullscreen mode Exit fullscreen mode

That math is correct. I checked it three times. And the values were real — these markets genuinely showed a wide touch. So where was the bug?

The bug was in believing that a wide touch means an expensive fill. It doesn't. What you actually pay is the slippage from the price you cross at as you walk down the book — not the distance between the two best quotes. A market can have a wide touch and still fill you cheaply, if there's real size sitting right at the best ask.

And then there was the kicker: most of the symbols tripping this gate were cheap tokens — sub-dollar, some sub-penny. When a token trades at $0.03 and the exchange's minimum price increment (the tick) is $0.0001, a one-tick spread is already ~33 basis points. Two ticks, 66. Not because the book is thin — because the price is small and bps is a ratio. My gate was set at a level that, for a three-cent coin, flagged a perfectly healthy book as "too wide" on tick granularity alone.

I was, in effect, refusing to trade any cheap asset on the grounds that cheap assets have coarse ticks. That's not a liquidity filter. That's a unit-of-measure bug wearing a liquidity filter's clothes.

The gate that already knew the answer

Here's the part that stung. The bot already had a real liquidity check — a good one. When it builds its watchlist, it does a depth probe: it walks the actual order book and confirms it can fill a target size within a tight slippage budget. Every symbol on the list had passed that probe. The bot had walked the book, proven these markets fill cheaply, put them on the watchlist... and then refused to trade them at entry because the touch looked wide.

Two checks, measuring two different things, disagreeing — and I'd let the cruder one (the touch) overrule the one that actually walks the book.

Worse, the wide-spread gate was firing before the bot's real economic check — the one that weighs the genuine round-trip cost against the expected edge and rejects a trade only if it can't pay for itself. By bailing out early on a bps threshold, the crude gate never let the smart, money-aware check have an opinion.

The fix: demote the proxy, trust the real guards

I didn't delete the gate — a genuinely broken, one-sided, gapped book is a real thing worth refusing on sight. I demoted it. I moved the threshold out to a level that only catches pathological books, and let the checks that measure cost correctly make the marginal calls:

  1. The economic check prices the real round-trip cost against the expected edge.
  2. The fill-time slippage budget walks the actual book and rejects any fill whose real slippage blows the budget — the true protection, at the moment it matters.
  3. The build-time depth probe keeps structurally thin markets off the list entirely.

The validation was immediate. I restarted and watched the first decision on a symbol that had been rejected hundreds of times for wide_spread. New verdict: it cleared the spread gate and moved on to the next, real check. The wall was gone.

The takeaway

The lesson isn't "set the threshold higher." It's that a guard is only as good as the thing it measures, and it's dangerously easy to ship a proxy that looks like the real quantity. Touch-spread looks like cost — it's shaped like cost, denominated in bps like cost. It is not cost. The bot even had the correct measurement sitting right next to the wrong one.

When a gate is rejecting everything, the bug isn't always the threshold. Sometimes it's that you're measuring the wrong number with great precision.

Part of an ongoing build-in-public log about building a small algorithmic trading bot the slow, paper-first way.

Top comments (0)