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
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.
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:
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
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.
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.
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
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_calendarsif installed).
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
| L0 | L1+ | |
|---|---|---|
| Protocol, NYSE, AlwaysOpen, Resolution | Shipped | |
pandas_market_calendars integration | Shipped (opt-in) | |
| Half-day (13:00 close) detection | Shipped | |
Gate order submission on is_open | Shipped via watchdog | |
| Scheduled flatten before close | Shipped via watchdog | |
| Option / future expiry scheduling via calendar | Planned | |
| LULD / circuit-breaker halt detection | From feed | |
| Non-US venues (LSE, TSE) | Add per-venue impls |