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:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user