The A5 cutover has been on and observed in production, so SEC Company Facts + DoltHub earnings are already the live source for `fundamental_data`. This removes everything the legacy path still occupied. Gone: the three providers and their config/env keys; the weekly `fundamental_collector` job; the cutover toggle (SEC + Dolt is now the unconditional path, so `off` can no longer silently freeze scoring inputs); the A5 parity report, whose deltas became structurally zero once the candidate builder started writing the table it compared against; and the FMP tier of universe bootstrap. Two behavioral notes: - Disabling **SEC Fundamentals Import** now stops the SEC network fetch only. The local cache refresh moved outside the job-enable check, because candidates also derive from daily closes and earnings events — freezing those on an ingestion pause would stale scoring with no fallback left to recover from. - `/ingestion/fetch?sources=fundamentals` still accepts the key and reports `skipped`; there is no per-ticker fetch any more. Migration 029 does not blanket-delete the leftover settings rows. Migrations run before the service restart, and pre-A6 code reads an absent `job_*_enabled` row as *enabled* — so the two behavior-bearing keys become tombstones pinned to safe values (hidden in Admin) and only the inert three are deleted. Removing the provider keys from the production `.env` is the matching rollout step. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
142 lines
4.3 KiB
Python
142 lines
4.3 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from datetime import date, datetime, 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
|