DEV Community

Sam Hartley
Sam Hartley

Posted on

Buy Was Always Right — Which Is Why the Bug Survived 383 Trades

A copy-trading tracker of mine keeps a paper ledger. One row per trade it mirrors from a tracked wallet, plus a settlement pass that marks each row win or lose once the market resolves.

The ledger balanced. Every row had a matching trade, every settlement had a source, and the running total moved in the direction the strategy predicted. I had reconciled it twice, line by line, against what the tracked wallets actually did.

Then I checked one invariant that had nothing to do with profit: does the token I booked equal the token I graded?

For 343 rows, yes. For 3 rows, no. Those 3 rows had booked a win of +364.57 on tokens that lost.

The invariant nobody writes down

The copy code does something simple: when a tracked wallet buys a token, my tracker buys the same token. One field, asset, decides what goes into the ledger.

The settlement code does something equally simple: it compares the market's outcome against direction — the side the strategy believed in.

For a buy, those are the same side. The wallet bought the token, so the token is the view. direction == asset in 343 out of 343 rows.

For a sell, they are opposite. The wallet sells the token it holds and its view flips to the other side. My code still booked asset — the token being handed away — while direction described the view. So the ledger held one side and scored the other.

Side Rows direction == held token
Buy 343 / 343 always
Sell 0 / 3 never

That's the whole bug. It is not a math error, not a rounding problem, not a stale cache. It's a position/label mismatch that can only exist in one branch, and that branch was 0.82% of the book.

The correction I had to make to my own diagnosis

I had been telling myself this was an inference bug — that my direction inference got sells wrong.

It didn't. The inference was correct. Ask the code what the wallet thinks and it answers properly, including on sells. What was wrong was the assumption underneath the copy: that the token the wallet traded is the token I should hold.

On a buy, "copy what they did" and "hold what they believe" happen to be the same instruction. On a sell they are two different instructions, and I had only ever written one of them.

I spent an hour looking at the wrong function because the wrong function was the one that had a bug-shaped name.

Why the numbers are worse than three rows suggest

Three rows out of 383 is noise if the error is symmetric. It isn't.

Booked Actually true
Total across 3 rows +364.57 −15.43

Two of the three were Yes/No markets where my settlement path never even touched the outcome token — they went through an odds-based path that compared a price to a direction. The third went through the normal path and still settled wrong: the market resolved up, my direction said up, and I was holding down.

One of them is the reason I stopped treating this as cosmetic. Entry price 0.015. Booked +327.64. A position opened for a penny and a half, marked as a 200x win, on the wrong side of the market.

Six percent of copyable flow can flip an entire book when the mistake compounds that way. Frequency of the bug tells you nothing about its size.

Four ways to fix it, three of them wrong

Buy the token, derive direction from the token. Label always correct, no inversion anywhere. But it means deliberately buying the side a wallet is exiting, every time. On the three measured rows: −15.43. The label becomes honest and the strategy becomes systematically bad. Fixing the accounting is not the same as fixing the trade.

Don't copy sells at all. A gate, placed before any network call. Costs 6.69% of copyable flow, which is higher than the 1.74% share in the raw stream — sells arrive at low prices, so they pass the price ceiling more often than buys do.

Mirror the short by buying the complement token. The correct trade, in theory. In practice: the complement's ask was never logged, and all I can bound is complement ≥ 1 − ask, a lower bound. Against a 0.50 ceiling, that bound blocked 3 of 3 backtest rows. Going forward, 1 of 27 short-opened sells in copyable range was mirrorable — 4% — and it came from the class I trust least. It's not a strategy, it's a rounding error with a theory attached.

I took the gate. It removes a proven error instead of adding a presumed edge, and those are different kinds of change. One of them I can verify in a backtest tonight.

The part I couldn't decide, and said so

The obvious follow-up: can I tell an exit from a short? If a wallet sells something it already holds, that's an exit and I should ignore it. If it sells something it doesn't hold, that's a short and I might want it.

I reconstructed positions per wallet and market from my own trade log. Of 1,435 sell rows, 857 had no earlier trade from that wallet at all — 59.7%. Another 578 sold more than the log showed them ever buying. Only 20 were provable partial exits.

76.2% of the "flat position, therefore short" rows came from wallets that had been in my log for under three days. My trade log is an observation window, not an account statement. It cannot answer a question about holdings, and building a rule on top of it would be guessing with extra arithmetic.

Deciding that requires the wallet's actual position history from the exchange, which is a separate piece of work. So the gate stands and the classifier waits.

What I actually take from this

Every test I had written for the copy path used a buy. Not through laziness — the fixtures came from real activity, and real activity is 99% buys. My tests sampled the same distribution as my logs, so the invariant I never wrote down was also never contradicted.

A bug that only lives in the rare branch survives precisely because everything you look at, by default, is the common branch. The invariant was true in 99.2% of rows. That is not a passing test. That is a blind spot with good coverage.

The check that finally caught it costs one comparison — the token I booked against the token I graded — and it doesn't need a fixture, a mock, or a network call. I now run it over the whole ledger whenever I reconcile, and it's the first thing I'd write again from scratch.

Top comments (0)