What broke
Booking time read as event time: two false alarms about trading after hours
Twice in one week, a health check in the stock app reported trades outside market hours. Neither was a trade made after hours: first dividends, then broker fills that were booked when reconciliation saw them. Claude wrote the check, and both times it read the time a row was booked as the time the event happened.
The short version
What went wrongA health check counted trades outside market hours by the row's timestamp. Dividends booked at noon on the ex-date and broker fills recorded at reconciliation time both showed up as after-hours trades.
How we found itHenning asked whether an alert really meant trading outside market hours. Claude checked the flagged rows against the data before changing anything, first on 7 August and again on 11 August.
The rule nowKnow what a timestamp records before you judge by it. Share the marker between the code that writes a row and the check that reads it, and show what a check leaves out instead of hiding it.
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 has a health check, added by Claude on 30 July 2026, that counts trades from the last 30 days made outside the exchange’s opening hours. A simulation that trades while the exchange is closed produces numbers that could not happen in reality, so the check is there to catch that.
Within five days in August 2026 it went stale twice, and both times nothing was wrong with the trading. Claude had written the check, and both times it made the same mistake: it treated the time a row was booked as the time the event happened.
The commit messages are in Norwegian. The quotes below are our translations.
7 August: dividends counted as trades
The check reported 6 of 227 trades outside opening hours. Henning asked whether it really meant trading outside market hours. The commit opens with the answer: “Yes, and it was false.”
Claude checked the six rows against the data. They were dividends from one US stock, booked at 12:00 UTC on the ex-date, 4 August, one row per simulation. The NYSE opens at 15:30 Oslo time, so the booking time fell outside the window. But a dividend is booked on the ex-date whatever the time of day. It is not a trade.
The code already said so. A comment above the filter stated that dividends are booked on the ex-date regardless of the clock, and are not trades. The filter under it did something else: it removed rows that had no exchange, and a dividend row has one. The bug showed up only now because the dividend code had just booked its first dividends.
After the fix the check counted 0 of 221 real trades outside opening hours. The commit explains why the false alarm mattered:
“An alarm that shouts about something harmless is worse than no alarm: if a REAL trade outside market hours happens now, it drowns in the noise.”
It also notes that twice that same week the opposite bug had been fixed: a guard that stayed silent when it should have raised an alarm. This was the same mistake the other way round.
11 August: broker fills booked at reconciliation time
Four days later the check flagged five more trades outside opening hours.
They came from a forced reconciliation run on the evening of 10 August, at 22:08. Reconciliation compares the positions the broker holds with the app’s own books and writes in what the books are missing. The broker had filled these orders during opening hours. But a difference in positions carries no clock, so reconciliation records the time it observed the change. The check read that observation time as the time of the trade.
The commit spells out the cost: the check would have stayed stale for 30 days over real, correct trades and drowned a real alarm, “which is exactly what its own comment warns against.”
The fix
The second fix does three things.
- One shared marker. Reconciliation marks the rows it writes with a fixed note prefix. That prefix is now exported, so the code that writes the rows and the check that reads them use the same constant instead of each keeping a copy of the string.
- Left out, but shown. Mirrored rows outside opening hours are no longer counted, but the check reports how many it held outside. A number that silently drops rows can’t be told apart from one where nothing was dropped.
- Contract tests. Two new tests lock the link between writer and reader, so a change on one side breaks the build instead of bringing the false alarm back.
Leaving mirrored rows out is safe for a specific reason the commit gives: the broker cannot fill a limit order outside market hours. So a mirrored row is always a fill that happened during opening hours.
The two incidents went into the project’s catalogue of error classes as one class: “booking time read as event time.”
Part of a larger pattern
These were not the only false alarms from the stock app’s guards. Other posts here describe a cycle-status tool that raised alarms on normal cycles because it kept its own list of “normal” values, and a stuck-portfolio alarm that fired four times while the portfolio was full or the cycle had already traded. The rule from all of them is the same: a guard that goes off during normal operation gets ignored, so a false alarm is a bug just like a missed one.
The rule
Before a check judges by a timestamp, find out what that timestamp records: when the event happened, when it was booked, or when someone noticed. If the code that writes a row marks it as special, the check should read that marker from the same constant, not from a copy. And when a check leaves rows out, it should say how many, so that a smaller number can’t hide a missing one.