You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
96 lines
3.7 KiB
Python
96 lines
3.7 KiB
Python
"""
|
|
Unit tests for app.services.news.session_window.
|
|
|
|
Critical invariant: `post` must end at the next trading day's premarket
|
|
start (04:00 ET) — NOT next session's open (09:30 ET) — to avoid 4-hour
|
|
overlap with the next session's `premarket` window.
|
|
"""
|
|
from datetime import date, datetime, time
|
|
from zoneinfo import ZoneInfo
|
|
|
|
import pytest
|
|
|
|
from app.services.news.session_window import session_window
|
|
|
|
ET = ZoneInfo("America/New_York")
|
|
UTC = ZoneInfo("UTC")
|
|
|
|
|
|
def _et(d: date, t: time) -> datetime:
|
|
return datetime.combine(d, t, tzinfo=ET).astimezone(UTC)
|
|
|
|
|
|
def test_premarket_bounds_basic_weekday():
|
|
# Wednesday 2026-04-22 (Tue 2026-04-21 prev day, both weekdays)
|
|
target = date(2026, 4, 22)
|
|
start, end = session_window(target, "premarket")
|
|
# Premarket starts at the previous session's *close*, not 04:00
|
|
expected_start = _et(date(2026, 4, 21), time(16, 0))
|
|
expected_end = _et(target, time(9, 30))
|
|
assert start == expected_start
|
|
assert end == expected_end
|
|
|
|
|
|
def test_intraday_bounds_basic_weekday():
|
|
target = date(2026, 4, 22)
|
|
start, end = session_window(target, "intraday")
|
|
assert start == _et(target, time(9, 30))
|
|
assert end == _et(target, time(16, 0))
|
|
|
|
|
|
def test_post_ends_at_next_premarket_start_not_next_open():
|
|
"""post must NOT extend to next session's 09:30 — that would overlap T+1 premarket."""
|
|
target = date(2026, 4, 22) # Wed; next trading day Thu 2026-04-23
|
|
_, end = session_window(target, "post")
|
|
expected_end = _et(date(2026, 4, 23), time(4, 0))
|
|
assert end == expected_end, f"post should end at next premarket start (04:00 ET), got {end.astimezone(ET)}"
|
|
|
|
|
|
def test_post_and_next_premarket_are_disjoint():
|
|
target = date(2026, 4, 22)
|
|
_, post_end = session_window(target, "post")
|
|
next_pm_start, _ = session_window(date(2026, 4, 23), "premarket")
|
|
# premarket starts at *previous close* (16:00), so they don't share boundary;
|
|
# the contract is post_end <= 04:00 ET while next premarket fully overlaps that
|
|
# zone. We assert the no-double-count guarantee: a 04:30 ET headline lands in
|
|
# premarket (Thu) only, not in post (Wed).
|
|
sample_dt = _et(date(2026, 4, 23), time(4, 30))
|
|
assert sample_dt >= post_end, "04:30 ET headline should be past post-window end"
|
|
assert sample_dt > next_pm_start # premarket window includes 04:30
|
|
|
|
|
|
def test_full_session_spans_prev_close_to_next_premarket_start():
|
|
target = date(2026, 4, 22)
|
|
start, end = session_window(target, "full_session")
|
|
assert start == _et(date(2026, 4, 21), time(16, 0))
|
|
assert end == _et(date(2026, 4, 23), time(4, 0))
|
|
|
|
|
|
def test_premarket_after_weekend_uses_friday_close():
|
|
"""Monday's premarket starts at the previous *Friday*'s close, not Sunday."""
|
|
target = date(2026, 4, 27) # Monday
|
|
start, _ = session_window(target, "premarket")
|
|
expected_start = _et(date(2026, 4, 24), time(16, 0)) # Friday
|
|
assert start == expected_start
|
|
|
|
|
|
def test_friday_post_extends_to_monday_premarket_start():
|
|
"""Friday's `post` window must extend across the weekend to Monday 04:00 ET."""
|
|
target = date(2026, 4, 24) # Friday
|
|
_, end = session_window(target, "post")
|
|
expected_end = _et(date(2026, 4, 27), time(4, 0)) # Monday 04:00
|
|
assert end == expected_end
|
|
|
|
|
|
def test_unknown_window_raises():
|
|
with pytest.raises(ValueError):
|
|
session_window(date(2026, 4, 22), "lunch") # type: ignore[arg-type]
|
|
|
|
|
|
def test_returned_datetimes_are_utc_aware():
|
|
start, end = session_window(date(2026, 4, 22), "premarket")
|
|
assert start.tzinfo is not None
|
|
assert end.tzinfo is not None
|
|
assert start.utcoffset().total_seconds() == 0 # type: ignore[union-attr]
|
|
assert end.utcoffset().total_seconds() == 0 # type: ignore[union-attr]
|