SPB Git

Repository Public Research preview

sessions.py 5.0 KB 99 lines python

1"""Session conventions per asset class (decision D-02 in docs/methodology_decisions.md).23All vendor timestamps are bar OPEN in UTC. We define, per asset class, the *session window* over which the daily4close-to-close return and the intraday realized measures are computed:56| class   | session id         | window (local)                       | overnight segment          | calendar            |7|---------|--------------------|--------------------------------------|----------------------------|---------------------|8| etf     | XNYS trading day   | 09:30–16:00 ET (closing print incl.) | prev close → today's open  | exchange_calendars  |9| stock   | idem               | idem                                 | idem                       | XNYS (early closes) |10| futures | CME trading day D  | D-1 18:00 ET → D 17:00 ET            | none (17:00→18:00 break)   | derived from bars   |11| fx      | FX day D (NY)      | D-1 17:00 ET → D 17:00 ET            | none                       | derived from bars   |12| crypto  | UTC calendar day   | 00:00–24:00 UTC                      | none                       | 7 days a week       |1314For futures and FX the session date is the *end* date of the window (the day containing the 17:00 ET close), which15is the CME/NY convention. A futures/FX session is kept only if it has at least MIN_FRACTION of the modal number of16bars for that instrument (drops Sunday evening stubs and holiday half-sessions); equity sessions follow the XNYS17calendar with the official early-close times.18"""19from __future__ import annotations2021from dataclasses import dataclass22from datetime import time23from functools import lru_cache2425import numpy as np26import pandas as pd2728ET = "America/New_York"29MIN_FRACTION = 0.60303132@dataclass(frozen=True)33class SessionSpec:34    asset_class: str35    tz: str36    start: time | None      # local wall-clock start (None = anchored at previous day's end)37    end: time               # local wall-clock end (exclusive for intraday bars except the closing print rule)38    anchor_prev_day: bool   # window starts on the previous calendar day39    has_overnight: bool40    seven_days: bool414243ASSET_SESSION = {44    "etf": SessionSpec("etf", ET, time(9, 30), time(16, 0), False, True, False),45    "stock": SessionSpec("stock", ET, time(9, 30), time(16, 0), False, True, False),46    "futures": SessionSpec("futures", ET, time(18, 0), time(17, 0), True, False, False),47    "fx": SessionSpec("fx", ET, time(17, 0), time(17, 0), True, False, False),48    "crypto": SessionSpec("crypto", "UTC", time(0, 0), time(0, 0), False, False, True),49}505152@lru_cache(maxsize=4)53def _xnys():54    import exchange_calendars as xc55    return xc.get_calendar("XNYS", start="1999-01-01")565758def xnys_sessions(start: str, end: str) -> pd.DatetimeIndex:59    return _xnys().sessions_in_range(pd.Timestamp(start), pd.Timestamp(end))606162def xnys_early_closes(start: str, end: str) -> dict[str, time]:63    """Map 'YYYY-MM-DD' -> local close time for early-close sessions (13:00 ET) in [start, end]."""64    cal = _xnys()65    s = cal.sessions_in_range(pd.Timestamp(start), pd.Timestamp(end))66    closes = cal.closes.reindex(s).dt.tz_convert(ET)67    return {d.strftime("%Y-%m-%d"): c.time() for d, c in zip(s, closes) if c.time() != time(16, 0)}686970def assign_sessions(ts_utc: pd.Series, asset_class: str) -> pd.DataFrame:71    """Return a frame with columns `session` (date), `local` (tz-aware local timestamp), `in_window` (bool) for72    each bar-open timestamp. Equities: in_window = 09:30 <= local < close (close = 16:00 or early close), plus the73    closing print bar whose open time equals the close time. Futures/FX: window anchored at the previous day74    18:00/17:00 ET; crypto: UTC day."""75    spec = ASSET_SESSION[asset_class]76    t = pd.to_datetime(ts_utc, utc=True)77    local = t.dt.tz_convert(spec.tz)78    if asset_class in ("etf", "stock"):79        d = local.dt.normalize()80        day = local.dt.strftime("%Y-%m-%d")81        ec = xnys_early_closes(day.min(), day.max()) if len(day) else {}82        close_t = day.map(lambda s: ec.get(s, time(16, 0)))83        lt = local.dt.time84        in_win = (lt >= spec.start) & (lt <= close_t)          # includes the closing print (open == close time)85        return pd.DataFrame({"session": d.dt.tz_localize(None), "local": local, "in_window": in_win.values})86    if asset_class == "crypto":87        d = local.dt.normalize()88        return pd.DataFrame({"session": d.dt.tz_localize(None), "local": local, "in_window": np.ones(len(t), bool)})89    # futures / fx: shift the clock so that the window end (17:00 ET) maps to midnight, then take the date90    shift = pd.Timedelta(hours=24 - spec.end.hour)91    shifted = (local + shift)92    sess = shifted.dt.normalize().dt.tz_localize(None)93    if asset_class == "futures":   # bars between 17:00 and 18:00 ET belong to no session (maintenance break)94        lt = local.dt.time95        in_win = ~((lt >= time(17, 0)) & (lt < time(18, 0)))96    else:97        in_win = np.ones(len(t), bool)98    return pd.DataFrame({"session": sess, "local": local, "in_window": np.asarray(in_win)})99
Files main
Menu

Theme