Skip to content

core.sessions

Asia, London and New York — the regional windows that scope indicator data and tradable hours. A different axis from the Topstep day above.

sessions

Regional trading sessions (Asia / London / New York).

This is a different axis from core/time.py, and confusing the two is the easiest bug to write here:

  • core/time.py models the Topstep trading day — one 23-hour block that opens 18:00 ET the previous calendar day and resets at 18:00 ET. It answers "which day does this bar's P&L land on" and drives the DLL, the MLL ratchet and the 16:10 flatten.
  • This module answers "which regional session was trading when this bar printed". A session says nothing about which trading day the bar belongs to, and a trading day spans all three sessions.

The interaction that surprises people: the Asia session sits at roughly 19:00-01:00 ET, i.e. after the 18:00 ET rollover, so Asia bars belong to the next trading day. Both classifications are needed; neither substitutes for the other.

SESSIONS ARE DEFINED IN THEIR OWN LOCAL TIMEZONE, NOT AS FIXED ET OFFSETS.

That is deliberate and it is the whole reason this module resolves timezones through zoneinfo instead of shipping a table of ET windows:

  • Daylight-saving correctness comes from the IANA database, so there is no hand-maintained table to rot. This repository already dropped its exchange calendar for exactly that failure mode (docs/ROADMAP.md), and a fixed-ET session table is the same mistake in a smaller package: London and New York shift on different dates — roughly two weeks in March and one in late October — across which "London = 03:00 ET" is silently an hour wrong.
  • In its own timezone no real session wraps midnight, which keeps the common path a single comparison.

None of this costs flexibility. Session is a plain value type, so a fixed ET block is one line away and behaves identically::

LONDON_ET = Session("LONDON_ET", ET, time(3), time(11))

Midnight-wrapping windows (start > end) are supported for exactly that case — an Asia session expressed in ET necessarily wraps.

The three shipped constants are reference definitions of the cash sessions each region is named for. They are a starting point, not an authority: if your venue or your strategy means something else by "the London session", define your own Session rather than bending these.

ASIA module-attribute

ASIA = Session('ASIA', ZoneInfo('Asia/Tokyo'), time(9, 0), time(15, 0))

LONDON module-attribute

LONDON = Session('LONDON', ZoneInfo('Europe/London'), time(8, 0), time(16, 30))

NEW_YORK module-attribute

NEW_YORK = Session('NEW_YORK', ET, time(9, 30), time(16, 0))

SESSIONS module-attribute

SESSIONS: dict[str, Session] = {session.name: session for session in (ASIA, LONDON, NEW_YORK)}

Session

Bases: Struct

A named intraday window, defined in its own local timezone.

The window is half-open — [start, end) — so a bar opening exactly at end belongs to the next window, and two adjacent sessions sharing a boundary never both claim the same bar.

start > end denotes a window that wraps midnight (an Asia session expressed in ET, say); membership is then "at/after start OR before end". start == end is rejected: it reads equally as an empty window and a 24-hour one.

name instance-attribute

name: str

tz instance-attribute

tz: ZoneInfo

start instance-attribute

start: time

end instance-attribute

end: time

wraps_midnight property

wraps_midnight: bool

True when the window runs past local midnight (start > end).

contains_ns

contains_ns(ns: int) -> bool

Is UTC-nanosecond instant ns inside this session's window?

The instant is converted to the session's OWN timezone before the comparison, so the answer follows that region's daylight-saving schedule rather than ET's.

Source code in src/topstep_backtest/core/sessions.py
def contains_ns(self, ns: int) -> bool:
    """Is UTC-nanosecond instant ``ns`` inside this session's window?

    The instant is converted to the session's OWN timezone before the
    comparison, so the answer follows that region's daylight-saving
    schedule rather than ET's.
    """
    local = ns_to_dt(ns).astimezone(self.tz).time()
    if self.wraps_midnight:
        return local >= self.start or local < self.end
    return self.start <= local < self.end

contains

contains(bar: Bar) -> bool

Does bar belong to this session?

Tested on ts_event — the bar's OPEN — not ts_init. A bar's session is a property of the window it covers, and ts_init is the close: testing it would pull the 09:29->09:30 bar, whose every print is pre-market, into the New York session. Both stamps are already in the past when a decision is made, so this introduces no look-ahead either way; it is purely about classifying the bar correctly.

Source code in src/topstep_backtest/core/sessions.py
def contains(self, bar: Bar) -> bool:
    """Does ``bar`` belong to this session?

    Tested on ``ts_event`` — the bar's OPEN — not ``ts_init``. A bar's
    session is a property of the window it covers, and ``ts_init`` is the
    close: testing it would pull the 09:29->09:30 bar, whose every print is
    pre-market, into the New York session. Both stamps are already in the
    past when a decision is made, so this introduces no look-ahead either
    way; it is purely about classifying the bar correctly.
    """
    return self.contains_ns(bar.ts_event)