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.
stock-oracle/tests/test_news_v2_session_window.py

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]