"""Pydantic response models for Stock Oracle API.""" from __future__ import annotations from typing import Any from pydantic import BaseModel, Field, field_validator # --------------------------------------------------------------------------- # Company Info # --------------------------------------------------------------------------- class CompanyInfo(BaseModel): ticker: str name: str | None = None cik: str | None = None exchange: str | None = None sector: str | None = None industry: str | None = None country: str | None = None market_cap: float | None = None extra: dict[str, Any] = Field(default_factory=dict) # --------------------------------------------------------------------------- # SEC Filings # --------------------------------------------------------------------------- class FilingEntry(BaseModel): accession_no: str form_type: str filing_date: str # ISO date string accepted_at: str | None = None primary_document: str | None = None description: str | None = None items: list[str] = Field(default_factory=list) @field_validator("items", mode="before") @classmethod def _coerce_none_to_list(cls, v: object) -> object: return v if v is not None else [] class FilingSearchResponse(BaseModel): ticker: str filings: list[FilingEntry] = Field(default_factory=list) total: int = 0 class ExhibitDocument(BaseModel): exhibit_type: str filename: str | None = None url: str | None = None class FilingDocumentsResponse(BaseModel): accession_no: str exhibits: list[ExhibitDocument] = Field(default_factory=list) class ExhibitResponse(BaseModel): accession_no: str exhibit_type: str content: str content_type: str = "text/plain" # --------------------------------------------------------------------------- # Filing Events (pre-parsed by Oracle) # --------------------------------------------------------------------------- class FilingEventEntry(BaseModel): id: str ticker: str accession_number: str form_type: str filing_date: str item_number: str | None = None event_type: str title: str | None = None summary: str | None = None content_source: str | None = None class FilingEventsResponse(BaseModel): ticker: str events: list[FilingEventEntry] = Field(default_factory=list) total_count: int = 0 # --------------------------------------------------------------------------- # Price / Market Data # --------------------------------------------------------------------------- class PriceBar(BaseModel): date: str # ISO date open: float high: float low: float close: float volume: int vwap: float | None = None trade_count: int | None = None class PriceDataResponse(BaseModel): ticker: str bars: list[PriceBar] = Field(default_factory=list) source: str = "yfinance" class PriceQuote(BaseModel): ticker: str price: float bid: float | None = None ask: float | None = None volume: int | None = None timestamp: str | None = None class IntradayBar(BaseModel): timestamp: str open: float high: float low: float close: float volume: int class IntradayResponse(BaseModel): ticker: str bars: list[IntradayBar] = Field(default_factory=list) timeframe: str = "1m" # --------------------------------------------------------------------------- # Financial / XBRL # --------------------------------------------------------------------------- class FinancialPeriod(BaseModel): period: str # e.g. "2025-Q4" period_end: str # ISO date revenue: float | None = None net_income: float | None = None eps: float | None = None gross_margin: float | None = None operating_margin: float | None = None extra: dict[str, Any] = Field(default_factory=dict) class FinancialDataResponse(BaseModel): ticker: str periods: list[FinancialPeriod] = Field(default_factory=list) # --------------------------------------------------------------------------- # FRED # --------------------------------------------------------------------------- class FredObservation(BaseModel): date: str # ISO date value: float | None = None class FredProxyResponse(BaseModel): series_id: str observations: list[FredObservation] = Field(default_factory=list) realtime_start: str | None = None realtime_end: str | None = None class FredSeriesInfo(BaseModel): id: str title: str | None = None frequency: str | None = None units: str | None = None notes: str | None = None # --------------------------------------------------------------------------- # FINRA Short Volume # --------------------------------------------------------------------------- class ShortVolumeEntry(BaseModel): date: str # ISO date short_volume: int short_exempt_volume: int | None = None total_volume: int | None = None class ShortVolumeResponse(BaseModel): symbol: str data: list[ShortVolumeEntry] = Field(default_factory=list) class ShortRatioPoint(BaseModel): date: str short_ratio: float | None = None short_percent: float | None = None class ShortRatioResponse(BaseModel): symbol: str data: list[ShortRatioPoint] = Field(default_factory=list) # --------------------------------------------------------------------------- # Dividends # --------------------------------------------------------------------------- class DividendCalendarEntry(BaseModel): ticker: str ex_dividend_date: str amount: float declaration_date: str | None = None record_date: str | None = None payment_date: str | None = None currency: str = "USD" dividend_type: str = "regular" frequency: str | None = None as_of_date: str source: str # --------------------------------------------------------------------------- # Form 4 Insider Transactions # --------------------------------------------------------------------------- class Form4Transaction(BaseModel): symbol: str filing_date: str transaction_date: str | None = None owner_cik: str | None = None owner_name: str | None = None owner_relationship: str | None = None is_officer: bool = False is_director: bool = False is_ten_percent_owner: bool = False is_ceo: bool = False is_cfo: bool = False is_c_suite: bool = False transaction_code: str | None = None transaction_type: str | None = None shares: float = 0.0 price: float = 0.0 total_value: float = 0.0 shares_owned_following: float | None = None purchase_pct_of_holding: float | None = None accession_number: str | None = None class Form4AggregateResult(BaseModel): symbol: str as_of: str window_days: int buy_count: int = 0 buy_dollar_total: float = 0.0 cluster_size: int = 0 csuite_count: int = 0 avg_pct_of_holding: float | None = None recency_days: int = 0 # --------------------------------------------------------------------------- # SC 13D/13G Activist Ownership # --------------------------------------------------------------------------- class ActivistEvent(BaseModel): symbol: str filing_date: str filer_name: str | None = None filer_cik: str | None = None form_type: str | None = None ownership_pct: float | None = None shares_owned: float | None = None is_amendment: bool = False change_pct: float | None = None accession_number: str | None = None parse_status: str | None = None class DividendUpcomingResponse(BaseModel): dividends: list[DividendCalendarEntry] = Field(default_factory=list) total_count: int = 0 metadata: dict[str, Any] = Field(default_factory=dict) class DividendHistoryResponse(BaseModel): symbol: str dividends: list[DividendCalendarEntry] = Field(default_factory=list) total_count: int = 0 annual_yield_estimate: float | None = None metadata: dict[str, Any] = Field(default_factory=dict) class DividendIngestResponse(BaseModel): symbols_processed: int total_records_upserted: int failed_symbols: list[str] = Field(default_factory=list) status: str metadata: dict[str, Any] = Field(default_factory=dict) # --------------------------------------------------------------------------- # Screener # --------------------------------------------------------------------------- class ScreenerStock(BaseModel): symbol: str name: str | None = None exchange: str | None = None market_cap: float | None = None avg_volume: float | None = None class ScreenerResponse(BaseModel): stocks: list[ScreenerStock] = Field(default_factory=list) total: int = 0 page: int = 1 page_size: int = 250 # --------------------------------------------------------------------------- # Attention # --------------------------------------------------------------------------- class EntityInfo(BaseModel): ticker: str canonical_name: str wiki_title: str | None = None gdelt_query: str | None = None aliases: list[str] = Field(default_factory=list) resolver_confidence: float = 0.0 is_manual_override: bool = False class EntityResolveResponse(BaseModel): ticker: str entity: EntityInfo status: str message: str class WikiFeatures(BaseModel): views: int | None = None baseline_10d: float | None = None spike_10d: float | None = None zscore_20d: float | None = None class NewsFeatures(BaseModel): article_count_1d: int = 0 article_count_3d: int = 0 unique_domains_3d: int = 0 us_article_count_3d: int = 0 gdelt_status: str = "not_collected" class EventAttentionResponse(BaseModel): ticker: str event_date: str entity: EntityInfo wiki: WikiFeatures news: NewsFeatures metadata: dict[str, Any] = Field(default_factory=dict) class CollectionStatusResponse(BaseModel): ticker: str source: str records_collected: int date_range: dict[str, Any] = Field(default_factory=dict) status: str