premarket_eval's shadow enrichment call site queried session_date = current_date (wrong on UTC-server Postgres at night, so it never matched any symbols) and shared one try-block for float_cache + dilution filings (a float_cache failure silently skipped the dilution refresh too). Fix: compute the ET session date in Python, split into two independent try-blocks, lift into a testable _refresh_shadow_enrichment(con, session_date). float_cache now applies its own DDL unconditionally on use instead of only when an API key is present. Adds iv_dilution_gate (additive, mirrors iv_regime_gate's keep-or-zero shape keyed on symbol instead of session_date, fail-open on unfetched symbols) plus its once-per-run preload, and registers the pre-registered dilution_veto_v1 experiment via the existing experiments/*.json self-registering cohort. Live veto stays OFF; this only wires the shadow measurement. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
73 lines
2.8 KiB
Python
73 lines
2.8 KiB
Python
"""research/premarket_eval.py::_refresh_shadow_enrichment (plan 016) — the
|
|
nightly float/dilution enrichment call site was broken two ways: a
|
|
``session_date = current_date`` query (wrong on UTC-server Postgres at night)
|
|
and a shared try-block where a float_cache failure silently skipped the
|
|
dilution refresh too. This test runs against an ISOLATED throwaway Postgres
|
|
schema (EventStore ":memory:"), never the production DB (mirrors
|
|
tests/unit/experiments/conftest.py), and never makes a live HTTP call — both
|
|
enrichment functions are monkeypatched."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import date, datetime
|
|
|
|
import pytest
|
|
|
|
from research.premarket_eval import _refresh_shadow_enrichment
|
|
from storage.event_store import EventStore
|
|
|
|
SESS = date(2026, 6, 11)
|
|
|
|
|
|
@pytest.fixture
|
|
def store():
|
|
s = EventStore(":memory:")
|
|
yield s
|
|
s.close()
|
|
|
|
|
|
def _seed_minute_bar(store, symbol, session_date, volume):
|
|
store.con.execute(
|
|
"INSERT INTO minute_bars (symbol, timestamp, session_date, is_premarket, "
|
|
"is_regular_hours, open, high, low, close, volume, vwap, quality_score) "
|
|
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
|
|
[symbol, datetime(session_date.year, session_date.month, session_date.day, 13, 35),
|
|
session_date, False, True, 10.0, 10.1, 9.9, 10.0, volume, 10.0, 1.0])
|
|
|
|
|
|
def test_dilution_still_called_when_float_raises(store, monkeypatch):
|
|
_seed_minute_bar(store, "AAA", SESS, 1_000_000)
|
|
_seed_minute_bar(store, "BBB", SESS, 500_000)
|
|
# a different session_date's bars must not leak into the query
|
|
_seed_minute_bar(store, "ZZZ", date(2026, 6, 10), 9_999_999)
|
|
|
|
def _raise_float(con, symbols):
|
|
raise RuntimeError("relation \"float_cache\" does not exist")
|
|
|
|
calls = []
|
|
|
|
def _record_dilution(con, symbols):
|
|
calls.append((con, list(symbols)))
|
|
return {"symbols": len(symbols), "filings": 0}
|
|
|
|
monkeypatch.setattr("research.ingestion.float_cache.refresh_float_cache", _raise_float)
|
|
monkeypatch.setattr("research.ingestion.sec_dilution.refresh_dilution_events", _record_dilution)
|
|
|
|
_refresh_shadow_enrichment(store.con, SESS)
|
|
|
|
assert len(calls) == 1
|
|
seen_con, seen_symbols = calls[0]
|
|
assert seen_con is store.con
|
|
# highest-volume first, only the seeded session's symbols
|
|
assert seen_symbols == ["AAA", "BBB"]
|
|
|
|
|
|
def test_no_symbols_is_a_noop(store, monkeypatch):
|
|
called = {"float": False, "dilution": False}
|
|
monkeypatch.setattr("research.ingestion.float_cache.refresh_float_cache",
|
|
lambda con, syms: called.__setitem__("float", True))
|
|
monkeypatch.setattr("research.ingestion.sec_dilution.refresh_dilution_events",
|
|
lambda con, syms: called.__setitem__("dilution", True))
|
|
_refresh_shadow_enrichment(store.con, SESS) # no minute_bars seeded
|
|
assert called == {"float": False, "dilution": False}
|