"""市场数据领域实体(Phase 1)。 约定(AGENT.md §8/§9): - 行情时间用 trade_date;财务数据同时区分 report_date(报告期)与 announce_date(公告日) - 禁止以 report_date 作可见性依据 —— 只允许 announce_date 已过的数据进入研究 - 复权一律通过独立 AdjustFactor 表达,不在此层偷偷改前/后复权口径 """ from __future__ import annotations from datetime import date, datetime from decimal import Decimal from pydantic import BaseModel, ConfigDict, Field # 常见精度:价格 4 位小数;成交量(股) 2 位;金额(元) 2 位 PRICE_PLACES = Decimal("0.0001") AMOUNT_PLACES = Decimal("0.01") class Stock(BaseModel): """A 股基础信息。symbol 统一为 Tushare 风格,如 600519.SH。""" model_config = ConfigDict(str_strip_whitespace=True) symbol: str = Field(pattern=r"^\d{6}\.(SH|SZ|BJ)$", description="如 600519.SH") name: str industry: str | None = None area: str | None = None market: str | None = Field(default=None, description="主板/创业板/科创板/北交所") exchange: str | None = None list_date: date delist_date: date | None = None status: str = Field(default="L", description="L 上市 / D 退市 / P 暂停") class TradingCalendar(BaseModel): """交易日历。""" calendar_date: date is_open: bool = True class DailyBar(BaseModel): """日线。默认不复权(source=tushare, adjust=none)。 备用源兜底行会标记 source=sina、adjust=qfq(新浪返回前复权价)。 字段统一、可区分、可追溯(AGENT §5.2/§8):研究侧应优先消费 source=tushare 且 adjust=none 的行;新浪行仅在 Tushare 不可用期间作为兜底, Tushare 恢复后重跑 --resume 会按日覆盖回不复权口径。 """ symbol: str trade_date: date source: str = Field(default="tushare", description="tushare | sina") adjust: str = Field(default="none", description="none 不复权 | qfq 前复权") open: Decimal | None = None high: Decimal | None = None low: Decimal | None = None close: Decimal | None = None volume: Decimal | None = Field(default=None, description="成交量(股)") amount: Decimal | None = Field(default=None, description="成交额(元)") @property def is_complete(self) -> bool: """基础行情字段是否齐全(供校验器使用)。""" return all( v is not None for v in (self.open, self.high, self.low, self.close, self.volume, self.amount) ) class AdjustFactor(BaseModel): """复权因子。因子原始口径由数据源决定,必须与数据源文档一致地存取。""" symbol: str trade_date: date factor: Decimal class FinancialIndicator(BaseModel): """核心财务指标(快照)。 可见性红线:研究侧查询一律按 announce_date <= as_of_date 过滤, report_date 只表示报告所属期间,不代表公开时间。 source 标记数据来源:tushare(首选,字段全)| sina(兜底,字段 可能不全——新浪关键指标只含 eps/roe/gross_margin 等少数项)。 新浪兜底行只在「该股票本地历史与新浪重叠部分两边一致」通过校验后 才导入(见 application/services/data_sync.py),且只补本地缺失键。 研究侧对同一报告期应优先消费 source=tushare 的行。 """ symbol: str report_date: date announce_date: date source: str = Field(default="tushare", description="tushare | sina") eps: Decimal | None = None roe: Decimal | None = None total_revenue: Decimal | None = None net_profit: Decimal | None = None gross_margin: Decimal | None = None def announced_by(self, as_of_date: date) -> bool: """as_of_date(含当日)是否已可见。防未来函数的核心判断。""" return self.announce_date <= as_of_date class SyncLog(BaseModel): """数据拉取审计记录(AGENT.md §7:来源必须可追踪,禁止静默切换)。""" source: str api: str request_time: datetime = Field(default_factory=datetime.utcnow) success: bool failure_reason: str | None = None row_count: int = 0 data_start: date | None = None data_end: date | None = None