Repository Public Research preview
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