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 <noreply@anthropic.com>
This commit is contained in:
@@ -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", [])]
|
return [_Pt(p["value"]) for p in by_metric.get(key, {}).get("history", [])]
|
||||||
|
|
||||||
growth = reads.growth_read(hist("revenue_growth_yoy"))
|
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"))
|
op_margin = reads.margin_read(hist("operating_margin"))
|
||||||
fcf_margin = reads.margin_read(hist("fcf_margin"))
|
fcf_margin = reads.margin_read(hist("fcf_margin"))
|
||||||
share = reads.share_count_read(by_metric.get("share_count_change_yoy", {}).get("value"))
|
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: dict[str, str | None] = {k: None for k in _READ_KEYS}
|
||||||
by_key.update({
|
by_key.update({
|
||||||
"revenue_growth_yoy": growth,
|
"revenue_growth_yoy": growth,
|
||||||
|
"eps_growth_yoy": eps_growth,
|
||||||
"operating_margin": op_margin,
|
"operating_margin": op_margin,
|
||||||
"fcf_margin": fcf_margin,
|
"fcf_margin": fcf_margin,
|
||||||
"share_count_change_yoy": share,
|
"share_count_change_yoy": share,
|
||||||
|
|||||||
@@ -48,8 +48,9 @@ async def _seed_issuer(s, symbol, cik, sic, rev_base, price, *, eps_base=1.0, sn
|
|||||||
s.add(t)
|
s.add(t)
|
||||||
await s.flush()
|
await s.flush()
|
||||||
if snapshots:
|
if snapshots:
|
||||||
for fy, mult in [(2025, 1.0), (2026, 1.1)]:
|
# three fiscal years so YoY growth reads have a >=3 consecutive run
|
||||||
shares = 1000 if fy == 2025 else 950 # buyback
|
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)]
|
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)]
|
eps = [eps_base * mult * x for x in (1.0, 1.05, 1.1, 1.15)]
|
||||||
for i, fp in enumerate(_FP):
|
for i, fp in enumerate(_FP):
|
||||||
@@ -116,6 +117,44 @@ async def test_full_assembly(factory):
|
|||||||
assert dumped["metrics"][0]["key"] == "revenue_growth_yoy"
|
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 def test_no_cik_ticker_yields_null_metrics(factory):
|
||||||
async with factory() as s:
|
async with factory() as s:
|
||||||
s.add(Ticker(symbol="ADR", cik=None)) # no SEC identity
|
s.add(Ticker(symbol="ADR", cik=None)) # no SEC identity
|
||||||
|
|||||||
Reference in New Issue
Block a user