Metadata-Version: 2.4
Name: MARKET-TRADING-HOURS
Version: 1.2.4
Summary: Async queue-based structured JSONL logging with thread-safe performance and auto-detected module names
Author-email: rocky <rocky@null.net>
License: MIT
Project-URL: Homepage, https://github.com/rocky/TRADING_HOURS
Project-URL: Bug Reports, https://github.com/rocky/TRADING_HOURS/issues
Project-URL: Source, https://github.com/rocky/TRADING_HOURS
Keywords: logging,jsonl,structured-logging,async-logging,audit-logging
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: System :: Logging
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: python-dotenv>=1.2.2
Provides-Extra: dev
Requires-Dist: pytest>=9.0.2; extra == "dev"
Requires-Dist: pytest-cov>=7.1.0; extra == "dev"
Dynamic: license-file

# 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

```bash
pip install TRADING_HOURS
```

## Quick Start

### Set environment variables in .env

```bash
PROJECT_DIRECTORY=/path/to/your/project
```

### Config Files (required)

Place these two JSON files relative to `PROJECT_DIRECTORY`:

```
{PROJECT_DIRECTORY}/_CONFIG/trading_hours/trading_holidays.json
{PROJECT_DIRECTORY}/_CONFIG/trading_hours/market_timings.json
```

**trading_holidays.json** format:

```json
{
  "market_holidays": [
    {
      "year": 2026,
      "holidays": [
        {
          "date": "26-Feb-2026",
          "day": "Thursday",
          "description": "Mahashivratri"
        }
      ]
    }
  ]
}
```

**market_timings.json** format:

```json
{
  "market_timings": {
    "open": "09:15",
    "close": "15:30",
    "timezone": "Asia/Kolkata",
    "trading_days_in_week": 5
  }
}
```

### Import and use

```python
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, info = processing_market_window(minutes_before_open=15, minutes_after_close=30)
```

## 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/trading_hours/trading_holidays.json` |
| Market timings | `{PROJECT_DIRECTORY}/_CONFIG/trading_hours/market_timings.json` |

### market_timings.json Fields

| Field | Required | Default | Description |
|-------|----------|---------|-------------|
| `open` | Yes | — | Market open time in `HH:MM` format |
| `close` | Yes | — | Market close time in `HH:MM` format |
| `timezone` | Yes | — | IANA timezone (e.g. `Asia/Kolkata`, `America/New_York`) |
| `trading_days_in_week` | No | `5` | Number of trading days per week |

## Key Features

1. **Pure Functions Only** — No OOP, no class state; all logic in module-level functions
2. **Lazy Config Loading** — No side effects on import; validated on first use
3. **Cached Holiday/Timing Data** — Loaded once with thread-safe double-checked locking
4. **Configurable Market Window** — `minutes_before_open` / `minutes_after_close` parameters
5. **Midnight-Crossing Sessions** — Supports overnight sessions (close < open)
6. **stdlib zoneinfo** — Uses Python 3.9+ `zoneinfo` exclusively; no pytz dependency
7. **Retry with Exponential Backoff + Jitter** — Config file reads retry 3 times
8. **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: `True` if trading day, `False` otherwise

### is_today_trading_day() -> bool
Check if today (UTC) is a trading day.
- Returns: `True` if today is a trading day, `False` otherwise

### was_date_trading_day(check_date: date) -> bool
Historical date check — same logic as `is_trading_day` with clearer intent for past dates.
- Returns: `True` if it was a trading day, `False` otherwise

### find_next_trading_date(from_date: date, inclusive: bool = False) -> date
Find the next valid trading date from a given date.
- `inclusive`: If `True`, returns `from_date` itself 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 returns `from_date` unchanged
- Returns: The resulting date

### is_market_open() -> bool
Check if the market is currently open (no buffer).
- Returns: `True` if within market hours on a trading day, `False` otherwise

### processing_market_window(minutes_before_open: int = 0, minutes_after_close: int = 0) -> tuple[bool, str]
Check if current time falls within the market window, with optional buffers.
- Returns: `(in_window: bool, debug_info: str)` tuple

### 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**: `FileNotFoundError` and `json.JSONDecodeError` (raised immediately as config errors)

## Error Handling

The library will raise:
- `ValueError` if `PROJECT_DIRECTORY` is not set (on first use, not at import)
- `HolidayConfigError` if the holidays JSON file is missing or malformed
- `MarketTimingError` if the market timings JSON file is missing, malformed, or has missing fields
- `ValueError` if `minutes_before_open` or `minutes_after_close` are negative

## Test Coverage

```bash
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:

```python
import TRADING_HOURS
TRADING_HOURS._HOLIDAY_CACHE = None
TRADING_HOURS._TIMINGS_CACHE = None
```

## Version

Current: 1.0.0
