101 lines
3.6 KiB
Python
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
|