mirror of
https://github.com/shawnkim1997/All-in-one-Financial-Analysis.git
synced 2026-08-19 13:28:07 +00:00
- /daily-news route fetches FT ePaper headlines with Gemini-backed Korean translation, plus calendar selector and cached translation TTL - /api/search and use-ticker-search wire async sidebar search through a unified backend that merges static aliases with the local KOSPI/KOSDAQ universe (162 names) and yfinance metadata - ticker-alias gains Korean display names, currency and market hints; PeerComparison formats KRW/JPY with locale-aware zero decimals - EquityOverview surfaces HQ city/country, with Korean Naver snapshot fallback when yfinance info is empty - market_data adds /korean-universe/search for autocomplete - New tests for FT ingestion, Korean universe lookups, and updated smoke prefixes
436 lines
12 KiB
Python
436 lines
12 KiB
Python
"""Pydantic request/response schemas for ATLAS Terminal API."""
|
|
|
|
from pydantic import BaseModel, Field
|
|
from typing import Any, Dict, List, Literal, Optional
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Request models
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TickerRequest(BaseModel):
|
|
"""Generic request carrying a ticker and optional market selector."""
|
|
ticker: str = Field(..., description="Stock ticker symbol, e.g. AAPL, 005930.KS")
|
|
market: str = Field(
|
|
default="US (S&P/Dow/Nasdaq)",
|
|
description="Market selector: US, South Korea (KOSPI/KOSDAQ), Japan (Nikkei), UK (LSE)",
|
|
)
|
|
|
|
|
|
class EdgarRequest(BaseModel):
|
|
"""Request to download / fetch SEC EDGAR 10-K filings."""
|
|
ticker: str = Field(..., description="Stock ticker symbol")
|
|
email: str = Field(..., description="Email address required by SEC EDGAR fair-access policy")
|
|
|
|
|
|
class AnalysisRequest(BaseModel):
|
|
"""Request for AI-powered 10-K analysis (Gemini)."""
|
|
ticker: str
|
|
api_key: str = Field(..., description="Google Gemini API key")
|
|
sector: str = ""
|
|
industry: str = ""
|
|
|
|
|
|
class DCFInputs(BaseModel):
|
|
"""Inputs for discounted cash-flow valuation."""
|
|
fcf: float = Field(..., description="Base free cash flow (trailing)")
|
|
wacc: float = Field(..., description="Weighted-average cost of capital (decimal, e.g. 0.10)")
|
|
terminal_growth: float = Field(..., description="Terminal growth rate (decimal, e.g. 0.025)")
|
|
fcf_growth: float = Field(..., description="Near-term FCF growth rate (decimal, e.g. 0.12)")
|
|
total_debt: float = Field(default=0, description="Total debt for bridge to equity value")
|
|
cash: float = Field(default=0, description="Cash & equivalents for bridge to equity value")
|
|
shares: float = Field(default=1, description="Shares outstanding for per-share value")
|
|
|
|
|
|
class CompanySearch(BaseModel):
|
|
"""Search for a company by name or partial ticker."""
|
|
query: str = Field(..., description="Search term, e.g. 'Apple', 'Samsung'")
|
|
market: str = ""
|
|
|
|
|
|
class CompsRequest(BaseModel):
|
|
"""Request for industry comparable companies data."""
|
|
tickers: List[str] = Field(..., description="List of ticker symbols to compare")
|
|
|
|
|
|
class ForensicRequest(BaseModel):
|
|
"""Request for forensic audit analysis (Item 3 & 9A)."""
|
|
ticker: str
|
|
api_key: str
|
|
item3: str = ""
|
|
item9a: str = ""
|
|
|
|
|
|
class FinancialsLLMRequest(BaseModel):
|
|
"""Request to extract financials from Item 8 via LLM."""
|
|
ticker: str
|
|
api_key: str
|
|
item8_text: str = ""
|
|
|
|
|
|
class PortfolioPositionCreate(BaseModel):
|
|
"""Create a new portfolio position."""
|
|
ticker: str
|
|
company_name: str = ""
|
|
quantity: float
|
|
avg_price: float
|
|
currency: str = "USD"
|
|
exchange: str = ""
|
|
source: str = "manual"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Response models
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class DCFResult(BaseModel):
|
|
"""Result of a DCF valuation calculation."""
|
|
enterprise_value: float = 0
|
|
equity_value: float = 0
|
|
value_per_share: Optional[float] = None
|
|
shares: Optional[float] = None
|
|
scenarios: Dict[str, Any] = {}
|
|
|
|
|
|
class DCFInputsResponse(BaseModel):
|
|
"""Auto-filled DCF inputs from market data."""
|
|
fcf: Optional[float] = None
|
|
total_debt: float = 0
|
|
cash: float = 0
|
|
shares: Optional[float] = None
|
|
|
|
|
|
class SmartDefaultsResponse(BaseModel):
|
|
"""Smart defaults for DCF with analyst consensus guidance."""
|
|
wacc: float = 0.10
|
|
terminal_growth: float = 0.025
|
|
fcf_growth: float = 0.10
|
|
sector: str = "N/A"
|
|
industry: str = "N/A"
|
|
|
|
|
|
class ConsensusResponse(BaseModel):
|
|
"""Analyst consensus data for a ticker."""
|
|
target_mean: Optional[float] = None
|
|
target_median: Optional[float] = None
|
|
target_low: Optional[float] = None
|
|
target_high: Optional[float] = None
|
|
recommendation: str = ""
|
|
num_analysts: int = 0
|
|
data: Dict[str, Any] = {}
|
|
|
|
|
|
class FTHeadline(BaseModel):
|
|
"""Translated FT headline card for the daily news view."""
|
|
url: str
|
|
title_en: str
|
|
title_ko: Optional[str] = None
|
|
lede_en: Optional[str] = None
|
|
lede_ko: Optional[str] = None
|
|
section: Optional[str] = None
|
|
published_at: str
|
|
image: Optional[str] = None
|
|
|
|
|
|
class SectorIndustryResponse(BaseModel):
|
|
"""Sector and industry classification."""
|
|
sector: str = "N/A"
|
|
industry: str = "N/A"
|
|
|
|
|
|
class FinancialHealth(BaseModel):
|
|
"""Comprehensive financial health metrics."""
|
|
dupont: Dict[str, Any] = {}
|
|
altman_z: Dict[str, Any] = {}
|
|
red_flags: List[str] = []
|
|
piotroski: Dict[str, Any] = {}
|
|
|
|
|
|
class PiotroskiResponse(BaseModel):
|
|
"""Piotroski F-Score breakdown."""
|
|
score: int = 0
|
|
criteria: List[Dict[str, Any]] = []
|
|
used_ttm: bool = False
|
|
|
|
|
|
class FScoreYearPoint(BaseModel):
|
|
"""Single fiscal year pass/fail for one Piotroski criterion."""
|
|
year: int
|
|
pass_flag: bool
|
|
|
|
|
|
class FScoreCriterionSeries(BaseModel):
|
|
"""Three-year (or shorter) history for one F-Score criterion."""
|
|
key: str
|
|
label: str
|
|
history: List[FScoreYearPoint] = Field(default_factory=list)
|
|
|
|
|
|
class DuPontTreeNode(BaseModel):
|
|
"""One node in the DuPont ROE tree."""
|
|
id: str
|
|
label: str
|
|
value: float
|
|
unit: str = "pct"
|
|
avg_5y: Optional[float] = None
|
|
vs_5y_avg_pct: Optional[float] = None
|
|
trend: str = "flat"
|
|
|
|
|
|
class DuPontTreePayload(BaseModel):
|
|
"""ROE root with NPM, asset turnover, equity multiplier children."""
|
|
root: DuPontTreeNode
|
|
npm: DuPontTreeNode
|
|
asset_turnover: DuPontTreeNode
|
|
equity_mult: DuPontTreeNode
|
|
|
|
|
|
class SankeyNivoNode(BaseModel):
|
|
"""Node for @nivo/sankey."""
|
|
id: str
|
|
label: Optional[str] = None
|
|
|
|
|
|
class SankeyNivoLink(BaseModel):
|
|
"""Directed link for @nivo/sankey."""
|
|
source: str
|
|
target: str
|
|
value: float
|
|
|
|
|
|
class SankeyGraphPayload(BaseModel):
|
|
"""Sankey graph in Nivo-friendly shape."""
|
|
nodes: List[SankeyNivoNode] = Field(default_factory=list)
|
|
links: List[SankeyNivoLink] = Field(default_factory=list)
|
|
|
|
|
|
class WaterfallStep(BaseModel):
|
|
"""One bar in operating-income bridge chart."""
|
|
id: str
|
|
label: str
|
|
value: float
|
|
cumulative: float
|
|
step_type: str = "relative"
|
|
|
|
|
|
class FinancialAnomalyItem(BaseModel):
|
|
"""YoY line-item spike/drop for UI chips."""
|
|
account_key: str
|
|
display_name: str
|
|
prior_value: Optional[float] = None
|
|
current_value: Optional[float] = None
|
|
change_pct: Optional[float] = None
|
|
direction: str = "up"
|
|
|
|
|
|
class ResearchDashboardResponse(BaseModel):
|
|
"""Full research deep-dive payload (quant only)."""
|
|
ticker: str
|
|
fscore_total: int = 0
|
|
fscore_criteria: List[FScoreCriterionSeries] = Field(default_factory=list)
|
|
dupont_tree: Optional[DuPontTreePayload] = None
|
|
sankey: SankeyGraphPayload = Field(default_factory=SankeyGraphPayload)
|
|
waterfall: List[WaterfallStep] = Field(default_factory=list)
|
|
anomalies: List[FinancialAnomalyItem] = Field(default_factory=list)
|
|
error: Optional[str] = None
|
|
|
|
|
|
class AnomalyExplainRequest(BaseModel):
|
|
"""Request Gemini to explain a flagged line item using filing text."""
|
|
ticker: str
|
|
api_key: str
|
|
sec_email: str = Field(default="", description="SEC fair-access email; else SEC_EDGAR_EMAIL env")
|
|
account_key: str = ""
|
|
display_name: str = ""
|
|
direction: str = "up"
|
|
magnitude_pct: float = 0.0
|
|
filing_focus: str = Field(default="10k_mda", description="10k_mda or risk")
|
|
|
|
|
|
class AnomalyExplainResponse(BaseModel):
|
|
"""Strict JSON-shaped explanation from LLM."""
|
|
summary: str = ""
|
|
likely_causes: List[str] = Field(default_factory=list)
|
|
citations: List[Dict[str, str]] = Field(default_factory=list)
|
|
confidence: str = "medium"
|
|
|
|
|
|
class SankeyData(BaseModel):
|
|
"""Income statement Sankey diagram data."""
|
|
labels: List[str] = []
|
|
sources: List[int] = []
|
|
targets: List[int] = []
|
|
values: List[float] = []
|
|
colors: List[str] = []
|
|
|
|
|
|
class RadarMetrics(BaseModel):
|
|
"""Normalised radar chart metrics."""
|
|
labels: List[str] = []
|
|
values: List[float] = []
|
|
raw: Dict[str, Any] = {}
|
|
|
|
|
|
class TrendData(BaseModel):
|
|
"""5-year financial trend data."""
|
|
years: List[int] = []
|
|
revenue: List[Optional[float]] = []
|
|
net_income: List[Optional[float]] = []
|
|
operating_margin: List[Optional[float]] = []
|
|
fcf: List[Optional[float]] = []
|
|
|
|
|
|
class NewsItem(BaseModel):
|
|
"""A single news article."""
|
|
title: str
|
|
source: str = ""
|
|
url: str = ""
|
|
published_at: str = ""
|
|
summary: str = ""
|
|
|
|
|
|
class PortfolioPosition(BaseModel):
|
|
"""A portfolio position with current market data."""
|
|
id: Optional[str] = None
|
|
ticker: str
|
|
company_name: str = ""
|
|
quantity: float
|
|
avg_price: float
|
|
currency: str = "USD"
|
|
exchange: str = ""
|
|
source: str = "manual"
|
|
current_price: Optional[float] = None
|
|
stock_currency: str = ""
|
|
yf_ticker: str = ""
|
|
market_value: Optional[float] = None
|
|
pnl: Optional[float] = None
|
|
pnl_pct: Optional[float] = None
|
|
|
|
|
|
class PortfolioSummary(BaseModel):
|
|
"""Aggregated portfolio summary."""
|
|
total_value: float = 0
|
|
total_cost: float = 0
|
|
total_pnl: float = 0
|
|
total_pnl_pct: Optional[float] = None
|
|
positions: List[PortfolioPosition] = []
|
|
|
|
|
|
class MarketOverview(BaseModel):
|
|
"""Market overview with indices, FX, and crypto."""
|
|
indices: Dict[str, Any] = {}
|
|
fx_rates: Dict[str, float] = {}
|
|
crypto: List[Dict[str, Any]] = []
|
|
|
|
|
|
class FXRateResponse(BaseModel):
|
|
"""Foreign exchange rate response."""
|
|
pair: str
|
|
rate: Optional[float] = None
|
|
rates: Dict[str, float] = {}
|
|
|
|
|
|
class FXHistoryResponse(BaseModel):
|
|
"""FX pair historical data."""
|
|
pair: str
|
|
dates: List[str] = []
|
|
rates: List[float] = []
|
|
|
|
|
|
class CryptoPrice(BaseModel):
|
|
"""Single cryptocurrency price data."""
|
|
symbol: str
|
|
name: str = ""
|
|
price_usd: Optional[float] = None
|
|
price_krw: Optional[float] = None
|
|
change_24h_pct: Optional[float] = None
|
|
|
|
|
|
class EdgarSectionsResponse(BaseModel):
|
|
"""Cached or downloaded filing section texts (SEC, DART, or EDINET)."""
|
|
source: Literal["sec", "dart", "edinet"] = "sec"
|
|
filing_form: Optional[str] = None
|
|
filing_label: Optional[str] = None
|
|
configured: bool = True
|
|
message: Optional[str] = None
|
|
links: Optional[Dict[str, str]] = None
|
|
status: str = ""
|
|
item1a: str = ""
|
|
item3: str = ""
|
|
item7: str = ""
|
|
item8: str = ""
|
|
item9a: str = ""
|
|
# When ``include_html=true``: HTML fragment for native viewer
|
|
html: str = ""
|
|
|
|
|
|
class Item7Response(BaseModel):
|
|
"""Item 7 MD&A text."""
|
|
item7: str = ""
|
|
|
|
|
|
class CompareResponse(BaseModel):
|
|
"""Comparison of latest vs 3-year-ago Item 7."""
|
|
item1a_latest: str = ""
|
|
item7_latest: str = ""
|
|
item7_3y_ago: Optional[str] = None
|
|
has_comparison: bool = False
|
|
|
|
|
|
class HealthCheckResponse(BaseModel):
|
|
"""API health check."""
|
|
status: str = "ok"
|
|
version: str = "1.0.0"
|
|
|
|
|
|
class VideoSubmitRequest(BaseModel):
|
|
"""Request to start transcript extraction for a video or audio source."""
|
|
url: str = Field(..., description="YouTube URL, web URL, or local file path")
|
|
source_type: Literal["youtube", "url", "local"] = "url"
|
|
language: Optional[str] = Field(default=None, description="Optional source language hint for Whisper")
|
|
|
|
|
|
class VideoJob(BaseModel):
|
|
"""Video transcript job metadata."""
|
|
job_id: str
|
|
status: Literal["queued", "fetching", "transcribing", "analyzing", "completed", "failed"]
|
|
source_url: str
|
|
source_type: Literal["youtube", "url", "local"]
|
|
progress: int = 0
|
|
error: Optional[str] = None
|
|
title: Optional[str] = None
|
|
duration_sec: Optional[int] = None
|
|
language: Optional[str] = None
|
|
created_at: str
|
|
completed_at: Optional[str] = None
|
|
|
|
|
|
class VideoTranscript(BaseModel):
|
|
"""Stored transcript text plus analysis output."""
|
|
job_id: str
|
|
text: str
|
|
summary: Optional[str] = None
|
|
keywords: List[str] = Field(default_factory=list)
|
|
topics: List[str] = Field(default_factory=list)
|
|
sentiment: Optional[Literal["positive", "neutral", "negative"]] = None
|
|
intent: Optional[str] = None
|
|
|
|
|
|
class VideoSearchHit(BaseModel):
|
|
"""One transcript full-text search match."""
|
|
job_id: str
|
|
title: Optional[str] = None
|
|
snippet: str
|
|
rank: float
|
|
|
|
|
|
class VideoTranslation(BaseModel):
|
|
"""On-demand translated transcript payload."""
|
|
job_id: str
|
|
target_language: str = "ko"
|
|
summary: Optional[str] = None
|
|
keywords: List[str] = Field(default_factory=list)
|
|
topics: List[str] = Field(default_factory=list)
|
|
intent: Optional[str] = None
|
|
text: str = ""
|