Headless FastAPI-Backend (server.py + core/engine.py) mit Mobile-PWA (web/), Strategie-/Backtest-Suite und Doku. Secrets, DB, Logs und Laufzeit-State sind via .gitignore ausgeschlossen; Config-Vorlage: oil_widget_config.ini.example. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
78 lines
2.5 KiB
Python
78 lines
2.5 KiB
Python
"""
|
||
core/market_hours.py — Börsen-Session-Status (Frankfurt / US)
|
||
==============================================================
|
||
Liefert den aktuellen Session-Status für die Empfehlungs-Logik, das UI und
|
||
den KI-Agenten. Zeiten in **Berlin-Lokalzeit** (DST-sicher via zoneinfo):
|
||
|
||
DE (Frankfurt): 09:00 – 17:30
|
||
US (Wall St.): 15:00 – 22:00 (Overlap 15:00–17:30 = höchste Liquidität)
|
||
|
||
Nutzen:
|
||
• "just_opened": die ersten _CAUTION_MIN nach einem Open (Whipsaw-Vorsicht)
|
||
• "active": welche Sessions gerade offen sind (Liquiditäts-Bonus)
|
||
• "next_open": nächster Open + Minuten bis dahin (UI-Countdown)
|
||
"""
|
||
from __future__ import annotations
|
||
from datetime import datetime, time, timezone
|
||
|
||
try:
|
||
from zoneinfo import ZoneInfo
|
||
_BERLIN = ZoneInfo("Europe/Berlin")
|
||
except Exception:
|
||
_BERLIN = None
|
||
|
||
# Sessions in Berlin-Lokalzeit (Start, Ende)
|
||
_SESSIONS = {
|
||
"DE": (time(9, 0), time(17, 30)),
|
||
"US": (time(15, 0), time(22, 0)),
|
||
}
|
||
_CAUTION_MIN = 20 # erste N Min nach einem Open = volatil → Vorsicht
|
||
|
||
|
||
def _mins(t: time) -> int:
|
||
return t.hour * 60 + t.minute
|
||
|
||
|
||
def session_state(now: datetime | None = None) -> dict:
|
||
now = now or datetime.now(timezone.utc)
|
||
loc = now.astimezone(_BERLIN) if _BERLIN else now
|
||
is_weekend = loc.weekday() >= 5 # 5=Sa, 6=So
|
||
nowm = loc.hour * 60 + loc.minute
|
||
|
||
active, since, just_opened, next_open = [], {}, None, None
|
||
for name, (o, c) in _SESSIONS.items():
|
||
om, cm = _mins(o), _mins(c)
|
||
open_now = (not is_weekend) and (om <= nowm <= cm)
|
||
if open_now:
|
||
active.append(name)
|
||
if (not is_weekend) and nowm >= om:
|
||
since[name] = nowm - om
|
||
if open_now and (nowm - om) < _CAUTION_MIN:
|
||
just_opened = name
|
||
else:
|
||
since[name] = None
|
||
if (not is_weekend) and nowm < om:
|
||
d = om - nowm
|
||
if next_open is None or d < next_open["in_min"]:
|
||
next_open = {"name": name, "in_min": d}
|
||
|
||
if is_weekend:
|
||
phase = "Wochenende"
|
||
elif just_opened:
|
||
phase = f"{just_opened}-Open frisch (volatil)"
|
||
elif active:
|
||
phase = " + ".join(active) + "-Session offen"
|
||
elif next_open:
|
||
phase = f"vor {next_open['name']}-Open"
|
||
else:
|
||
phase = "außerhalb DE/US"
|
||
|
||
return {
|
||
"active": active,
|
||
"since_open_min": since,
|
||
"just_opened": just_opened,
|
||
"next_open": next_open,
|
||
"phase": phase,
|
||
"weekend": is_weekend,
|
||
}
|