chore: decommission FMP, Finnhub and Alpha Vantage (A6)

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>
This commit is contained in:
2026-08-07 11:19:28 +02:00
co-authored by Claude Opus 5
parent f5d4b516ab
commit 3e83d63b05
51 changed files with 368 additions and 3787 deletions
+28 -42
View File
@@ -1,13 +1,20 @@
"""Unit tests for fundamental_service — unavailable_fields persistence."""
"""Unit tests for fundamental_service — the surviving read path.
Writes to ``fundamental_data`` are covered by test_fundamental_data_refresh.py;
this file only guards the lookup used by the router and scoring.
"""
from __future__ import annotations
import json
from datetime import datetime, timezone
import pytest
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from app.database import Base
from app.exceptions import NotFoundError
from app.models.fundamental import FundamentalData
from app.models.ticker import Ticker
from app.services import fundamental_service
@@ -43,57 +50,36 @@ async def ticker(session: AsyncSession) -> Ticker:
@pytest.mark.asyncio
async def test_store_fundamental_persists_unavailable_fields(
async def test_get_fundamental_returns_the_cached_row(
session: AsyncSession, ticker: Ticker
):
"""unavailable_fields dict is serialized to JSON and stored."""
fields = {"pe_ratio": "requires paid plan", "revenue_growth": "requires paid plan"}
record = await fundamental_service.store_fundamental(
session,
symbol="AAPL",
pe_ratio=None,
revenue_growth=None,
market_cap=1_000_000.0,
unavailable_fields=fields,
fields = {"pe_ratio": "split guard applied"}
session.add(
FundamentalData(
ticker_id=ticker.id,
pe_ratio=None,
market_cap=1_000_000.0,
fetched_at=datetime.now(timezone.utc),
unavailable_fields_json=json.dumps(fields),
)
)
await session.commit()
record = await fundamental_service.get_fundamental(session, symbol="aapl")
assert record is not None
assert record.market_cap == 1_000_000.0
assert json.loads(record.unavailable_fields_json) == fields
@pytest.mark.asyncio
async def test_store_fundamental_defaults_to_empty_dict(
async def test_get_fundamental_returns_none_without_a_cached_row(
session: AsyncSession, ticker: Ticker
):
"""When unavailable_fields is not provided, column defaults to '{}'."""
record = await fundamental_service.store_fundamental(
session,
symbol="AAPL",
pe_ratio=25.0,
)
assert json.loads(record.unavailable_fields_json) == {}
assert await fundamental_service.get_fundamental(session, symbol="AAPL") is None
@pytest.mark.asyncio
async def test_store_fundamental_updates_unavailable_fields(
session: AsyncSession, ticker: Ticker
):
"""Updating an existing record also updates unavailable_fields_json."""
# First store
await fundamental_service.store_fundamental(
session,
symbol="AAPL",
pe_ratio=None,
unavailable_fields={"pe_ratio": "requires paid plan"},
)
# Second store — fields now available
record = await fundamental_service.store_fundamental(
session,
symbol="AAPL",
pe_ratio=25.0,
unavailable_fields={},
)
assert json.loads(record.unavailable_fields_json) == {}
async def test_get_fundamental_rejects_an_unknown_symbol(session: AsyncSession):
with pytest.raises(NotFoundError):
await fundamental_service.get_fundamental(session, symbol="NOPE")