Files
signal-platform/tests/unit/test_earnings_alignment.py
T
dennisthiessenandClaude Opus 4.8 54ae8ba153 feat(dolt): A2 — DoltHub earnings importer (shadow ingestion)
A SourceImporter that ingests post-no-preference/earnings into earnings_events
for the tracked universe. Shadow by construction (nothing reads earnings_events
until A4).

- earnings_alignment.py: pure calendar<->EPS-history min-cost monotonic DP,
  reused from scripts/import_dolthub_earnings.py with identical constants (not
  extending that one-off script); symbol/session normalization; unit-tested
  against the pinned constants.
- dolt_client.py: async dolt CLI wrapper (pull / current_commit / query_csv via
  asyncio.create_subprocess_exec — never blocks the shared event loop) + disk
  guard before pull.
- dolt_earnings_importer.py: detect_revision = pull + HEAD hash; stage = query
  earnings_calendar + eps_history, dedup, align, map act_symbol->ticker_id
  (normalize both sides so dotted BRK.B joins); promote is destructive
  (delete future dolt_earnings rows + upsert; past never deleted) so validate is
  FAIL-CLOSED — blocks when the staged forward calendar is empty or has collapsed
  below 50% of what's loaded (the forward calendar is the acceptance gate).
- NOTICE: CC BY-SA 4.0 attribution; config: DOLT_BINARY / DOLT_DATA_DIR / etc.

Verified end-to-end against the real 1.68 GB clone (5 tickers: 133 events, 128
paired, forward calendar to 2026-08-26, BRK.B joined). Tests: 9 alignment + 7
importer + 1 skip-guarded real-clone smoke. Full suite 699 passed.

Remaining for A2: wire the daily ~02:30 ET shadow cron — deferred to pair with
the deploy-time dolt install + DOLT_DATA_DIR provisioning.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 10:55:27 +02:00

105 lines
3.7 KiB
Python

"""Unit tests for the pure calendar<->EPS-history alignment.
Anchored on the research script's exact constants (SKIP costs = 45, typical lag
= 30, session penalty = 3, windows 90/14) so a silently changed constant fails
here rather than quietly corrupting surprise-history pairing.
"""
from __future__ import annotations
from datetime import date
from app.services import earnings_alignment as ea
def test_normalise_symbol():
assert ea.normalise_symbol("bf.b ") == "BF-B"
assert ea.normalise_symbol(" aapl") == "AAPL"
assert ea.normalise_symbol(None) == ""
def test_normalise_session():
assert ea.normalise_session("Before market open") == "bmo"
assert ea.normalise_session("After market close") == "amc"
assert ea.normalise_session("During market hours") == "unknown"
assert ea.normalise_session(None) == "unknown"
assert ea.normalise_session("") == "unknown"
def test_safe_number():
assert ea.safe_number("1.5") == 1.5
assert ea.safe_number("") is None
assert ea.safe_number("not-a-number") is None
assert ea.safe_number("nan") is None # non-finite rejected
def test_constants_pinned():
assert ea.SKIP_EVENT_COST == 45.0
assert ea.SKIP_PERIOD_COST == 45.0
assert ea._TYPICAL_ANNOUNCE_LAG_DAYS == 30
assert ea._MISSING_SESSION_PENALTY == 3.0
def test_match_cost_uses_pinned_lag_and_penalty():
period = {"period_end_date": date(2026, 3, 31)}
# delta == 30 (typical lag) → base cost 0; known session → no penalty
e_known = {"announce_date": date(2026, 4, 30), "session": "amc"}
assert ea.match_cost(e_known, period) == 0.0
# unknown session adds the penalty
e_unknown = {"announce_date": date(2026, 4, 30), "session": "unknown"}
assert ea.match_cost(e_unknown, period) == 3.0
# delta 45 → |45-30| == 15
e_far = {"announce_date": date(2026, 5, 15), "session": "amc"}
assert ea.match_cost(e_far, period) == 15.0
def test_dedup_calendar_prefers_known_session():
rows = [
{"symbol": "AAPL", "announce_date": date(2026, 5, 1), "session": "unknown"},
{"symbol": "AAPL", "announce_date": date(2026, 5, 1), "session": "amc"},
]
grouped, stats = ea.dedup_calendar(rows)
assert stats["duplicate_rows"] == 1
assert grouped["AAPL"][0]["session"] == "amc"
def test_dedup_history_prefers_fuller_row():
rows = [
{"symbol": "AAPL", "period_end_date": date(2026, 3, 31), "eps_actual": 2.0, "eps_estimate": None},
{"symbol": "AAPL", "period_end_date": date(2026, 3, 31), "eps_actual": 2.0, "eps_estimate": 1.9},
]
grouped, stats = ea.dedup_history(rows)
assert stats["duplicate_rows"] == 1
kept = grouped["AAPL"][0]
assert kept["eps_actual"] == 2.0 and kept["eps_estimate"] == 1.9
def _events(*days):
return [{"announce_date": d, "session": "amc"} for d in days]
def _periods(*days):
return [{"period_end_date": d, "eps_actual": 1.0, "eps_estimate": 0.9} for d in days]
def test_align_matches_monotonic_pairs():
# two announcements ~30d after two quarter ends
events = _events(date(2026, 4, 30), date(2026, 7, 30))
periods = _periods(date(2026, 3, 31), date(2026, 6, 30))
matches, um_events, um_periods = ea.align_symbol(
events, periods, max_lag_days=90, max_lead_days=14
)
assert matches == [(0, 0), (1, 1)]
assert um_events == [] and um_periods == []
def test_align_leaves_out_of_window_unmatched():
# announcement 200 days after the only period end → outside the 90d window
events = _events(date(2026, 10, 17))
periods = _periods(date(2026, 3, 31))
matches, um_events, um_periods = ea.align_symbol(
events, periods, max_lag_days=90, max_lead_days=14
)
assert matches == []
assert um_events == [0] and um_periods == [0]