"""基金行情公共契约。内部使用稳定英文字段,不暴露供应商字段名。""" from datetime import date, datetime from decimal import Decimal from typing import Literal from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator class FundQuoteQuery(BaseModel): model_config = ConfigDict(extra="forbid", frozen=True) fund_codes: tuple[str, ...] = () fund_type: str | None = None target_date: date | None = None limit: int = Field(default=50, ge=1, le=1000) @field_validator("fund_codes") @classmethod def validate_codes(cls, value: tuple[str, ...]) -> tuple[str, ...]: for code in value: if len(code) != 6 or not code.isdigit(): raise ValueError("fund code must be a six-digit number") return tuple(dict.fromkeys(value)) class FundQuote(BaseModel): model_config = ConfigDict(extra="forbid", frozen=True) fund_code: str fund_name: str fund_type: str | None = None nav: Decimal | None = None nav_date: date | None = None daily_change: Decimal | None = None half_year_return: Decimal | None = None one_year_return: Decimal | None = None year_to_date_return: Decimal | None = None since_inception_return: Decimal | None = None quote_time: datetime quote_source: Literal["eastmoney", "cache", "degraded"] is_intraday: bool degraded: bool = False class FundTrendQuery(BaseModel): """`E6` 行情走势出口的入参(`W27` / `D3.9` §4.2)。 **只收实体,不收问句**:`fund_code` 与 `fund_name` 至少给一个 —— 由 Agent 侧 「表层判定层」抽好再传,工具自己不做自然语言理解。这样"抽错实体"与"查不到数据" 是两类可分开归因、分开回归的失败。 """ model_config = ConfigDict(extra="forbid", frozen=True) #: 6 位场内代码(最确定的实体) fund_code: str | None = None #: 产品名(需在 `fin_product.product_name` 里**唯一**命中,否则判 `ambiguous`) fund_name: str | None = None #: 取多少个**净值日**(不是自然日,见 `fund_trend_service` 的口径说明) days: int = Field(default=120, ge=5, le=365) @field_validator("fund_code") @classmethod def validate_code(cls, value: str | None) -> str | None: if value is None: return None text = value.strip() if len(text) != 6 or not text.isdigit(): raise ValueError("fund code must be a six-digit number") return text @model_validator(mode="after") def require_entity(self) -> "FundTrendQuery": if not (self.fund_code or (self.fund_name or "").strip()): raise ValueError("fund_code or fund_name is required") return self