I had another AI review my trading bot. It found three ways my books were wrong.

I wrote a trading bot with AI assistance, tested it, and ran it. Then I did something I had not done before: I handed the whole file to a different model, cold, with no context about how it was built, and asked it to look for accounting errors.

It came back with three. All of them were in code I had read many times.

None of them crashed anything. That is the part worth writing down.

The three

1. The bot recorded the size it asked for, not the size it got.

market_fill() returned the fill price and nothing else. So open_position(), add_in(), and close_out() all took the requested quantity and wrote that into state as if it were fact.

Exchanges do not always give you what you asked for. Contract counts get rounded. A single order can be truncated by the venue max market order size. When either happens, the exchange position and the bot idea of the position drift apart, silently, with no error anywhere, because from the bot perspective the order succeeded.

The fix was to widen the return value to (fill_price, actual_filled_qty, blended_fee_rate) and have every caller write the value that came back rather than the value it sent.

2. Exit fees were calculated against the entry price.

close_out() computed its fee from ep, the entry price, instead of fill_px, the price the exit actually filled at.

What makes this one instructive is that it was inconsistent with the rest of the same file. add_in() and open_position() were already using fill price. One function out of three was doing it differently, in a file I wrote, and I never noticed, because nothing about it looks wrong when you read it. ep is a real variable holding a real price.

3. Market fallbacks were billed at the maker rate.

The bot places post-only limit orders. If one does not fill within the timeout, it falls back to a market order.

The fee accounting charged the maker rate in every case, including the fallback. But a fallback is precisely the case where you stopped being a maker. Real fills in that path are a mix, so the correct number is a blended rate.

This one has a direction: it always understated cost. Every fallback made the books look slightly better than reality.

Three accounting bugs - what the bot recorded versus what actually happened at the exchange.

What they have in common is worse than what they are

Individually these are small. What connects them is that not one of them produces an error.

The bot keeps running. Orders keep going through. Logs look normal. State just quietly stops matching the exchange, or the fee number is quietly a little too low. There is no exception to catch, no alert to fire, no failed run to investigate. The only symptom is that the numbers you use to decide whether the strategy works are not the real numbers.

For a system whose entire job is to be right about money, that is the worst failure mode available: wrong and confident.

The backtest could not have caught them

After fixing them I re-ran the replay cross-validation to check I had not broken anything. The two runs differed by 0.03%.

That number is why the bugs survived so long. Three real accounting errors, and the offline validation moves by three hundredths of a percent.

The reason is that the replay path does not exercise the code that was broken. Fill quantities always equal requested quantities in a simulation - there is no venue rounding your contracts or truncating your order. Post-only orders never time out, so the market fallback branch never runs. The only difference the fix produced offline was the exit-fee basis, which is a small consistent adjustment.

The bugs lived entirely in the live path, and the live path is the one you cannot replay. I cannot test what happens when the exchange fills 0.97 of what I asked for without an exchange filling 0.97 of what I asked for.

So the 0.03% is not reassurance that the fix was minor. It is a measurement of how blind the offline test is to this class of problem.

The replay path differed by 0.03% because it never exercises partial fills or maker-order timeouts, the exact conditions the bugs lived in.

The second review found something worse

I sent the same file back for a deeper pass. That one found a bug that would not have been quiet at all.

Exit orders were not marked reduce-only.

On this exchange, that flag is what says an order may only shrink an existing position, never open a new one. My exit orders did not set it.

Combine that with the minimum-size rule the bot enforces - at least one contract - and there is a path where closing a small remainder submits an order larger than the remaining position. Without the flag, the exchange closes what is left and opens a new position in the opposite direction with the excess.

An unmonitored bot that occasionally opens surprise reversed positions is not a bug you find in a log review. You find it in your balance.

The same review also flagged that the state lock was only held inside the save function, while the position-sync thread and the main loop both mutate state fields directly - a race that had been sitting there since I added the sync thread.

It also checked the indicator math against the backtest engine line by line and found nothing wrong there. That null result mattered: it told me the problem was concentrated in execution and accounting, not in the strategy logic I had actually been worrying about.

Porting the fix taught me more than the fix

The bot above is one of five I run. The obvious next step was to apply the same fix to the other four.

That would have been wrong, and checking each one is what showed me why.

The other four use percentage-based PnL accounting. Their return math is built on fractions of notional, not absolute contract counts, so a quantity mismatch on close genuinely has no effect there - the same bug is harmless because the surrounding accounting is different.

But not everywhere in them. The open-position path in all four, and the rescue path in the three 15-minute bots, feed quantity straight into average-price and notional calculations. Same bug, real impact, different function.

And one bot has no pyramiding or rescue logic in its live code at all, so most of it simply did not apply.

Three different answers across four codebases descended from the same original file. Porting a fix to sibling systems is not a mechanical operation. It requires reading how each one accounts for money before deciding whether the fix means anything there.

The same fix applied differently across four bots - real impact, no impact, and not applicable, depending on each bot's accounting model.

How I run these reviews

The setup matters more than the model choice, so here is exactly what I do.

Send the whole file, not a diff. A diff shows what changed. Two of the three bugs above were about inconsistency between functions - one place using fill price while another used entry price - and that is invisible unless the reviewer can see both.

Give no context on purpose. No explanation of the strategy, no "this part is fine," no history. Context is what makes me blind to my own code; handing it over exports the blindness.

Name the class of bug you want. "Review this" gets style opinions. "Look for accounting errors: places where the recorded value could differ from what the exchange actually did" gets the list above. The narrower the ask, the better the return.

Use more than one model, in separate passes. The first review found the three accounting bugs. A second pass on the same file found the reduce-only flag and the state race - things the first one had not mentioned. They were not redundant.

Verify every finding in the code before acting. This is the step people skip. A review will also produce confident claims that are wrong, and a "fix" applied to a non-bug in money-handling code is a new bug. Each of the three above got traced to the exact line and reasoned through before I changed anything - and the indicator-math check came back clean, which was equally worth knowing.

What I do differently now

Have something with no context read the code. Every one of these was visible in a file I had read repeatedly. Familiarity is what hid them - I knew what each line was supposed to do, so I saw that instead of what it did. A reviewer with no memory of writing it has no such handicap.

Return what happened, not what you asked for. Any function that talks to an external system and returns less information than the system gave it is throwing away the part where reality disagreed with you.

Ask which code paths your tests cannot reach. My offline validation was thorough about strategy and blind about execution. Knowing which half is untested is more useful than the test result itself.

Treat silent correctness bugs as their own category. Crashes announce themselves. Wrong-but-running does not, and in a system that handles money it is the only category that costs you something while looking fine.

For the record, these bots run on paper, not live capital, and the figures here are validation numbers rather than returns. That is the point of the post, in a way: I found three accounting errors before real money was involved, and only because I stopped trusting my own reading of my own code.

Comments

Popular posts from this blog

I thought my pipeline retried 6 times. It was making up to 48 billed calls.

I let AI run two blogs for a month. Google indexed 1 post out of 81.