Files
US2KR/us2kr/models.py
T
2026-08-31 14:33:53 +09:00

101 lines
3.6 KiB
Python

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