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>
93 lines
2.6 KiB
Python
93 lines
2.6 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.
|
|
|
|
``by_key`` is a fixed map over every metric key plus ``pe`` and ``fcf_yield``,
|
|
each a read string or null. ``header`` is null when there is no read at all."""
|
|
|
|
header: str | None = None
|
|
by_key: dict[str, str | None] = {}
|
|
|
|
|
|
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
|