Source code → github.com/pon00050/kr-trading-calendar
A subtle bug recurs across every back-test, event study, and anomaly screen written against Korean equity data: the code shifts a reference date by N calendar days when it meant to shift by N trading days. The two are not the same number, and the gap is large.
The Off-By-37% Problem
The Korea Exchange (KRX) closes on weekends and on roughly fifteen non-weekend holidays per year — 설날 (Lunar New Year, three days), 추석 (Chuseok, three days), 광복절, 어린이날, 부처님 오신 날, 신정, 삼일절, 현충일, and a small number of additional government-declared closures. Combined with weekends, a typical year yields about 248 trading sessions out of 365 calendar days.
Translate that into a window: subtract 60 calendar days from any date in the trading year and you land 60 days earlier on the calendar — but only ~38 of the days in between were actual KRX sessions. The window is short by 22 sessions, a 37% gap.
For an event study around a corporate filing, that means your “60 trading days before” baseline isn’t 60 trading days; it’s roughly 38. For a moving average over the prior quarter, your window slides by a different number of observations every time it crosses a holiday block. For a CB issuance screen looking at price drift in the ±60 trading days around the issuance date, the entire windowing assumption breaks if calendar arithmetic is used.
The bug is silent. The code runs. The numbers look reasonable. They are wrong by a third.
What This Package Does
Three functions, no more.
is_trading_day(date) returns True if the date is a KRX session — accounting for weekends, statutory holidays, and KRX-specific closures.
trading_day_offset(date, n) returns the KRX session n trading days from date. Positive n moves forward in time, negative moves backward. If date is not itself a session — a Saturday, a holiday — the function snaps to the nearest session in the offset direction before counting, which preserves the “N sessions away” semantics regardless of the starting day.
trading_days_in_range(start, end) returns every KRX session between two dates inclusive, as a pd.DatetimeIndex.
That is the entire public API. There is no calendar object to instantiate, no configuration, no state. The functions accept either str or pd.Timestamp for date arguments. Every return value normalizes to midnight via pd.Timestamp.normalize().
Where the Holiday Data Comes From
The package is a thin wrapper around the exchange_calendars library, specifically its XKRX calendar. That library is the authoritative open-source source for KRX session data — it tracks Korean statutory holidays, observed closures, and KRX-specific market closures going back to 2000.
Wrapping exchange_calendars rather than reimplementing the holiday table is a deliberate choice. The Korean holiday calendar is not stable: 설날 and 추석 follow the lunar calendar and shift by year; the government periodically declares ad-hoc public holidays (임시공휴일) for elections, bridge days, or one-off commemorations; the KRX schedules special closures for system maintenance. Maintaining that holiday table by hand would require continuous attention. exchange_calendars is community-maintained and updated as KRX publishes its annual session calendar.
The trade-off: ad-hoc holidays declared on short notice take days to weeks to land in exchange_calendars via community pull request. For live or current-year analysis where this matters, cross-check against the data.go.kr 특일정보 API (dataset 15012690), which is the government’s authoritative ad-hoc holiday feed.
What This Package Does Not Do
It does not provide intra-day session times — half-day closures, opening-bell delays, or extended trading hours are not modeled. It returns dates, not timestamps with KST offsets.
It does not model bond market sessions, futures market sessions, or KOSDAQ-only versus KOSPI-only closures. KRX rarely splits closures across markets, but when it does, this library treats the day as closed if any of the equity sessions are closed.
It does not include historical reconstructions of pre-2000 sessions. exchange_calendars covers 2000–2024 reliably. Older data would require reconstructing the pre-2000 holiday calendar from primary sources, which is out of scope.
Why It Was Extracted
This package was originally embedded in a larger forensic accounting toolkit, where it powered the ±60 trading-day price windows used in convertible bond event analysis and timing anomaly detection. The functions were small, well-tested, and useful far beyond the toolkit — any analyst writing Korean market code needs this.
Extracting it as a standalone library means anyone working with KRX data can uv add it, get three functions, and stop reinventing the calendar arithmetic. The package has thirteen tests, covers the holiday cases that consistently bite production code (설날, 추석, weekend snap behavior), and depends on nothing except pandas and exchange_calendars.
The library is at github.com/pon00050/kr-trading-calendar. MIT license. Install with uv add git+https://github.com/pon00050/kr-trading-calendar.