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() raises DateOutOfBounds instead 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 to pip install -U exchange_calendars.
  • Compare dates, not strings or timestamps. last_session is a pandas Timestamp; converting it with .date() keeps the comparison with a plain datetime.date simple 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

Popular posts from this blog

Gear Ratio Calculator: Multi-Stage Ratio, Output Speed, Torque and Direction

Mecanum-Wheel Pick-and-Place Robot: From Arduino UNO to Mega2560 + ESP32-CAM

Voltage Divider Calculator: Output Voltage, or the Best E24 Resistor Pair for a Target Voltage