fix(sec): alert when a filing gap's reprieve lapses instead of re-pausing quietly
An escalated gap stops pausing setups while the issuer's own fundamentals are still recent. That reprieve ends on its own — the stored filings age past GAP_GATE_RECENT_FILING_DAYS, or a newer gap arrives and the all-escalated condition fails — and nothing reported either, because filing_gap_aged only escalates gaps whose escalated_at is NULL and so never fires twice for the same gap. For the 43 issuers behind the previous commit that lands around 2026-10-26, when their late-April filings age out together. sec_filing_gaps.exempted_at (migration 034) makes the transition observable: stamped quietly while the issuer is exempt, cleared when the exemption lapses, and the clear is what raises filing_gap_repaused — once per lapse, re-arming if the issuer's data recovers and ages out again. A gap that was never exempt has no transition and stays silent; it is simply still paused, which filing_gap_aged already said. gap_exempt_ciks is public so the importer alerts on membership changes in exactly the set the gate reads, rather than restating the rule. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Lo99z3jWqu9X9ueBU3Z3D
This commit is contained in:
@@ -1388,3 +1388,79 @@ async def test_reparse_never_restamps_a_collision_onto_the_co_registrant(engine)
|
||||
async with factory() as s:
|
||||
row = (await s.execute(select(FundamentalSnapshot))).scalar_one()
|
||||
assert row.cik == "0000906107" # still the issuer that filed it
|
||||
|
||||
|
||||
# --- the reprieve ending: an exemption that lapses must not do so silently ---
|
||||
|
||||
def _stale_gap(cik, *, exempted: bool):
|
||||
now = datetime.now(timezone.utc)
|
||||
return SecFilingGap(
|
||||
cik=cik, accession=f"{cik}-AGED-Q", form="10-Q",
|
||||
index_date=(now - timedelta(days=30)).date(), reason="not_in_companyfacts",
|
||||
first_seen_at=now - timedelta(days=30), last_attempted_at=now,
|
||||
escalated_at=now - timedelta(days=16),
|
||||
exempted_at=(now - timedelta(days=16)) if exempted else None,
|
||||
)
|
||||
|
||||
|
||||
def _snapshot(cik, *, age_days):
|
||||
filed = date.today() - timedelta(days=age_days)
|
||||
return FundamentalSnapshot(
|
||||
cik=cik, accession=f"{cik}-PRIOR", form="10-Q", filed_date=filed,
|
||||
accepted_at=datetime.now(timezone.utc) - timedelta(days=age_days),
|
||||
period_end=filed, fiscal_year=filed.year, fiscal_period="Q1",
|
||||
)
|
||||
|
||||
|
||||
async def _promote_only(engine, seed):
|
||||
"""Run promote() alone against seeded gap/snapshot state."""
|
||||
factory = _factory(engine)
|
||||
async with factory() as s:
|
||||
for obj in seed:
|
||||
s.add(obj)
|
||||
await s.commit()
|
||||
importer = _importer(FakeSecClient(
|
||||
tickers={}, companyfacts={}, submissions={}, latest_index=date(2026, 3, 1)
|
||||
))
|
||||
async with factory() as db:
|
||||
await importer.promote(db, StagedFundamentals(resolved=ResolvedUniverse()), run_id=77)
|
||||
await db.commit()
|
||||
async with factory() as s:
|
||||
gaps = (await s.execute(select(SecFilingGap))).scalars().all()
|
||||
events = (await s.execute(select(SystemEvent))).scalars().all()
|
||||
return gaps, events
|
||||
|
||||
|
||||
async def test_a_lapsed_exemption_raises_its_own_alert(engine):
|
||||
"""filing_gap_aged fires once and never again, so nothing else would say the
|
||||
pause came back when the issuer's own fundamentals aged out."""
|
||||
cik = "0000000060"
|
||||
gaps, events = await _promote_only(
|
||||
engine, [_stale_gap(cik, exempted=True), _snapshot(cik, age_days=400)]
|
||||
)
|
||||
repaused = [e for e in events if e.code == "filing_gap_repaused"]
|
||||
assert len(repaused) == 1
|
||||
assert f"{cik}/{cik}-AGED-Q" in repaused[0].message
|
||||
# Cleared, so a later recovery can re-arm and lapse again.
|
||||
assert gaps[0].exempted_at is None
|
||||
|
||||
|
||||
async def test_an_exemption_taking_effect_is_stamped_silently(engine):
|
||||
"""Setups resuming is what filing_gap_aged already described — stamping the
|
||||
state must not raise a second alert for it."""
|
||||
cik = "0000000061"
|
||||
gaps, events = await _promote_only(
|
||||
engine, [_stale_gap(cik, exempted=False), _snapshot(cik, age_days=120)]
|
||||
)
|
||||
assert [e.code for e in events if e.code.startswith("filing_gap")] == []
|
||||
assert gaps[0].exempted_at is not None
|
||||
|
||||
|
||||
async def test_a_still_paused_gap_is_not_reported_as_lapsing(engine):
|
||||
"""It never became exempt, so there is no transition to report."""
|
||||
cik = "0000000062"
|
||||
gaps, events = await _promote_only(
|
||||
engine, [_stale_gap(cik, exempted=False), _snapshot(cik, age_days=400)]
|
||||
)
|
||||
assert [e for e in events if e.code == "filing_gap_repaused"] == []
|
||||
assert gaps[0].exempted_at is None
|
||||
|
||||
Reference in New Issue
Block a user