You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

386 lines
9.9 KiB
Python

"""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