Files
signal-platform/tests/unit/test_fundamentals_quality_service.py
T
dennisthiessenandClaude Opus 5 a13dbc9710 fix(sec): stop an unrecoverable filing gap pausing setups forever
A filing gap pauses its issuer until the filing is ingested or a later one
supersedes it, which assumes the gap is temporary. It is not always: SEC's
per-company Company-Facts files can go stale indefinitely — 43 large caps
whose Q2 10-Qs the frames API carries but whose companyfacts files never
received (Abbott's newest fact was 2026-04-29 in late August) — and because
the supersede rule needs a *successfully ingested* later filing, a stale file
swallows the next quarter too. The pause was open-ended, not seasonal.

So the pause hands off to the alert: once filing_gap_aged has escalated a gap,
it stops gating if the issuer's newest stored 10-K/10-Q is under 180 days old.
An issuer with nothing that recent has no usable fundamentals at all and stays
paused, which is the case the gate was built for.

Applied in the gate service only. active_gaps is deliberately untouched so
_retry_backlog keeps retrying and a recovered filing still resolves normally,
and the bound covers both gate paths — the queue and the validation_json
summary that mirrors the same filings — since bounding one leaves production
behaviour unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Lo99z3jWqu9X9ueBU3Z3D
2026-08-21 16:46:28 +02:00

260 lines
8.3 KiB
Python

from __future__ import annotations
import json
from datetime import date, datetime, timedelta, timezone
from app.models.data_import_run import DataImportRun
from app.models.fundamental_snapshot import FundamentalSnapshot
from app.models.sec_filing_gap import SecFilingGap
from app.models.ticker import Ticker
from app.services import fundamentals_quality_service
async def test_latest_sec_validation_blocks_deferred_and_no_history_ciks(
db_session,
):
missing = Ticker(symbol="MISSING", cik="0000000001")
no_history = Ticker(symbol="NEWREG", cik="0000000002")
healthy = Ticker(symbol="HEALTHY", cik="0000000003")
db_session.add_all([missing, no_history, healthy])
await db_session.flush()
db_session.add(
DataImportRun(
source="sec_facts",
status="deferred",
validation_json=json.dumps({
"missing_xbrl": [{"cik": missing.cik, "accession": "MISSING-Q"}],
"no_xbrl_filings": [{"cik": no_history.cik}],
}),
started_at=datetime.now(timezone.utc),
)
)
await db_session.flush()
assert await fundamentals_quality_service.blocked_ticker_ids(db_session) == {
missing.id,
no_history.id,
}
async def test_active_gap_is_blocked_until_a_later_filing_supersedes_it(db_session):
ticker = Ticker(symbol="HIST", cik="0000000043")
now = datetime.now(timezone.utc)
db_session.add_all([
ticker,
SecFilingGap(
cik=ticker.cik,
accession="HIST-Q",
form="10-Q",
index_date=date.today().replace(day=1),
reason="coregistrant_facts_rejected",
first_seen_at=now,
last_attempted_at=now,
),
])
await db_session.flush()
assert await fundamentals_quality_service.blocked_ticker_ids(db_session) == {
ticker.id
}
db_session.add(
FundamentalSnapshot(
cik=ticker.cik,
accession="LATER-Q",
form="10-Q",
filed_date=date.today(),
accepted_at=datetime.now(timezone.utc),
period_end=date.today(),
fiscal_year=date.today().year,
fiscal_period="Q2",
)
)
await db_session.flush()
assert await fundamentals_quality_service.blocked_ticker_ids(db_session) == set()
async def test_gap_without_index_date_uses_first_seen_date_for_supersession(
db_session,
):
ticker = Ticker(symbol="DATELESS", cik="0000000045")
first_seen = datetime(2026, 5, 1, 12, tzinfo=timezone.utc)
db_session.add_all([
ticker,
SecFilingGap(
cik=ticker.cik,
accession="DATELESS-Q",
form="10-Q",
index_date=None,
reason="not_in_companyfacts",
first_seen_at=first_seen,
last_attempted_at=first_seen,
),
])
await db_session.flush()
assert await fundamentals_quality_service.blocked_ticker_ids(db_session) == {
ticker.id
}
db_session.add(
FundamentalSnapshot(
cik=ticker.cik,
accession="LATER-DATELESS-Q",
form="10-Q",
filed_date=date(2026, 5, 2),
accepted_at=datetime(2026, 5, 2, 12, tzinfo=timezone.utc),
period_end=date(2026, 3, 31),
fiscal_year=2026,
fiscal_period="Q1",
)
)
await db_session.flush()
assert await fundamentals_quality_service.blocked_ticker_ids(db_session) == set()
async def test_ticker_quality_explains_no_xbrl_block(db_session):
ticker = Ticker(symbol="NEWREG", cik="0000000044")
db_session.add_all([
ticker,
DataImportRun(
source="sec_facts",
status="promoted",
validation_json=json.dumps({
"setup_blocked_ciks": [ticker.cik],
"no_xbrl_ciks": [ticker.cik],
"no_xbrl_filings": [],
}),
started_at=datetime.now(timezone.utc),
),
])
await db_session.flush()
quality = await fundamentals_quality_service.ticker_quality(db_session, "NEWREG")
assert quality.eligible is False
assert quality.code == "no_xbrl_filings"
assert "CIK override" in (quality.message or "")
assert await fundamentals_quality_service.ticker_is_eligible(
db_session, ticker.id
) is False
def _escalated_gap(cik: str, *, escalated: bool = True) -> SecFilingGap:
first_seen = datetime.now(timezone.utc) - timedelta(days=24)
return SecFilingGap(
cik=cik,
accession=f"{cik}-STALE-Q",
form="10-Q",
index_date=(first_seen.date()),
reason="not_in_companyfacts",
first_seen_at=first_seen,
last_attempted_at=datetime.now(timezone.utc),
escalated_at=(
datetime.now(timezone.utc) - timedelta(days=10) if escalated else None
),
)
def _prior_quarter(cik: str, *, age_days: int) -> FundamentalSnapshot:
"""The issuer's last successfully ingested filing, older than the gap so it
cannot supersede it — exactly the production shape of a stale companyfacts
file: Q1 stored, Q2 missing."""
filed = date.today() - timedelta(days=age_days)
return FundamentalSnapshot(
cik=cik,
accession=f"{cik}-PRIOR-Q",
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 test_escalated_gap_stops_blocking_when_fundamentals_are_recent(db_session):
ticker = Ticker(symbol="STALEFACTS", cik="0000000046")
db_session.add(ticker)
db_session.add(_escalated_gap(ticker.cik))
await db_session.flush()
assert await fundamentals_quality_service.blocked_ticker_ids(db_session) == {
ticker.id
}
# The alert has run and the issuer still has last quarter to score on.
db_session.add(_prior_quarter(ticker.cik, age_days=120))
await db_session.flush()
assert await fundamentals_quality_service.blocked_ticker_ids(db_session) == set()
# ...but the filing is still queued, so the importer keeps retrying it.
assert len(await fundamentals_quality_service.active_gaps(db_session)) == 1
async def test_escalated_gap_keeps_blocking_when_fundamentals_are_stale(db_session):
ticker = Ticker(symbol="NOTHINGFRESH", cik="0000000047")
db_session.add_all([
ticker,
_escalated_gap(ticker.cik),
_prior_quarter(
ticker.cik,
age_days=fundamentals_quality_service.GAP_GATE_RECENT_FILING_DAYS + 30,
),
])
await db_session.flush()
assert await fundamentals_quality_service.blocked_ticker_ids(db_session) == {
ticker.id
}
async def test_unescalated_gap_still_blocks_alongside_an_escalated_one(db_session):
ticker = Ticker(symbol="TWOGAPS", cik="0000000048")
fresh = datetime.now(timezone.utc)
db_session.add_all([
ticker,
_escalated_gap(ticker.cik),
SecFilingGap(
cik=ticker.cik,
accession="TWOGAPS-FRESH-Q",
form="10-Q",
index_date=date.today(),
reason="not_in_companyfacts",
first_seen_at=fresh,
last_attempted_at=fresh,
),
_prior_quarter(ticker.cik, age_days=120),
])
await db_session.flush()
assert await fundamentals_quality_service.blocked_ticker_ids(db_session) == {
ticker.id
}
async def test_summary_path_does_not_reblock_an_exempt_cik(db_session):
"""The run summary mirrors the same filings as the queue — it must honour the
same hand-off, or the bound is inert in production."""
ticker = Ticker(symbol="MIRRORED", cik="0000000049")
db_session.add(ticker)
db_session.add(_escalated_gap(ticker.cik))
db_session.add(_prior_quarter(ticker.cik, age_days=120))
await db_session.flush()
db_session.add(
DataImportRun(
source="sec_facts",
status="promoted",
validation_json=json.dumps({
"setup_blocked_ciks": [ticker.cik],
"missing_xbrl": [
{"cik": ticker.cik, "accession": f"{ticker.cik}-STALE-Q"}
],
}),
started_at=datetime.now(timezone.utc),
)
)
await db_session.flush()
assert await fundamentals_quality_service.blocked_ticker_ids(db_session) == set()