From fb6d39c68bf5b25979766ece0fa296838be51dcf Mon Sep 17 00:00:00 2001 From: Dennis Thiessen Date: Wed, 22 Jul 2026 22:01:24 +0200 Subject: [PATCH] fix(fundamentals): compute eps_growth_yoy read; cover same-day + price guards - The eps_growth_yoy read was never computed, leaving that fixed by_key entry null even with sufficient EPS history; now growth_read() is applied to EPS history just like revenue. - Tests: same-day earnings returns as next with days_until 0 (and not in recent); zero close guards valuation to null; eps read populated. Fixture seeds three fiscal years so YoY growth reads have a >=3 run. 9 API tests pass. Co-Authored-By: Claude Opus 4.8 --- app/services/fundamentals_api_service.py | 2 ++ tests/unit/test_fundamentals_api.py | 43 ++++++++++++++++++++++-- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/app/services/fundamentals_api_service.py b/app/services/fundamentals_api_service.py index 1a1da33..3620bd9 100644 --- a/app/services/fundamentals_api_service.py +++ b/app/services/fundamentals_api_service.py @@ -198,6 +198,7 @@ def _build_reads(metrics: list[dict], valuation: dict | None) -> dict[str, Any]: return [_Pt(p["value"]) for p in by_metric.get(key, {}).get("history", [])] growth = reads.growth_read(hist("revenue_growth_yoy")) + eps_growth = reads.growth_read(hist("eps_growth_yoy")) op_margin = reads.margin_read(hist("operating_margin")) fcf_margin = reads.margin_read(hist("fcf_margin")) share = reads.share_count_read(by_metric.get("share_count_change_yoy", {}).get("value")) @@ -209,6 +210,7 @@ def _build_reads(metrics: list[dict], valuation: dict | None) -> dict[str, Any]: by_key: dict[str, str | None] = {k: None for k in _READ_KEYS} by_key.update({ "revenue_growth_yoy": growth, + "eps_growth_yoy": eps_growth, "operating_margin": op_margin, "fcf_margin": fcf_margin, "share_count_change_yoy": share, diff --git a/tests/unit/test_fundamentals_api.py b/tests/unit/test_fundamentals_api.py index c88ef74..d99b0b1 100644 --- a/tests/unit/test_fundamentals_api.py +++ b/tests/unit/test_fundamentals_api.py @@ -48,8 +48,9 @@ async def _seed_issuer(s, symbol, cik, sic, rev_base, price, *, eps_base=1.0, sn s.add(t) await s.flush() if snapshots: - for fy, mult in [(2025, 1.0), (2026, 1.1)]: - shares = 1000 if fy == 2025 else 950 # buyback + # three fiscal years so YoY growth reads have a >=3 consecutive run + for fy, mult in [(2024, 0.9), (2025, 1.0), (2026, 1.1)]: + shares = {2024: 1050, 2025: 1000, 2026: 950}[fy] # steady buyback rev = [rev_base * mult * x for x in (1.0, 1.05, 1.1, 1.15)] eps = [eps_base * mult * x for x in (1.0, 1.05, 1.1, 1.15)] for i, fp in enumerate(_FP): @@ -116,6 +117,44 @@ async def test_full_assembly(factory): assert dumped["metrics"][0]["key"] == "revenue_growth_yoy" +async def test_same_day_earnings_is_next_with_zero_days(factory): + async with factory() as s: + t = Ticker(symbol="TDY", cik=None) + s.add(t) + await s.flush() + s.add(EarningsEvent(ticker_id=t.id, announce_date=TODAY, session="bmo", source="dolt_earnings")) + s.add(EarningsEvent(ticker_id=t.id, announce_date=date(2026, 9, 1), session="amc", + eps_estimate=1.0, eps_actual=1.1, source="dolt_earnings")) + await s.commit() + async with factory() as s: + v1 = await build_fundamentals_v1(s, "TDY", today=TODAY) + assert v1["earnings"]["next"] == {"date": TODAY.isoformat(), "session": "bmo", "days_until": 0} + # the same-day event is upcoming, not in recent + assert all(r["announce_date"] != TODAY.isoformat() for r in v1["earnings"]["recent"]) + + +async def test_eps_growth_read_is_populated(factory): + await _seed_group(factory) + async with factory() as s: + v1 = await build_fundamentals_v1(s, "AAPL", today=TODAY) + assert v1["reads"]["by_key"]["eps_growth_yoy"] is not None # EPS read now computed + + +async def test_non_positive_price_guards_valuation(factory): + async with factory() as s: + t = Ticker(symbol="ZERO", cik="0000000055", sic="3571") + s.add(t) + await s.flush() + s.add(FundamentalSnapshot(cik="0000000055", accession="z", form="10-K", filed_date=date(2026, 1, 1), + accepted_at=datetime(2026, 1, 1, tzinfo=UTC), period_end=date(2025, 12, 31), + fiscal_year=2025, fiscal_period="FY", diluted_eps=5.0, shares_outstanding=1000)) + s.add(OHLCVRecord(ticker_id=t.id, date=date(2026, 1, 2), open=0, high=0, low=0, close=0, volume=1)) + await s.commit() + async with factory() as s: + v1 = await build_fundamentals_v1(s, "ZERO", today=TODAY) + assert v1["valuation"] is None # close of 0 is not a usable price + + async def test_no_cik_ticker_yields_null_metrics(factory): async with factory() as s: s.add(Ticker(symbol="ADR", cik=None)) # no SEC identity