diff --git a/compute/main.py b/compute/main.py index 6a3638800..613fb7508 100644 --- a/compute/main.py +++ b/compute/main.py @@ -1952,6 +1952,16 @@ def _fetch_one_form4(ticker: str) -> tuple[dict, float, bool]: next_update_utc=_iso(now + timedelta(days=_next_business_day_offset(now))), universe=config.UNIVERSE, universe_size=len(summaries), + # Phase 4.6 (0.10.7-phase4.6) — survivorship-bias provenance per + # Research Report v1.0 §7.4. Forward cron's as-of is today, and + # the universe we just scored IS today's current S&P 500 — so + # the lookup is honest by definition. survivorship_bias_corrected + # = True signals "this output's universe assumption is honest for + # its as_of_date" (vs False = historical query that fell back to + # current). Backtest / validation callers populate these from + # ``compute.ingest.historical_universe.members_at()`` directly. + universe_membership_as_of=now.date().isoformat(), + survivorship_bias_corrected=True, compute_run_id=os.environ.get("GITHUB_RUN_ID", "local"), git_commit=(os.environ.get("GITHUB_SHA") or "unknown")[:40], mos_trailing_ic_smoke=mos_ic, diff --git a/tests/test_output/test_writer.py b/tests/test_output/test_writer.py index b147c2ddf..a77ee3cf9 100644 --- a/tests/test_output/test_writer.py +++ b/tests/test_output/test_writer.py @@ -101,6 +101,47 @@ def test_write_metadata_json_tier2_disabled_round_trip(tmp_path): assert payload["tier2_enabled"] is False +def test_write_metadata_json_universe_provenance_round_trip(tmp_path): + """Phase 4.6 — `universe_membership_as_of` + `survivorship_bias_corrected` + survive the Pydantic → JSON round trip and stay accessible to the + verify-helper + downstream backtest consumers.""" + meta = Metadata( + version="0.10.7-phase4.6", + last_update_utc="2026-05-27T22:00:00Z", + next_update_utc="2026-05-28T22:00:00Z", + universe="SP500", + universe_size=502, + compute_run_id="run-789", + git_commit="abc789", + universe_membership_as_of="2026-05-27", + survivorship_bias_corrected=True, + ) + out = write_metadata_json(meta, tmp_path) + payload = json.loads(out.read_text()) + assert payload["universe_membership_as_of"] == "2026-05-27" + assert payload["survivorship_bias_corrected"] is True + + +def test_write_metadata_json_universe_provenance_legacy_snapshot(tmp_path): + """Pre-0.10.7 metadata.json shapes (no universe-provenance fields) + still serialize cleanly — the two new fields default to None and the + payload contains them as null.""" + meta = Metadata( + version="0.10.6-phase4.5e", + last_update_utc="2026-05-26T22:00:00Z", + next_update_utc="2026-05-27T22:00:00Z", + universe="SP500", + universe_size=502, + compute_run_id="run-legacy", + git_commit="legacy0", + # universe_membership_as_of / survivorship_bias_corrected omitted + ) + out = write_metadata_json(meta, tmp_path) + payload = json.loads(out.read_text()) + assert payload["universe_membership_as_of"] is None + assert payload["survivorship_bias_corrected"] is None + + def test_write_stock_detail_round_trip(tmp_path): detail = StockDetail( ticker="AAPL",