TRADING_HOURS
Determine whether a given date/time is a valid market trading day and whether the current moment falls within a configurable market window.
Installation
pip install TRADING_HOURS
Quick Start
Set environment variables in .env
PROJECT_DIRECTORY=/path/to/your/project
Config Files (required)
Place these two JSON files relative to PROJECT_DIRECTORY:
{PROJECT_DIRECTORY}/_CONFIG/{EXCHANGE}/trading_holidays.json
{PROJECT_DIRECTORY}/_CONFIG/{EXCHANGE}/market_timings.json
trading_holidays.json format:
{
"market_holidays": [
{
"year": 2026,
"holidays": [
{
"date": "26-Feb-2026",
"day": "Thursday",
"description": "Mahashivratri"
}
]
}
]
}
market_timings.json — session format (multiple named sessions, required):
{
"market_timings": {
"timezone": "UTC",
"trading_days_in_week": 5,
"weekend_days": ["Saturday", "Sunday"],
"trading_minutes": 375,
"sessions": [
{ "name": "premarket", "open": "07:00", "close": "09:00", "breaks": null },
{ "name": "regular", "open": "09:00", "close": "15:00",
"breaks": [{"start": "11:30", "end": "12:30"}] },
{ "name": "afterhours", "open": "15:00", "close": "17:00", "breaks": null }
]
}
}
Import and use
from TRADING_HOURS import (
is_trading_day,
is_today_trading_day,
was_date_trading_day,
find_next_trading_date,
get_date_n_trading_days_later,
is_market_open,
processing_market_window,
)
from datetime import date
# Check if a specific date is a trading day
is_trading_day(date(2025, 7, 4)) # → True (Friday)
is_trading_day(date(2025, 7, 5)) # → False (Saturday)
# Check today
is_today_trading_day() # → True / False
# Historical check
was_date_trading_day(date(2025, 1, 1)) # → True / False
# Find next trading date
find_next_trading_date(date(2025, 7, 4)) # → date(2025, 7, 7) (Monday)
find_next_trading_date(date(2025, 7, 4), inclusive=True) # → same if already trading day
# Settlement date calculations (T+1, T+2, etc.)
get_date_n_trading_days_later(date(2025, 7, 4), 1) # → date(2025, 7, 7)
get_date_n_trading_days_later(date(2025, 7, 4), 2) # → date(2025, 7, 8)
get_date_n_trading_days_later(date(2025, 7, 4), 0) # → date(2025, 7, 4) (T+0)
# Check if market is currently open
is_market_open() # → True / False
# Check with configurable pre/post buffers
in_window, phase = processing_market_window(minutes_before_open=15, minutes_after_close=30)
# → (True, "regular") if in regular hours
# → (True, "afterhours") if in after-hours
# → (False, "Market closed") if outside all sessions
# Check only specific sessions
in_window, phase = processing_market_window(1, 0, sessions=["premarket"])
# → (True, "premarket") 1 minute before premarket open
in_window, phase = processing_market_window(0, 0, sessions=["regular"])
# → (True, "regular") only checks regular hours
Configuration
Environment Variables
| Variable | Required | Default | Description |
|---|---|---|---|
PROJECT_DIRECTORY |
Yes | — | Root directory for config file storage |
Config File Paths
| File | Path |
|---|---|
| Holiday calendar | {PROJECT_DIRECTORY}/_CONFIG/{EXCHANGE}/trading_holidays.json |
| Market timings | {PROJECT_DIRECTORY}/_CONFIG/{EXCHANGE}/market_timings.json |
market_timings.json Fields
| Field | Required | Default | Description |
|---|---|---|---|
timezone |
Yes | — | IANA timezone (e.g. Asia/Kolkata, America/New_York) |
trading_days_in_week |
No | 5 |
Number of trading days per week |
weekend_days |
No | ["Saturday", "Sunday"] |
List of weekday names that are non-trading days |
trading_minutes |
No | 0 |
Total active trading minutes per session (excl. breaks) |
sessions |
Yes | [] |
List of named sessions with own open/close/breaks |
Session fields:
| Field | Required | Default | Description |
|---|---|---|---|
name |
Yes | - | Session name (e.g., "premarket", "regular") |
open |
Yes | - | Session open time in HH:MM format |
close |
Yes | - | Session close time in HH:MM format |
breaks |
No | null |
null = no breaks, [] = explicit no breaks, [{"start":"HH:MM","end":"HH:MM"}] = session breaks |
Key Features
- Pure Functions Only — No OOP, no class state; all logic in module-level functions
- Lazy Config Loading — No side effects on import; validated on first use
- Cached Holiday/Timing Data — Loaded once with thread-safe double-checked locking
- Named Sessions — Multiple named market phases (premarket, regular, afterhours, etc.)
- Configurable Market Window —
minutes_before_open/minutes_after_closebuffers per call - Session Filters — Check specific sessions by name with optional
sessionsparameter - Per-Session Breaks — Breaks defined per session only (no global fallback)
- Midnight-Crossing Sessions — Supports overnight sessions (close < open)
- stdlib zoneinfo — Uses Python 3.9+
zoneinfoexclusively; no pytz dependency - Retry with Exponential Backoff + Jitter — Config file reads retry 3 times
- Settlement Date Calculations — T+0, T+1, T+2, etc.
API Reference
is_trading_day(check_date: date) -> bool
Check if a specific date is a trading day (not a weekend or holiday).
- Returns:
Trueif trading day,Falseotherwise
is_today_trading_day() -> bool
Check if today (UTC) is a trading day.
- Returns:
Trueif today is a trading day,Falseotherwise
was_date_trading_day(check_date: date) -> bool
Historical date check — same logic as is_trading_day with clearer intent for past dates.
- Returns:
Trueif it was a trading day,Falseotherwise
find_next_trading_date(from_date: date, inclusive: bool = False) -> date
Find the next valid trading date from a given date.
inclusive: IfTrue, returnsfrom_dateitself if already a trading day- Returns: The next trading date
get_date_n_trading_days_later(from_date: date, trading_days: int) -> date
Calculate the date N trading days from a given date (T+1, T+2 settlement).
trading_days: 0 returnsfrom_dateunchanged- Returns: The resulting date
is_market_open() -> bool
Check if the market is currently open (no buffer).
- Returns:
Trueif within market hours on a trading day,Falseotherwise
processing_market_window(minutes_before_open: int = 0, minutes_after_close: int = 0, sessions: list[str] | None = None) -> tuple[bool, str]
Check if current time falls within the market window, with optional buffers.
sessions: Optional list of session names to check (None = all configured sessions)- Returns:
(in_window: bool, phase: str)— phase is session name (e.g."premarket","regular") or"Market closed"/"Not a trading day"
HolidayConfigError
Raised when the holidays JSON is missing, malformed, or has an invalid date entry.
MarketTimingError
Raised when market_timings.json is missing, malformed, or contains an invalid time string.
Retry Policy
Config file reads use @with_retry decorator:
- Max attempts: 3
- Delay: Exponential backoff (1s, 2s, 4s) with ±25% jitter
- Retryable: All exceptions during file read
- Not retried:
FileNotFoundErrorandjson.JSONDecodeError(raised immediately as config errors)
Error Handling
The library will raise:
ValueErrorifPROJECT_DIRECTORYis not set (on first use, not at import)HolidayConfigErrorif the holidays JSON file is missing or malformedMarketTimingErrorif the market timings JSON file is missing, malformed, or has missing fieldsValueErrorifminutes_before_openorminutes_after_closeare negative
Test Coverage
python3 -m pytest TRADING_HOURS.py -v
| Function | Tier | Tests | What is tested |
|---|---|---|---|
is_trading_day() |
1 | 3 | Friday true, Saturday false, Sunday false |
find_next_trading_date() |
1 | 3 | Friday→Monday, inclusive flag, Saturday→Monday |
get_date_n_trading_days_later() |
1 | 4 | T+0 same, T+1 Friday, T+2 Friday, T+1 Saturday |
parse_trading_holidays() |
2 | 4 | valid entry, malformed skipped, weekend holiday, UserWarning |
with_retry() |
2 | 4 | first-attempt success, third-attempt success, exhaustion, all exception types |
_parse_time_str() |
2 | 3 | valid HH:MM, invalid format, midnight |
is_today_trading_day() |
1 | 1 | returns bool |
was_date_trading_day() |
1 | 2 | weekend false, weekday true |
processing_market_window() |
1 | 4 | returns tuple, debug info, buffer expansion, negative raises |
is_market_open() |
1 | 1 | returns bool |
fetch_trading_holidays() |
2 | 3 | returns set, missing file, invalid JSON |
fetch_market_timings() |
2 | 3 | returns timings, missing file, missing field |
_get_holidays() |
2 | 2 | returns cached, lazy loads |
_get_timings() |
2 | 2 | returns cached, lazy loads |
| Import validation | 3 | 1 | missing PROJECT_DIRECTORY raises |
Reloading Cached Data
Holiday and timing data are cached after the first load. To force a reload:
import TRADING_HOURS
TRADING_HOURS._HOLIDAY_CACHE = None
TRADING_HOURS._TIMINGS_CACHE = None
Release files for MARKET-TRADING-HOURS 0.0.3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| market_trading_hours-0.0.3.tar.gz | 25.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| market_trading_hours-0.0.3-py3-none-any.whl | Python 3 | none | any | Details |
Total release size:51.4 kB
Release files / market_trading_hours-0.0.3.tar.gz
| Download URL | market_trading_hours-0.0.3.tar.gz |
|---|---|
| Size | 25.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
717d382e649cb825ce5953baeb001e46b6cc086bb23479980c2bebc2820c41ac
|
|
BLAKE2b-256 checksum How to use checksums |
3dc4c3130516eded539de99abd5d957206bb08daf6be72fd3f4e9fe39b7cd8b1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|
Release files / market_trading_hours-0.0.3-py3-none-any.whl
| Download URL | market_trading_hours-0.0.3-py3-none-any.whl |
|---|---|
| Size | 25.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a0787dabe41796fed8b7168cab19bc269902605608a1787ab4883c9e37d5ef64
|
|
BLAKE2b-256 checksum How to use checksums |
0e0beb9a1d92c1d1d005bb365c1c6a50b379f4cf6c075d92f42dbf8a4166e17c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|