GET /fundamentals/{symbol} now returns the additive v1 objects alongside the
unchanged legacy fields (no legacy growth mapped onto the SEC TTM metric).
- earnings: next (date/session/days_until) + recent (<=4, with surprise_pct)
from earnings_events.
- metrics: fixed key set (value + dated history + per-metric SIC-peer industry
object + source=sec); net_debt has no industry (size-dependent).
- valuation: P/E, FCF yield, market_cap_est computed at REQUEST TIME from the
derived TTM inputs x the latest ohlcv close (no stored valuation); guarded to
null on missing/invalid inputs; pe_industry / fcf_yield_industry peer stats.
- reads: deterministic outputs in a SEPARATE object (header + per-metric reads).
Peer queries are batched and CIK-deduplicated by 2-digit SIC; industry omitted
below 5 valid peers. Schema extended with optional typed sub-models; the router
merges legacy + v1 so every existing field is preserved.
Tests: 4 (full assembly incl. peer industry + valuation + additive-merge, no-cik
null metrics, <5-peers omitted, price-guarded valuation).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
90 lines
2.4 KiB
Python
90 lines
2.4 KiB
Python
"""Pydantic schemas for fundamental data endpoints."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import date, datetime
|
|
|
|
from pydantic import BaseModel
|
|
|
|
|
|
class MetricIndustry(BaseModel):
|
|
label: str
|
|
median: float
|
|
favorable_percentile: int # 0-100, polarity-aware (higher = more favorable)
|
|
peer_count: int
|
|
|
|
|
|
class MetricHistoryPoint(BaseModel):
|
|
period_end: str # YYYY-MM-DD
|
|
value: float | None
|
|
|
|
|
|
class MetricItem(BaseModel):
|
|
key: str
|
|
value: float | None = None
|
|
history: list[MetricHistoryPoint] = []
|
|
industry: MetricIndustry | None = None
|
|
period_end: str | None = None
|
|
filed_date: str | None = None
|
|
source: str = "sec"
|
|
|
|
|
|
class EarningsNext(BaseModel):
|
|
date: str
|
|
session: str
|
|
days_until: int
|
|
|
|
|
|
class EarningsRecent(BaseModel):
|
|
announce_date: str
|
|
period_end: str | None = None
|
|
eps_estimate: float | None = None
|
|
eps_actual: float | None = None
|
|
surprise_pct: float | None = None
|
|
|
|
|
|
class EarningsObject(BaseModel):
|
|
next: EarningsNext | None = None
|
|
recent: list[EarningsRecent] = []
|
|
|
|
|
|
class Valuation(BaseModel):
|
|
pe: float | None = None
|
|
fcf_yield: float | None = None
|
|
market_cap_est: float | None = None
|
|
pe_industry: MetricIndustry | None = None
|
|
fcf_yield_industry: MetricIndustry | None = None
|
|
price_date: str | None = None
|
|
|
|
|
|
class FundamentalsReads(BaseModel):
|
|
"""Deterministic text outputs, separate from the numeric metrics."""
|
|
|
|
header: str = ""
|
|
metrics: dict[str, str] = {} # {metric_key: read}
|
|
|
|
|
|
class FundamentalResponse(BaseModel):
|
|
"""Envelope-ready fundamental data response.
|
|
|
|
Legacy fields are preserved unchanged (they come from ``fundamental_data`` /
|
|
the legacy providers). The additive v1 objects — earnings, metrics, valuation,
|
|
reads — are SEC/Dolt-derived and independent; a null legacy field is never
|
|
mapped onto the new SEC metrics and vice-versa.
|
|
"""
|
|
|
|
symbol: str
|
|
pe_ratio: float | None = None
|
|
revenue_growth: float | None = None
|
|
earnings_surprise: float | None = None
|
|
market_cap: float | None = None
|
|
next_earnings_date: date | None = None
|
|
fetched_at: datetime | None = None
|
|
unavailable_fields: dict[str, str] = {}
|
|
|
|
# --- additive v1 (always present; empty/null when unavailable) ---
|
|
earnings: EarningsObject | None = None
|
|
metrics: list[MetricItem] | None = None
|
|
valuation: Valuation | None = None
|
|
reads: FundamentalsReads | None = None
|