120 lines
4.2 KiB
Python
120 lines
4.2 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import date, timedelta
|
|
|
|
_A_SHARE_SUFFIXES = {"SH", "SS", "SZ"}
|
|
|
|
# Mainland exchanges close on weekends plus the annual State Council public-holiday windows.
|
|
# Weekend make-up workdays do not become exchange trading days.
|
|
_A_SHARE_HOLIDAYS = {
|
|
date(2024, 1, 1),
|
|
*[date(2024, 2, day) for day in range(10, 18)],
|
|
*[date(2024, 4, day) for day in range(4, 7)],
|
|
*[date(2024, 5, day) for day in range(1, 6)],
|
|
*[date(2024, 6, day) for day in range(8, 11)],
|
|
*[date(2024, 9, day) for day in range(15, 18)],
|
|
*[date(2024, 10, day) for day in range(1, 8)],
|
|
date(2025, 1, 1),
|
|
*[date(2025, 1, day) for day in range(28, 32)],
|
|
*[date(2025, 2, day) for day in range(1, 5)],
|
|
*[date(2025, 4, day) for day in range(4, 7)],
|
|
*[date(2025, 5, day) for day in range(1, 6)],
|
|
*[date(2025, 5, day) for day in range(31, 32)],
|
|
*[date(2025, 6, day) for day in range(1, 3)],
|
|
*[date(2025, 10, day) for day in range(1, 9)],
|
|
*[date(2026, 1, day) for day in range(1, 4)],
|
|
*[date(2026, 2, day) for day in range(15, 24)],
|
|
*[date(2026, 4, day) for day in range(4, 7)],
|
|
*[date(2026, 5, day) for day in range(1, 6)],
|
|
*[date(2026, 6, day) for day in range(19, 22)],
|
|
*[date(2026, 9, day) for day in range(25, 28)],
|
|
*[date(2026, 10, day) for day in range(1, 8)],
|
|
}
|
|
|
|
|
|
def is_non_trading_day(ticker: str, day: date) -> bool:
|
|
"""Return whether the requested date is a known non-trading day for the ticker's market."""
|
|
if day.weekday() >= 5:
|
|
return True
|
|
if _is_a_share_ticker(ticker):
|
|
return day in _A_SHARE_HOLIDAYS
|
|
return _is_nyse_holiday(day)
|
|
|
|
|
|
def _is_a_share_ticker(ticker: str) -> bool:
|
|
suffix = ticker.rsplit(".", 1)[-1].upper() if "." in ticker else ""
|
|
return suffix in _A_SHARE_SUFFIXES
|
|
|
|
|
|
def _is_nyse_holiday(day: date) -> bool:
|
|
observed_new_year = _observed_fixed_holiday(day.year, 1, 1)
|
|
observed_juneteenth = _observed_fixed_holiday(day.year, 6, 19)
|
|
observed_independence_day = _observed_fixed_holiday(day.year, 7, 4)
|
|
observed_christmas = _observed_fixed_holiday(day.year, 12, 25)
|
|
|
|
holidays = {
|
|
observed_new_year,
|
|
_nth_weekday(day.year, 1, 0, 3), # Martin Luther King, Jr. Day
|
|
_nth_weekday(day.year, 2, 0, 3), # Washington's Birthday
|
|
_easter(day.year) - timedelta(days=2), # Good Friday
|
|
_last_weekday(day.year, 5, 0), # Memorial Day
|
|
observed_independence_day,
|
|
_nth_weekday(day.year, 9, 0, 1), # Labor Day
|
|
_nth_weekday(day.year, 11, 3, 4), # Thanksgiving Day
|
|
observed_christmas,
|
|
}
|
|
if day.year >= 2022:
|
|
holidays.add(observed_juneteenth)
|
|
|
|
# When Jan 1 falls on Saturday, NYSE observes New Year's Day on the prior Friday.
|
|
if day.month == 12 and day.day == 31:
|
|
next_new_year = _observed_fixed_holiday(day.year + 1, 1, 1)
|
|
if next_new_year.year == day.year:
|
|
holidays.add(next_new_year)
|
|
|
|
return day in holidays
|
|
|
|
|
|
def _observed_fixed_holiday(year: int, month: int, day: int) -> date:
|
|
holiday = date(year, month, day)
|
|
if holiday.weekday() == 5:
|
|
return holiday - timedelta(days=1)
|
|
if holiday.weekday() == 6:
|
|
return holiday + timedelta(days=1)
|
|
return holiday
|
|
|
|
|
|
def _nth_weekday(year: int, month: int, weekday: int, occurrence: int) -> date:
|
|
first = date(year, month, 1)
|
|
delta = (weekday - first.weekday()) % 7
|
|
return first + timedelta(days=delta + 7 * (occurrence - 1))
|
|
|
|
|
|
def _last_weekday(year: int, month: int, weekday: int) -> date:
|
|
if month == 12:
|
|
cursor = date(year + 1, 1, 1) - timedelta(days=1)
|
|
else:
|
|
cursor = date(year, month + 1, 1) - timedelta(days=1)
|
|
while cursor.weekday() != weekday:
|
|
cursor -= timedelta(days=1)
|
|
return cursor
|
|
|
|
|
|
def _easter(year: int) -> date:
|
|
"""Anonymous Gregorian algorithm."""
|
|
a = year % 19
|
|
b = year // 100
|
|
c = year % 100
|
|
d = b // 4
|
|
e = b % 4
|
|
f = (b + 8) // 25
|
|
g = (b - f + 1) // 3
|
|
h = (19 * a + b - d - g + 15) % 30
|
|
i = c // 4
|
|
k = c % 4
|
|
l = (32 + 2 * e + 2 * i - h - k) % 7
|
|
m = (a + 11 * h + 22 * l) // 451
|
|
month = (h + l - 7 * m + 114) // 31
|
|
day = ((h + l - 7 * m + 114) % 31) + 1
|
|
return date(year, month, day)
|