"""字段库领域实体(2026-10:过滤条件字段目录 DB 化)。 与因子目录(``FactorDefinition``)同一套思路: - **DB 是字段库的契约源**:中文名、含义、单位、是否启用、自定义条目都入库; - **引擎是字段可用性的唯一事实来源**:字段能不能算由 ``quant.condition_fields`` 对着引擎域校验,登记不出来的字段一律拒绝(防「建出来永远选不出股票」的伪字段)。 可编辑边界(有意为之): - ``name`` 是引擎字段名,**不可改**(改了就指向另一个字段,等于换字段); - ``kind`` 由引擎类型决定,**不可改**(字符串字段不能比大小); - ``label`` / ``description`` / ``group_name`` / ``unit`` / ``enabled`` 可编辑 —— 内置字段也允许改文案(seed「只补不删」,不会覆盖用户的措辞)。 """ from __future__ import annotations from datetime import datetime from pydantic import BaseModel, ConfigDict, Field, computed_field FIELD_KINDS: tuple[str, ...] = ("num", "str") FIELD_SOURCES: tuple[str, ...] = ("builtin", "custom") # 类型 → 可用比较符(单一事实来源)。 # 字符串字段只能等值/集合:引擎 _compare 对字符串的 >/≥/ 5」这类选项摆出来,用户点出来的就是永远为假的条件。 OPS_NUM: tuple[str, ...] = ("gt", "gte", "lt", "lte", "eq", "ne") OPS_STR: tuple[str, ...] = ("eq", "ne", "in", "not_in") OPS_BY_KIND: dict[str, tuple[str, ...]] = {"num": OPS_NUM, "str": OPS_STR} class ConditionField(BaseModel): """字段库中的一个条件字段。""" model_config = ConfigDict(extra="forbid") name: str = Field(min_length=1, max_length=64, description="引擎字段名,如 dv_ratio / static.industry") label: str = Field(default="", max_length=64, description="中文名(下拉里展示)") description: str = Field(default="", max_length=500, description="含义 / 口径(含单位)") kind: str = Field(default="num", pattern="^(num|str)$") group_name: str = Field(default="行情", max_length=32) unit: str = Field(default="", max_length=16) source: str = Field(default="builtin", pattern="^(builtin|custom)$") enabled: bool = Field(default=True, description="False=从选择器隐藏(内置不可删除,只能停用)") sort_order: int = 100 created_at: datetime | None = None updated_at: datetime | None = None @computed_field # type: ignore[prop-decorator] @property def ops(self) -> list[str]: """该字段可用的比较符(前端据此收窄下拉,不自己猜)。""" return list(OPS_BY_KIND.get(self.kind, OPS_NUM))