"""Shared data models. Every module talks to the others through these types only.""" from __future__ import annotations from datetime import date, datetime from typing import Literal from pydantic import BaseModel, Field class Quote(BaseModel): symbol: str name: str group: str close: float | None = None prev_close: float | None = None change_pct: float | None = None # (close/prev_close - 1) * 100 change_abs: float | None = None # close - prev_close (used for rates: yield points) day_high: float | None = None day_low: float | None = None volume: float | None = None avg_volume_10d: float | None = None after_hours_price: float | None = None after_hours_change_pct: float | None = None # vs regular close as_of: datetime | None = None source: str = "yfinance" class NewsItem(BaseModel): title: str summary: str = "" link: str source: str published: datetime class EconEvent(BaseModel): name: str time: datetime | None = None country: str = "US" actual: str | None = None forecast: str | None = None previous: str | None = None importance: Literal["high", "medium", "low"] = "medium" kind: Literal["released", "upcoming"] = "released" class EarningsItem(BaseModel): symbol: str name: str report_date: date timing: Literal["bmo", "amc", "unknown"] = "unknown" eps_actual: float | None = None eps_estimate: float | None = None revenue_actual: float | None = None revenue_estimate: float | None = None class MarketPayload(BaseModel): """Everything collected for one briefing, before LLM summarisation.""" session_date: date # US trading date that just closed us_close_time: datetime # tz-aware (America/New_York) generated_at: datetime # tz-aware (Asia/Seoul) krx_date: date # the Korean trading date this briefing is for krx_open_today: bool quotes: list[Quote] = Field(default_factory=list) news: list[NewsItem] = Field(default_factory=list) events: list[EconEvent] = Field(default_factory=list) earnings: list[EarningsItem] = Field(default_factory=list) history_notes: list[str] = Field(default_factory=list) # e.g. "S&P 500 3일 연속 하락" data_gaps: list[str] = Field(default_factory=list) # collectors that failed / partial def quotes_by_group(self) -> dict[str, list[Quote]]: out: dict[str, list[Quote]] = {} for q in self.quotes: out.setdefault(q.group, []).append(q) return out class Report(BaseModel): """LLM output schema. Used verbatim as the JSON Schema for structured output.""" headline: str = Field(description="한 줄 총평 (40자 이내)") market_overview: str = Field(description="지수·섹터 흐름 3~5문장") key_drivers: list[str] = Field(description="상승/하락 요인 3~6개, 각 한 문장") rates_fx_commodities: str = Field(description="금리·달러·원화·유가·금 흐름 2~4문장") korea_implications: list[str] = Field(description="한국 장 시사점 3~6개 (반도체/SOX, 환율, ADR, EWY 등)") watchlist_today: list[str] = Field(description="오늘 한국 장에서 확인할 것 2~5개") risk_flags: list[str] = Field(description="주의사항 1~4개") data_gaps: list[str] = Field(default_factory=list, description="수집 실패로 반영하지 못한 항목") class BriefingResult(BaseModel): payload: MarketPayload report: Report | None markdown: str model_used: str | None = None degraded: bool = False # True when LLM failed and only the numeric table was produced error: str | None = None