The Bot That Tried to Trade on Constitution Day

Two days spent putting a broker-API validation layer on top of a hand-made calendar, after a missing market holiday caused an incident.

The trading bot runs on its own while nobody is watching, so it has to know by itself whether the market is open today. What that judgment was resting on is the problem in this episode.

Job failure alerts started piling up in Telegram from Friday morning. It was Constitution Day (a Korean public holiday).

What I set out to do

The trading bot runs unattended on a Mac mini as a resident launchd daemon, on a paper-trading account. Business-day determination depends on a hand-made market holiday calendar file. But on Constitution Day morning, three jobs including the trading cycle sent failure alerts. I asked Claude Code.

Today is a public holiday and the market is closed, so why did it try to trade?

The goals were to find out why it attempted to trade on a market holiday, check whether other market holidays were missing too, and prevent a recurrence.

How I asked

Claude opened the calendar file first. Not a single July market holiday was registered. With no Constitution Day, the system assumed an ordinary business day and ran the 09:05 trading cycle, and since the market was closed and there were no quotes, the cycle ended in failure. Real orders: 0. Fortunately it wasn’t a bad order going out, just spinning and failing. I then asked whether the other market holidays could be learned from an API, and Claude found the broker’s market holiday API and started a full comparison of the entire year against the calendar.

Where it broke

It got stuck twice. The first was an AI misjudgment. It sampled only six points in time and produced a list claiming “Lunar New Year and Chuseok are also missing from the API.” There’s no way the Lunar New Year holidays are trading days. Fortunately it sensed the oddity on its own and corrected itself: this API returns only 24 days per call, and the six samples had missed every date outside that window. Walking through 24 days at a time and comparing again, across all 384 days the only omission was Constitution Day, and the number of days wrongly marked as holidays was 0.

The second was the real twist. When I asked why a hand calendar was built when an API like this exists, it turned out the market holiday API is blocked on the paper-trading domain. Measured for real: the paper side returns a “TR not supported in paper trading” error, the live side responds normally. Since trading runs on the paper account, it couldn’t be used, and the sole dependence on the hand calendar was written up in the design doc as a known constraint. That constraint blew up on this day.

How I fixed it

The breakthrough already existed. There was a read-only broker on the live domain that had been attached for another observation feature. Trade on paper, but verify business-day determination alone through the live API. Instead of discarding the calendar, I put a validation layer on top. The API is the source of truth; on mismatch, skip trading and alert (once per date); on API failure, fall back to the calendar. Including a test that “the API overrides a missing market holiday,” the tests went from 112 to 114.

I thought that was the end, but at midday the next day, Saturday, another alert came.

It’s Saturday but I got a message saying it did virtual trading. Check it.

Same symptom, so I wondered if it was yesterday’s problem, but it was a completely different bug. Of the six jobs, only the trading cycle was missing the business-day guard. The other jobs skip on market holidays, but the trading cycle alone ran every day, and since this was the first weekend of operation, it surfaced for the first time today. The Claude-approach track, where the LLM makes a judgment daily, left 1 record of a virtual fill of 3 shares of the treasury bond ETF on a Saturday. I added the guard and a regression test that “trading logic must not be called on a market holiday.”

Lessons