import re from enum import Enum from typing import Any from pydantic import AwareDatetime, BaseModel, Field, field_validator # --- Constants --- SYMBOL_PATTERN = re.compile(r"^[A-Za-z0-9.\-]{1,20}$") # --- Request Models --- class Holding(BaseModel): symbol: str = Field(..., description="Stock symbol, e.g. AAPL or VOLV-B.ST") shares: float = Field(..., gt=0, description="Number of shares held") buy_in_price: float = Field(..., gt=0, description="Average cost basis per share") @field_validator("symbol") @classmethod def validate_symbol(cls, v: str) -> str: if not SYMBOL_PATTERN.match(v): raise ValueError("Invalid symbol format. Use 1-20 alphanumeric chars, dots, or hyphens.") return v.upper() class PortfolioRequest(BaseModel): holdings: list[Holding] = Field(..., min_length=1, max_length=50) # --- Response Models --- class ActionEnum(str, Enum): BUY_MORE = "BUY_MORE" HOLD = "HOLD" SELL = "SELL" class ConfidenceEnum(str, Enum): HIGH = "HIGH" MEDIUM = "MEDIUM" LOW = "LOW" class QuoteResponse(BaseModel): symbol: str name: str | None = None price: float | None = None change: float | None = None change_percent: float | None = None volume: int | None = None market_cap: float | None = None currency: str | None = None class ProfileResponse(BaseModel): symbol: str name: str | None = None sector: str | None = None industry: str | None = None country: str | None = None description: str | None = None website: str | None = None employees: int | None = None class MetricsResponse(BaseModel): symbol: str pe_ratio: float | None = None pb_ratio: float | None = None ps_ratio: float | None = None peg_ratio: float | None = None roe: float | None = None roa: float | None = None dividend_yield: float | None = None beta: float | None = None eps: float | None = None revenue_growth: float | None = None earnings_growth: float | None = None class HistoricalBar(BaseModel): date: str open: float | None = None high: float | None = None low: float | None = None close: float | None = None volume: int | None = None class FinancialsResponse(BaseModel): symbol: str income: list[dict] = Field(default_factory=list) balance: list[dict] = Field(default_factory=list) cash_flow: list[dict] = Field(default_factory=list) class NewsItem(BaseModel): title: str | None = None url: str | None = None date: str | None = None source: str | None = None class SummaryResponse(BaseModel): quote: QuoteResponse | None = None profile: ProfileResponse | None = None metrics: MetricsResponse | None = None financials: FinancialsResponse | None = None class AnalysisResult(BaseModel): action: ActionEnum confidence: ConfidenceEnum reasons: list[str] class HoldingAnalysis(BaseModel): symbol: str current_price: float | None = None buy_in_price: float shares: float pnl: float | None = None pnl_percent: float | None = None metrics: MetricsResponse | None = None target_price: float | None = None analysis: AnalysisResult class PortfolioResponse(BaseModel): holdings: list[HoldingAnalysis] analyzed_at: AwareDatetime class DiscoverItem(BaseModel): symbol: str | None = None name: str | None = None price: float | None = None change_percent: float | None = None volume: int | None = None market_cap: float | None = None class ApiResponse(BaseModel): success: bool = True data: dict[str, Any] | list[Any] | None = None error: str | None = None