Market calendars

NYSE sessions, 24/7 crypto, event-resolution windows. Gate order submission on what the venue will accept.

Backtests do not care what time it is. The loop advances bar by bar and positions change whenever data arrives. Live trading cares. An order sent into a closed NYSE session dangles until open. A halted LULD window gets orders rejected. A prediction market after resolution is an error.

horizon.calendars (plural, to avoid Python’s stdlib calendar) provides one Protocol and three implementations. The live loop calls calendar.is_open(ts) before every submission. See Watchdog for the integration.

Protocol

python
class ExchangeCalendar(Protocol):
    name: str
    def is_open(self, ts: datetime) -> bool: ...
    def session_kind(self, ts: datetime) -> SessionKind: ...
    def next_open(self, ts: datetime) -> datetime | None: ...
    def next_close(self, ts: datetime) -> datetime | None: ...
    def previous_close(self, ts: datetime) -> datetime | None: ...
    def current_session(self, ts: datetime) -> MarketSession | None: ...

SessionKind: Closed | PreMarket | Regular | PostMarket | Auction | Halted.

NYSECalendar

US equities, ETFs, equity options.

python
from horizon.calendars import NYSECalendar, SessionKind
from datetime import datetime
from zoneinfo import ZoneInfo

nyse = NYSECalendar()
nyse_ext = NYSECalendar(include_extended_hours=True)   # 4am-9:30am + 4pm-8pm ET

et = ZoneInfo("America/New_York")
ts = datetime(2026, 3, 2, 10, 30, tzinfo=et)   # Monday 10:30 ET
nyse.is_open(ts)                                # True
nyse.session_kind(ts)                           # SessionKind.Regular
nyse.current_session(ts).is_early_close          # False

Holidays and half-days

With pandas_market_calendars (in the optional equity extra) NYSECalendar uses accurate US holiday detection and half-day (13:00 ET close) identification:

bash
pip install pandas-market-calendars
# or
pip install "horizon[equity]"

Without it, the calendar falls back to a Mon-Fri 9:30-16:00 ET rule. Fine for dev, not production.

Next / previous

python
ts = datetime(2026, 3, 6, 18, 0, tzinfo=et)   # Friday evening
next_open  = nyse.next_open(ts)                # Monday 9:30 ET
next_close = nyse.next_close(ts)               # Monday 16:00 ET (or 13:00 early close)
prev_close = nyse.previous_close(ts)           # Friday 16:00 ET

AlwaysOpenCalendar

Crypto and perps.

python
from horizon.calendars import AlwaysOpenCalendar

crypto = AlwaysOpenCalendar()
crypto.is_open(any_timestamp)                   # True
crypto.session_kind(ts)                         # SessionKind.Regular
crypto.next_open(ts)                            # returns ts
crypto.next_close(ts)                           # None

Use for Hyperliquid, Binance, Coinbase, Kraken, most perps venues.

ResolutionCalendar

Polymarket and Kalshi events trade until the question resolves, then settle to $1 or $0 per Yes-share.

python
from horizon.calendars import ResolutionCalendar
from datetime import datetime, timezone

elections = ResolutionCalendar()
elections.register(
    "will_candidate_x_win_2028",
    resolution_ts=datetime(2028, 11, 8, 23, 59, tzinfo=timezone.utc),
)

# Before resolution
elections.is_open(datetime(2026, 10, 1, tzinfo=timezone.utc),
                  market_id="will_candidate_x_win_2028")   # True

# After resolution
elections.is_open(datetime(2028, 11, 9, tzinfo=timezone.utc),
                  market_id="will_candidate_x_win_2028")   # False

The L1 lifecycle manager listens for resolution_ts and emits AuditCategory.PredictionResolved events that credit winning positions.

Per-venue wiring

python
calendars = {
    "alpaca":      NYSECalendar(include_extended_hours=False),
    "ibkr":        NYSECalendar(include_extended_hours=True),
    "hyperliquid": AlwaysOpenCalendar(),
    "polymarket":  ResolutionCalendar(),
    "kalshi":      ResolutionCalendar(),
}

Each Venue picks up its calendar at construction. The execution loop consults the calendar on the tick timestamp, not wall clock.

Watchdog integration

Attach a calendar to LiveWatchdog. The watchdog then:

  • Halts new submissions outside the session (halt_outside_session=True).
  • Fires a one-shot flatten N seconds before the scheduled close (flatten_before_close_seconds > 0).
  • Respects half-days and holidays (via pandas_market_calendars if installed).
python
from horizon.calendars import NYSECalendar
from horizon.ops import LiveWatchdogConfig

hz.run(
    mode="live",
    feed=my_feed,
    watchdog=LiveWatchdogConfig(
        session_calendar=NYSECalendar(),
        halt_outside_session=True,
        flatten_before_close_seconds=120,
    ),
    ...,
)

Status

L0L1+
Protocol, NYSE, AlwaysOpen, ResolutionShipped
pandas_market_calendars integrationShipped (opt-in)
Half-day (13:00 close) detectionShipped
Gate order submission on is_openShipped via watchdog
Scheduled flatten before closeShipped via watchdog
Option / future expiry scheduling via calendarPlanned
LULD / circuit-breaker halt detectionFrom feed
Non-US venues (LSE, TSE)Add per-venue impls