39 lines
1.5 KiB
Python
39 lines
1.5 KiB
Python
from datetime import datetime, time, timedelta
|
|
from zoneinfo import ZoneInfo
|
|
|
|
from app.bars.models import Timeframe
|
|
|
|
UTC = ZoneInfo("UTC")
|
|
EASTERN = ZoneInfo("America/New_York")
|
|
SESSION_OPEN = time(18, 0)
|
|
SESSION_CLOSE = time(17, 0)
|
|
|
|
|
|
def _session_open_local(current: datetime) -> datetime:
|
|
session_date = current.date() if current.timetz().replace(tzinfo=None) >= SESSION_OPEN else current.date() - timedelta(days=1)
|
|
return datetime.combine(session_date, SESSION_OPEN, EASTERN)
|
|
|
|
|
|
def bucket_start(t: int, tf: Timeframe) -> int:
|
|
# Everything below a day divides the hour evenly, so UTC boundaries are
|
|
# correct and session anchoring is unnecessary. Only the daily bar needs to
|
|
# know that the CME trading day runs 18:00 to 17:00 ET.
|
|
if tf is not Timeframe.D1:
|
|
return (t // tf.seconds) * tf.seconds
|
|
|
|
current = datetime.fromtimestamp(t, UTC).astimezone(EASTERN)
|
|
return int(_session_open_local(current).timestamp())
|
|
|
|
|
|
def bucket_duration(t: int, tf: Timeframe) -> int:
|
|
"""Wall-clock span of one logical bar bucket.
|
|
|
|
Intraday buckets are fixed. A daily line advances only through the active
|
|
18:00-17:00 ET session, not the settlement halt; constructing the close in
|
|
Eastern keeps DST transitions correct without shifting stored UTC times.
|
|
"""
|
|
if tf is not Timeframe.D1:
|
|
return tf.seconds
|
|
current = datetime.fromtimestamp(t, UTC).astimezone(EASTERN)
|
|
next_close = datetime.combine(current.date() + timedelta(days=1), SESSION_CLOSE, EASTERN)
|
|
return int(next_close.timestamp()) - t
|