vike
ConceptsInstruments

Sessions and market hours

The one market-hours law: week-minute session calendars keyed by venue and asset class, declared as DST unions that can only ever under-block.

Before SessionCalendar existed, nothing in the workspace could answer "is this venue open at ts?". crates/vike-model/src/session.rs records what that cost: the FX week existed as a prose comment in one venue's live-smoke test, live execution discovered a closed market only as a venue rejection, and a backtest discovered it only as a data gap — two discovery mechanisms for one fact.

The shape is policy as data plus one pure query: a declarative table, and SessionCalendar::is_open. There is deliberately no second session codepath — a consumer either calls that query or it is not consulting the law.

Week-minutes, no timezone database

A calendar is a set of half-open SessionSegments over the trading week, measured in minutes since Monday 00:00 UTC (0 .. 10_080). Timestamps are epoch-ms UTC, and the only calendar arithmetic is the day division vike-model already owns — no chrono, no tz database, no hand-rolled civil math. Resolution is one minute. Segments never wrap, so a session spanning Sunday into Monday is declared as two segments, which keeps every query an ordered scan with no modular interval arithmetic.

Three rows cover the roster:

  • CRYPTO_24_7 — binance, bybit, okx, deribit, aster, hyperliquid, polymarket. No weekly close at all; the only interruptions are unscheduled maintenance windows, which a static calendar cannot predict and which surface as rejections or feed gaps anyway.
  • FX_WEEK — dukascopy, oanda, ig, fxcm, ctrader. The boundary tracks the New York 17:00 close, which is 21:00 UTC under EDT and 22:00 UTC under EST, so the row opens at the earlier (Sunday 21:00 UTC) and closes at the later (Friday 22:00 UTC). The weekend gap it blocks is Friday 22:00 → Sunday 21:00 UTC.
  • US_EQUITY_REGULAR — 09:30–16:00 America/New_York, Monday through Friday, declared in UTC as 13:30–21:00, the union of the EDT and EST windows.

The DST rule: under-block, never over-block

Where a real boundary moves with daylight saving, the declared segment is the union of both offsets. The module states the consequence once, for the whole table: "The gate may UNDER-block by up to one hour. It can never OVER-block." Under-blocking leaves an hour of genuinely closed time looking open — the pre-existing behaviour, since nothing blocked at all before. Over-blocking would silently delete fills that really happened in-session and quietly corrupt a backtest.

Every resolver here is fail-permissive: an unpinned session answers SessionCalendar::ALWAYS_OPEN, the deliberate opposite of the capability tables' fail-closed default. The module argues why the two differ — a capability we cannot confirm must not be offered, but a session we have no data for must not silently block a venue that in reality trades fine.

Keyed by venue and asset class

The correct key is (venue, asset class), never the venue alone. Alpaca trades cash equities and 24/7 crypto on one venue and IBKR is a global multi-asset book, so "what venue is this?" cannot answer "when is it open?".

AssetClass is a vike-catalog concept and vike-catalog depends on vike-model down-only, so the map cannot live beside the rows without inverting the layering. It lives one layer up instead: crates/vike-catalog/src/session.rs's session_calendar_for is the one keying site, and vike-model takes the resolved calendar as a caller-supplied parameter. Continuous classes (CryptoSpot, CryptoPerp, CryptoFuture, PredictionMarket) resolve 24/7 regardless of venue; Fx and Cfd resolve the FX week; Equity and Etf resolve the US regular session for alpaca and always-open for anything else. Listed options, futures and index products resolve 24/7 on the crypto-native venues that is_crypto_native names — deribit, binance, bybit, okx, aster, hyperliquid — and always-open elsewhere. So (alpaca, CryptoSpot) and (alpaca, Equity) return different calendars from the same venue, and an IBKR equity, whose region is not inferable from that pair, is left open rather than blocked wrongly.

session_for(venue) remains as the venue-only convenience default, and answers ALWAYS_OPEN for the mixed-asset venues on purpose. venue_is_open composes the two for a "venue closed" badge and is explicitly read-only: nothing in the live order path consults it today. next_open and next_close answer "from when may I trade?", returning the given instant verbatim when it already matches.

The backtest gate

EngineParams::session_gate is opt-in and defaults to false, which leaves every fill lane byte-identical. Turned on, each symbol resolves to its own calendar and nothing on that symbol fills on an event outside its session — resting orders, the tagged maker lane and protective stops alike, on the frozen fill lanes and their opt-in queue-model tick twins. Resolution order is the per-symbol session_calendars override, then session_for(default_venue), then always-open. That means the (venue, asset class) keying lives in the caller: build the map from session_calendar_for per symbol. Enabling the knob with neither a default venue nor an override is a silent no-op, not an error — a run whose session_deferrals stays 0 is how you see it. See the sim broker.

Deliberately not built

Holiday calendars and half-days are not modelled: every row is a plain recurring week, so a US holiday reads as open. Non-US regional equity sessions and listed-derivative extended hours are follow-ups, resolved fail-permissively until their rows land. SessionState has only Open and Closed — no pre- or post-market variant, because no shipped row carries extended-hours data to distinguish them with, and adding the variant first would be a field that is never set.

On this page