"""A5 fundamentals parity report: read-only comparison + artifact archive.""" from __future__ import annotations from datetime import date, datetime, timezone import pytest from sqlalchemy import func, select from app.models.data_import_run import DataImportRun from app.models.earnings_event import EarningsEvent from app.models.fundamental import FundamentalData from app.models.fundamental_snapshot import FundamentalSnapshot from app.models.ohlcv import OHLCVRecord from app.models.ticker import Ticker from app.services.fundamentals_parity_service import ( build_report, fundamental_score, load_latest, load_latest_csv, load_latest_json, store_report, ) UTC = timezone.utc GENERATED = datetime(2026, 7, 23, 10, 30, tzinfo=UTC) def _snapshot_rows(cik: str) -> list[FundamentalSnapshot]: rows = [] periods = ("Q1", "Q2", "Q3", "FY") months = (3, 6, 9, 12) for fy, multiplier in ((2025, 1.0), (2026, 1.1)): revenues = [100 * multiplier, 110 * multiplier, 120 * multiplier, 130 * multiplier] eps = [1.0 * multiplier, 1.1 * multiplier, 1.2 * multiplier, 1.3 * multiplier] for index, period in enumerate(periods): period_end = date(fy, months[index], 28) rows.append( FundamentalSnapshot( cik=cik, accession=f"{cik}-{fy}-{period}", form="10-K" if period == "FY" else "10-Q", filed_date=period_end, accepted_at=datetime(fy, months[index], 28, tzinfo=UTC), period_end=period_end, fiscal_year=fy, fiscal_period=period, revenue=sum(revenues[: index + 1]), operating_income=sum(revenues[: index + 1]) * 0.2, diluted_eps=sum(eps[: index + 1]), cfo=sum(revenues[: index + 1]) * 0.25, capex=sum(revenues[: index + 1]) * 0.05, depreciation_amortization=sum(revenues[: index + 1]) * 0.05, cash_and_st_investments=40, total_debt=100, shares_outstanding=1000, ) ) return rows async def _seed(db_session): first = Ticker(symbol="AAA", cik="0000000001", sic="3571") second = Ticker(symbol="BBB", cik=None, sic=None) db_session.add_all([first, second]) await db_session.flush() db_session.add_all(_snapshot_rows(first.cik)) db_session.add_all( [ FundamentalData( ticker_id=first.id, pe_ratio=25, revenue_growth=5, earnings_surprise=0, fetched_at=GENERATED, ), FundamentalData( ticker_id=second.id, pe_ratio=12, revenue_growth=3, earnings_surprise=None, fetched_at=GENERATED, ), OHLCVRecord( ticker_id=first.id, date=date(2026, 7, 22), open=100, high=100, low=100, close=100, volume=100, ), EarningsEvent( ticker_id=first.id, announce_date=date(2026, 7, 1), session="amc", eps_estimate=2, eps_actual=2.2, source="dolt_earnings", ), DataImportRun( source="sec_facts", revision="sec-rev", status="promoted", source_max_date=date(2026, 7, 22), started_at=GENERATED, completed_at=GENERATED, ), DataImportRun( source="dolt_earnings", revision="dolt-rev", status="no_op", source_max_date=date(2026, 7, 22), started_at=GENERATED, completed_at=GENERATED, ), ] ) await db_session.flush() def test_score_formula_matches_production_rules(): score = fundamental_score(pe_ratio=15, revenue_growth=0, earnings_surprise=0) assert score == pytest.approx((100 + 50 + 50) / 3) assert fundamental_score(pe_ratio=15, revenue_growth=None, earnings_surprise=None) is None async def test_report_compares_sources_and_leaves_database_untouched(db_session): await _seed(db_session) before = await db_session.scalar(select(func.count()).select_from(FundamentalData)) report = await build_report( db_session, generated_at=GENERATED, today=date(2026, 7, 23), ) after = await db_session.scalar(select(func.count()).select_from(FundamentalData)) assert before == after == 2 assert not db_session.new and not db_session.dirty and not db_session.deleted assert report["read_only"] is True assert report["approval_status"] == "pending_explicit_approval" assert report["source_runs"]["sec_facts"]["revision"] == "sec-rev" assert report["source_runs"]["dolt_earnings"]["revision"] == "dolt-rev" first = next(row for row in report["rows"] if row["symbol"] == "AAA") assert first["fields"]["pe_ratio"]["candidate"] == pytest.approx( 100 / 5.06, abs=1e-4 ) assert first["fields"]["revenue_growth"]["candidate"] == pytest.approx(10) assert first["fields"]["earnings_surprise"]["candidate"] == pytest.approx(10) assert first["scores"]["candidate_fundamental"] is not None assert report["summary"]["universe_count"] == 2 assert report["summary"]["field_stats"]["pe_ratio"]["both_available"] == 1 async def test_artifacts_archive_and_latest_manifest(db_session, tmp_path): await _seed(db_session) report = await build_report( db_session, generated_at=GENERATED, today=date(2026, 7, 23), ) paths = store_report(report, tmp_path) assert tmp_path.joinpath("latest.json").exists() assert paths["json"].endswith(".json") and paths["csv"].endswith(".csv") assert load_latest(tmp_path)["generated_at"] == GENERATED.isoformat() csv_artifact = load_latest_csv(tmp_path) assert csv_artifact is not None assert csv_artifact[0].endswith(".csv") assert "legacy_fundamental,candidate_fundamental" in csv_artifact[1] assert "AAA" in csv_artifact[1] json_artifact = load_latest_json(tmp_path) assert json_artifact is not None and '"rows"' in json_artifact[1] async def test_admin_endpoints_return_compact_summary_and_downloads( client, db_session, tmp_path, monkeypatch ): from app.config import settings from app.dependencies import require_admin from app.main import app await _seed(db_session) report = await build_report( db_session, generated_at=GENERATED, today=date(2026, 7, 23), ) store_report(report, tmp_path) monkeypatch.setattr(settings, "fundamentals_parity_report_dir", str(tmp_path)) app.dependency_overrides[require_admin] = lambda: None try: summary_response = await client.get("/api/v1/admin/fundamentals-parity") assert summary_response.status_code == 200 summary = summary_response.json()["data"] assert summary["summary"]["universe_count"] == 2 assert "rows" not in summary csv_response = await client.get("/api/v1/admin/fundamentals-parity/csv") assert csv_response.status_code == 200 assert "AAA" in csv_response.json()["data"]["content"] json_response = await client.get("/api/v1/admin/fundamentals-parity/json") assert json_response.status_code == 200 assert '"rows"' in json_response.json()["data"]["content"] finally: app.dependency_overrides.pop(require_admin, None)