Skipping Cron Jobs on Taiwan Stock Market Holidays with exchange_calendars
I run a few scheduled jobs around the Taiwan stock market: an intraday scan at 13:20 and a post-close screen plus a portfolio alert at 15:30, Monday to Friday. Cron's 1-5 handles weekends, but it knows nothing about market holidays. On those days the jobs still ran, fetched empty data, and sent alerts that meant nothing.
The fix is one small Python module and one cron pattern. No hand-maintained holiday list required.
exchange_calendars knows the TWSE calendar
exchange_calendars is an open-source library with trading calendars for 50+ exchanges. The Taiwan Stock Exchange is XTAI:
pip install exchange_calendars
Before trusting it, I listed every 2026 weekday that it says is not a trading session:
import exchange_calendars as xc
import pandas as pd
cal = xc.get_calendar("XTAI")
sessions = cal.sessions_in_range("2026-01-01", "2026-12-31")
weekdays = pd.bdate_range("2026-01-01", "2026-12-31")
print([d.strftime("%m-%d") for d in weekdays if d not in sessions])
['01-01', '02-12', '02-13', '02-16', '02-17', '02-18', '02-19', '02-20', '02-27',
'04-03', '04-06', '05-01', '06-19', '09-25', '09-28', '10-09', '10-26', '12-25']
I compared this against the TWSE's official 2026 holiday schedule and it matched exactly, including the less obvious days:
- Feb 12–13: the two days before the Lunar New Year break, when the exchange runs settlement only and there is no trading.
- Observed days: when a holiday falls on a weekend, the weekday off moves. Feb 27 (Peace Memorial Day), Apr 3 and Apr 6 (Children's Day and Tomb Sweeping Day), Oct 9 (National Day) and Oct 26 (Retrocession Day) are all in the list.
- Sep 28, Oct 26 and Dec 25: holidays that only came back to Taiwan's calendar recently. A hard-coded list copied from an older year would miss them.
A trading-day check with an exit code
The module answers one question and reports it as an exit code, the language cron and the shell already speak: 0 on a trading day, 1 when the market is closed.
"""Is today a Taiwan Stock Exchange trading day?
python -m trading_day # exit 0 on a trading day, 1 on a holiday
python -m trading_day 2026-10-26 # check a specific date
"""
import datetime as dt
import sys
import exchange_calendars as xc
_cal = None
def is_trading_day(day: dt.date | None = None) -> bool:
global _cal
if _cal is None:
_cal = xc.get_calendar("XTAI") # Taiwan Stock Exchange
day = day or dt.date.today()
if day > _cal.last_session.date():
# The bundled calendar only covers about a year ahead.
print(f"warning: XTAI calendar ends {_cal.last_session.date()}, "
f"treating {day} as a plain weekday; upgrade exchange_calendars",
file=sys.stderr)
return day.weekday() < 5
return bool(_cal.is_session(day.isoformat()))
def main() -> int:
day = dt.date.fromisoformat(sys.argv[1]) if len(sys.argv) > 1 else dt.date.today()
ok = is_trading_day(day)
print(f"{day} {'trading day' if ok else 'market closed'}")
return 0 if ok else 1
if __name__ == "__main__":
sys.exit(main())
$ python -m trading_day 2026-10-26; echo "exit=$?"
2026-10-26 market closed
exit=1
$ python -m trading_day 2026-10-27; echo "exit=$?"
2026-10-27 trading day
exit=0
Two details are worth copying:
- The calendar has an end date. The bundled data only runs about a year ahead; with version 4.13.2 it ends in October 2027. Past that,
is_session()raisesDateOutOfBoundsinstead of giving an answer. Rather than let the job crash, the module falls back to "weekday = trading day" and prints a warning into the log, which is the reminder topip install -U exchange_calendars. - Compare dates, not strings or timestamps.
last_sessionis a pandas Timestamp; converting it with.date()keeps the comparison with a plaindatetime.datesimple and timezone-free.
Gating cron jobs: mind the braces
With an exit code, gating a job is just &&. The one trap is a gate in front of several commands:
# m h dom mon dow command
# 13:20 on weekdays: one job, gated on the trading calendar
20 13 * * 1-5 cd ~/stocks && venv/bin/python -m trading_day >> logs/intraday.log 2>&1 && venv/bin/python -m intraday_scan >> logs/intraday.log 2>&1
# 15:30 on weekdays: TWO jobs behind one gate -- note the { ...; } group
30 15 * * 1-5 cd ~/stocks && venv/bin/python -m trading_day >> logs/daily.log 2>&1 && { venv/bin/python -m daily_screen >> logs/daily.log 2>&1; venv/bin/python -m portfolio_alert >> logs/alert.log 2>&1; }
In the shell, check && a; b means "run a if the check passes, then run b no matter what". Without the { …; } group, the portfolio alert would still fire on holidays. With the braces, both commands sit behind the gate. (The space after { and the ; before } are both required.)
I also log the check's own output (2026-10-26 market closed) into the job's log, so a quiet day in the log is clearly a holiday rather than a broken job.
What the calendar can't know
Typhoon closures. Taiwan's markets close when a typhoon day is declared, and that decision is made the day before or on the day itself. No calendar package can include it in advance. My daily screen copes on its own: if the exchange returns no data for the day, it treats the day as closed and exits quietly. If your jobs can't detect that, a manual skip on typhoon days is the remaining gap.
Summary
exchange_calendars' XTAI calendar matched the TWSE's 2026 schedule exactly, including observed days and the recently restored holidays.- Wrap it in a script that exits 0 or 1, and gate jobs with
&&. - Use
{ a; b; }when one gate protects several commands. - Handle the calendar's end date gracefully, and remember that typhoon days need their own check.
Comments
Post a Comment