2. The market calendar as the source of truth
Level 9: Orchestration and the calendar
There is one line of code that shows up in almost every home-made system and is wrong in all of them: checking whether today is Monday to Friday to decide whether to trade. It is an approximation that gets it right on most days of the year, and that is precisely what makes it dangerous: it fails rarely, always by surprise, and almost always on days when the market is behaving oddly.
Why the day of the week is not enough
A real exchange calendar has, as a minimum, four things the day of the week does not know:
- Holidays. The US market closes on around nine or ten days a year, and several of them move: Good Friday, Memorial Day or Thanksgiving fall on different dates each year.
- Half sessions. Days when it opens at the normal time and closes in the early afternoon, such as the Friday after Thanksgiving, which on the New York Stock Exchange closes at one o'clock local time instead of four.
- Auctions and cut-offs. The closing price does not come from the last trade but from an auction, and market-on-close orders have an entry cut-off minutes earlier: on the New York Stock Exchange it is around 15:50 local time. Arriving at 15:58 with a market-on-close order is arriving late even though the market is still open.
- Different markets. If you trade New York, London and Frankfurt at the same time, they share neither holidays nor hours. On July 4 New York is closed and Europe is open; on December 26 the opposite happens at several venues.
The calendar changes, and that is why it cannot live in your code
The decisive argument against the hand-written holiday list is not that it is ugly, it is that it expires. In 2022 the New York Stock Exchange added June 19 as an exchange holiday: any system with the previous year's list frozen in tried to trade that day against a closed market. Auction times change too, extraordinary closures are added for national mourning, and half sessions are adjusted when Christmas Eve falls at a weekend.
The rule is: the calendar is data you query, not a condition written into the code. It may come from a maintained library, from a table in your database or from the broker itself, but it has to be something you can update without deploying and review without reading code.
import pandas_market_calendars as mcal
nyse = mcal.get_calendar("XNYS")
# Thanksgiving week 2026: Thursday the 26th does not appear because it is a holiday,
# and Friday the 27th appears with an early close.
sesiones = nyse.schedule(start_date="2026-11-23", end_date="2026-11-27")
print(sesiones.index) # days with a session, as dates
print(sesiones["market_open"]) # each day's open, in UTC
print(sesiones["market_close"]) # each day's close, in UTCNotice one detail that settles half this module: the library returns instants, not wall-clock times. That means it has already resolved daylight saving time for you and that you can compare them directly with the server's clock, which will be on UTC as you will see in module four.
Deciding whether to trade today, and at exactly what time
The query is not "is it open?", but two separate questions: whether there is a session today and at exactly what instant I am supposed to act. That instant is almost never the open or the close plainly, but a time derived from the day's real schedule.
from datetime import timedelta
def sesion_de(cal, dia):
"""Returns (open, close) for that day, or None if there is no session."""
s = cal.schedule(start_date=dia, end_date=dia)
if s.empty:
return None
fila = s.iloc[0]
return fila["market_open"].to_pydatetime(), fila["market_close"].to_pydatetime()
def instante_de_ejecucion(cal, dia, minutos_antes=15):
sesion = sesion_de(cal, dia)
if sesion is None:
return None # holiday or weekend: no trading
apertura, cierre = sesion
if cierre - apertura < timedelta(hours=4):
minutos_antes = 5 # half session: the usable window shrinks
objetivo = cierre - timedelta(minutes=minutos_antes)
return max(objetivo, apertura + timedelta(minutes=5))The scheduler can keep waking up every few minutes: the one who decides is not cron's clock, it is the calendar. The task wakes up, asks, and on the vast majority of wake-ups does nothing.
def toca_actuar(cal, ahora_utc, tolerancia_seg=180):
objetivo = instante_de_ejecucion(cal, ahora_utc.date())
if objetivo is None:
return False
retraso = (ahora_utc - objetivo).total_seconds()
return 0 <= retraso <= tolerancia_seg # neither early nor staleThe tolerance is not decoration. Without it, a wake-up arriving three seconds late never coincides with the target instant and the system never trades at all; with an infinite tolerance, a task starting at midnight after a restart would believe it still owed yesterday's session.
And there is one case worth deciding before it happens: what you do if the calendar cannot be queried because the library fails or the table does not respond. The safe answer is the usual one: if you do not know whether there is a session, you do not trade.
Worked scenario
A worked scenario, with example figures. A system sends its market-on-close orders at 15:45 New York time, fifteen minutes before the usual close, with the time written in by hand. On the Friday after Thanksgiving the market closes at one o'clock: at 15:45 it had been shut for almost three hours. That day the system did not trade, kept the previous day's position through the whole long weekend and nobody spotted it until Monday.
The cost was not one day's gap, but that the portfolio was left off target right before three sessions with no chance to adjust. With the instant derived from the calendar, the execution would have gone out at 12:55 local time without touching a line. Education, not financial advice.