from datetime import datetime from zoneinfo import ZoneInfo import pytest from app.bars.models import Timeframe from app.bars.session import ( FUTURE_SLOT_COUNT, bucket_duration, bucket_start, future_bucket_starts, next_bucket_start, ) UTC = ZoneInfo("UTC") ET = ZoneInfo("America/New_York") def epoch(value: str, zone=UTC) -> int: return int(datetime.fromisoformat(value).replace(tzinfo=zone).timestamp()) @pytest.mark.parametrize( ("value", "tf", "expected"), [ ("2026-08-09T22:00:00", Timeframe.D1, "2026-08-09T22:00:00"), ("2026-08-10T16:37:00", Timeframe.D1, "2026-08-09T22:00:00"), ("2026-08-14T20:59:00", Timeframe.D1, "2026-08-13T22:00:00"), ("2026-08-10T21:30:00", Timeframe.D1, "2026-08-09T22:00:00"), ("2026-08-10T22:00:00", Timeframe.D1, "2026-08-10T22:00:00"), ], ) def test_session_boundaries(value, tf, expected): assert bucket_start(epoch(value), tf) == epoch(expected) def test_intraday_buckets_use_utc_boundaries(): assert bucket_start(epoch("2026-08-10T12:37:45"), Timeframe.M15) == epoch( "2026-08-10T12:30:00" ) @pytest.mark.parametrize( ("local_value", "expected_local"), [ ("2026-03-08T18:00:00", "2026-03-08T18:00:00"), ("2026-11-01T18:00:00", "2026-11-01T18:00:00"), ], ) def test_sunday_open_across_dst(local_value, expected_local): assert bucket_start(epoch(local_value, ET), Timeframe.D1) == epoch(expected_local, ET) @pytest.mark.parametrize( ("value_utc", "expected_open_utc"), [ ("2026-01-13T04:00:00", "2026-01-12T23:00:00"), ("2026-07-14T03:00:00", "2026-07-13T22:00:00"), ("2026-11-01T22:59:00", "2026-10-31T22:00:00"), ("2026-11-01T23:00:00", "2026-11-01T23:00:00"), ], ) def test_daily_open_tracks_eastern_dst_in_utc(value_utc, expected_open_utc): assert bucket_start(epoch(value_utc), Timeframe.D1) == epoch(expected_open_utc) def test_daily_geometry_excludes_the_settlement_halt(): start = epoch("2026-08-10T18:00:00", ET) assert bucket_duration(start, Timeframe.D1) == 23 * 3600 def test_daily_future_geometry_skips_the_weekend_and_opens_at_1800_eastern(): thursday = epoch("2026-08-13T18:00:00", ET) sunday = epoch("2026-08-16T18:00:00", ET) assert next_bucket_start(thursday, Timeframe.D1) == sunday def test_intraday_future_geometry_skips_the_settlement_halt(): monday_last = epoch("2026-08-10T16:55:00", ET) monday_reopen = epoch("2026-08-10T18:00:00", ET) assert next_bucket_start(monday_last, Timeframe.M5) == monday_reopen def test_intraday_future_geometry_skips_the_weekend(): friday_last = epoch("2026-08-14T16:55:00", ET) sunday_reopen = epoch("2026-08-16T18:00:00", ET) assert next_bucket_start(friday_last, Timeframe.M5) == sunday_reopen def test_future_geometry_uses_the_shared_180_slot_horizon(): start = epoch("2026-08-13T18:00:00", ET) future = future_bucket_starts(start, Timeframe.D1) assert len(future) == FUTURE_SLOT_COUNT == 180 assert all(datetime.fromtimestamp(value, ET).weekday() in {6, 0, 1, 2, 3} for value in future)