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