30 lines
1.2 KiB
Python
30 lines
1.2 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)
|
|
|
|
|
|
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:
|
|
if tf not in (Timeframe.H4, Timeframe.D1):
|
|
return (t // tf.seconds) * tf.seconds
|
|
|
|
current = datetime.fromtimestamp(t, UTC).astimezone(EASTERN)
|
|
session_open = _session_open_local(current)
|
|
if tf is Timeframe.D1:
|
|
return int(session_open.timestamp())
|
|
|
|
# CME's 4h anchors are wall-clock ET anchors. This intentionally makes the
|
|
# DST-transition bucket three or five elapsed hours instead of shifting it.
|
|
elapsed_wall = current.replace(tzinfo=None) - session_open.replace(tzinfo=None)
|
|
bucket_hours = int(elapsed_wall.total_seconds() // 14400) * 4
|
|
local_start = session_open.replace(tzinfo=None) + timedelta(hours=bucket_hours)
|
|
return int(local_start.replace(tzinfo=EASTERN).timestamp())
|