What brokeUpdated 3 October 2026
The double-order guard sat in the legacy path while the whole fleet ran the target-portfolio engine
A guard meant to stop the stock app from placing a second order on a stock that already had one open sat in a code path that nothing live used. On 5 August two buy orders for the same stock were open at once. The next day the sell side, which the fix had missed, let a duplicate sell through, and only the broker rejected it.
The short version
What went wrongThe guard against a second open order sat in the legacy candidate loop while the whole fleet ran the target-portfolio engine, so duplicate buys got through, and the sell side was missed too.
How we found itA duplicate buy appeared in the broker's open-order list on 5 August, and on 6 August the broker rejected a duplicate sell.
The rule nowPut the guard in the one function every order goes through, for both buys and sells, check our own order reference, count orders from the same cycle, and don't rely on the broker.
This post documents something Claude got wrong. The claim rests on a commit or a file, not on memory. All of Claude's mistakes
The stock app places real orders with a broker in scheduled cycles. One of its guards is simple: if an order for a stock is already open at the broker, don’t place another one. In early August 2026 we found that this guard did nothing where it mattered. A day later we found the same gap on the sell side.
A note on attribution. Henning writes no code himself, so the original guard, and the gap in it, were Claude’s.
What the 18:00 cycle did
On 5 August the 18:00 cycle placed four orders. One of them was a second buy order for a stock that already had a buy order open. The broker’s open-order list showed both:
- a buy order, status Working, placed by the 18:00 cycle
- a buy order for the same stock, status Working, placed by the 17:00 cycle
If both had filled, we would have owned the stock twice over. The commit message is clear that the app’s idempotency doesn’t catch this. Each order carries its own client order reference. To the broker these were two different orders, and in its own terms it was right.
Why the guard didn’t fire
The app has two decision paths. The older one, which the commit calls the legacy path, loops over a list of buy candidates. The newer one is the target-portfolio engine. The commit message states that the whole fleet, including the live account, runs the target-portfolio engine.
The open-order check sat inside the candidate loop in the legacy path. The commit says there were “zero references” to the open-order list anywhere in the target-portfolio branch. The guard existed, but the target-portfolio path that the fleet and the live account run never reached it.
The commit message also points out that this was the same trap an internal note on the rotation window describes: a case where a measurement sat in the legacy path while the fleet ran the target-portfolio engine. This time it was a guard rather than a measurement.
The fix on the buy side
The check moved into the single function every buy goes through. The commit message gives the reason: a new branch inherits the guard instead of having to remember it.
The fix changed two more things.
First, the check now compares against our own order reference as well as the symbol the broker reports. The broker’s open-order lookup works out the symbol through a second instrument-details call. If that call fails, the symbol comes back as a raw instrument ID instead. A check on symbol alone would then let the duplicate through without any warning. Our own reference is a string we build, so it can’t fail that way.
Second, the list of open orders is fetched once, at the start of the cycle. Without a change, the guard couldn’t see an order placed earlier in the same cycle. Now each order the app places is added to that list right away, so two buys of the same stock in one cycle are caught too.
We cancelled the duplicate order with a new cancel option in the open-orders tool. The tool now looks for duplicates (same stock, same side), prints them with a warning and exits with an error code instead of just listing them.
The next day: the sell side
On 6 August the owner asked whether there was anything we needed to check that day. There was. The cycle log showed:
- 13:30, a sell order on a holding being swapped out, submitted
- 14:30, a second sell order on the same holding, rejected by the broker because a sell order already existed for it
The app had placed a sell order on the same stock twice, while the first was still open. The 6 August commit says it plainly: the guard went into the buy function on 5 August, and the sell side was forgotten.
The broker caught it, so nothing went wrong. The commit message explains why that isn’t good enough. Relying on the broker to stop double trades means relying on a guard we don’t control. Another broker, or another order type, might accept both. If two sells fill on a position we own once, we end up short.
The sell-side fix follows the buy side: check the broker’s open orders and our own reference, and add each new sell to the open-order list straight away so two sells in one cycle are caught.
Two smaller failures found along the way
Positions shown one cycle late. On 5 August the owner reported that the live view did not show all positions. Two holdings had filled at the broker, but the app only reconciled with the broker at the start of a cycle, so a fill showed up in the next one. In the code comment, the gap is put at 30 minutes. The app now also reconciles after any cycle that placed orders. That second pass affects no decision. It only updates the books, so if it fails it logs a warning and the cycle carries on.
A watchdog that cried wolf. The tool that reports the latest cycle had been changed to flag unusual outcomes. The 5 August commit message says that only the outcomes “ok” and “ran” had been put on the list of normal outcomes. So the tool flagged an abnormal cycle for a cycle that had simply traded. The commit’s reasoning is that a guard that fires on normal operation gets ignored, which makes it worse than no guard. The list now covers the known normal outcomes and is still a list of known good outcomes, so a new, unknown outcome raises a warning instead of passing silently.
Blocked, with no reason given. On 6 August the status said one order was blocked, but the cycle tool showed zero details. The reason had been in memory, written to the console and then dropped. The stored status only kept a few top-level counts, and the tool read fields that nothing wrote. The cycle summary and up to 25 detail lines, plus the total count, are now stored. (The blocked order turned out to be correct: it was stopped by a position limit that worked as intended.)
The status check on 6 August reported 34 jobs, 2 warnings (both weekly jobs that hadn’t run yet), 16 of 16 freshness checks passing, and no duplicate orders open.
The rule
Put a guard in the one place every order goes through, not in one branch of the logic. A new branch should get the guard automatically, not have to remember it. Build it for both buys and sells at the same time.
Check against our own order reference as well as the broker’s symbol lookup, because the lookup can fail without warning. And count orders placed earlier in the same cycle.
Don’t count on the broker to catch double trades. When it does, that was luck, not a guard we control.
Corrections
- Attribution corrected: an earlier version said the material did not show who wrote the code and did not put the mistake on Claude. Henning writes no code himself; the code was written by Claude.