fix(fundamentals): API v1 review — multi-class pricing, reads contract, guards

1. Multi-class subject is priced by the REQUESTED ticker: the peer group's
   representative for the subject CIK is overridden to the requested ticker_id
   (other issuers pick a deterministic-by-symbol rep), so GOOGL's P/E uses
   GOOGL's price, not GOOG's. Differing-price GOOG/GOOGL test added.
2. reads matches the selected contract: header is null when there is no read;
   by_key is a fixed map over every metric key plus pe and fcf_yield, null when
   unavailable (was a sparse dict).
3. Earnings use the New York calendar date; same-day is UPCOMING (days_until 0),
   recent is strictly earlier.
4. Valuation is null when there is no usable price (> 0 required for P/E and
   market cap); when present, price_date is non-null.

Added a real router/API-envelope test with a seeded legacy record (the endpoint,
not just the schema merge). 6 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 21:49:17 +02:00
co-authored by Claude Opus 4.8
parent 459a925e36
commit b3dcf356a6
3 changed files with 124 additions and 38 deletions
+62 -5
View File
@@ -107,8 +107,9 @@ async def test_full_assembly(factory):
assert val["price_date"] == "2026-10-01"
assert val["pe_industry"] is not None
# reads present + additive merge validates against the schema
# reads: header string + fixed by_key map (every metric + pe + fcf_yield)
assert v1["reads"]["header"]
assert set(v1["reads"]["by_key"]) == set(METRIC_KEYS) | {"pe", "fcf_yield"}
data = FundamentalResponse(symbol="AAPL", pe_ratio=12.3, **v1) # legacy + v1 additive
dumped = data.model_dump()
assert dumped["pe_ratio"] == 12.3 # legacy preserved untouched
@@ -123,7 +124,8 @@ async def test_no_cik_ticker_yields_null_metrics(factory):
v1 = await build_fundamentals_v1(s, "ADR", today=TODAY)
assert v1["valuation"] is None
assert all(m["value"] is None and m["industry"] is None for m in v1["metrics"])
assert v1["reads"] == {"header": "", "metrics": {}}
assert v1["reads"]["header"] is None
assert v1["reads"]["by_key"] == {k: None for k in list(METRIC_KEYS) + ["pe", "fcf_yield"]}
async def test_industry_omitted_below_five_peers(factory):
@@ -151,6 +153,61 @@ async def test_valuation_guarded_without_price(factory):
await s.commit()
async with factory() as s:
v1 = await build_fundamentals_v1(s, "NOPX", today=TODAY)
val = v1["valuation"]
assert val is not None # snapshots exist -> object present
assert val["pe"] is None and val["market_cap_est"] is None and val["price_date"] is None
# no usable price -> valuation is null under the approved contract
assert v1["valuation"] is None
async def test_multiclass_subject_priced_by_requested_ticker(factory):
cik = "0001652044"
async with factory() as s:
# GOOGL and GOOG share one CIK/snapshots but trade at different prices
await _seed_issuer(s, "GOOGL", cik, "7372", rev_base=1000, price=200, eps_base=2.0)
# add a second class sharing the CIK: same snapshots exist; just its own ticker+price
goog = Ticker(symbol="GOOG", cik=cik, sic="7372")
s.add(goog)
await s.flush()
s.add(OHLCVRecord(ticker_id=goog.id, date=date(2026, 10, 1), open=100, high=100, low=100, close=100, volume=1))
for i in range(4): # peers so the group has >= 5 valid issuers
await _seed_issuer(s, f"P{i}", f"000000020{i}", "7373", rev_base=600 + i * 50, price=40 + i * 5)
await s.commit()
async with factory() as s:
googl = await build_fundamentals_v1(s, "GOOGL", today=TODAY)
goog_v = await build_fundamentals_v1(s, "GOOG", today=TODAY)
# subject P/E uses the REQUESTED class's price (200 vs 100), not an arbitrary sibling
assert googl["valuation"]["pe"] == pytest.approx(goog_v["valuation"]["pe"] * 2, rel=1e-6)
async def test_endpoint_merges_legacy_and_v1(client, db_session):
from datetime import timezone as _tz
from app.dependencies import require_access
from app.main import app
from app.models.fundamental import FundamentalData
app.dependency_overrides[require_access] = lambda: None
try:
t = Ticker(symbol="AAPL", cik="0000000001", sic="3571")
db_session.add(t)
await db_session.flush()
db_session.add(FundamentalData(ticker_id=t.id, pe_ratio=12.3, revenue_growth=5.0,
fetched_at=datetime(2026, 1, 1, tzinfo=_tz.utc)))
db_session.add(FundamentalSnapshot(cik="0000000001", accession="a", form="10-K",
filed_date=date(2026, 1, 1), accepted_at=datetime(2026, 1, 1, tzinfo=_tz.utc),
period_end=date(2025, 12, 31), fiscal_year=2025, fiscal_period="FY",
diluted_eps=5.0, shares_outstanding=1000))
db_session.add(OHLCVRecord(ticker_id=t.id, date=date(2026, 1, 2), open=100, high=100, low=100, close=100, volume=1))
await db_session.flush()
resp = await client.get("/api/v1/fundamentals/AAPL")
assert resp.status_code == 200
data = resp.json()["data"]
assert data["pe_ratio"] == 12.3 # legacy preserved
assert data["revenue_growth"] == 5.0
assert len(data["metrics"]) == 7 # additive v1
assert data["earnings"] is not None
assert "by_key" in data["reads"]
assert data["valuation"]["price_date"] == "2026-01-02"
finally:
app.dependency_overrides.pop(require_access, None)