Files
qlib/backend/app/domain/entities/condition_field.py
T
Simon 2e90f3eeac feat(backend): 字段库(condition_field)+ 因子参数化(模板/受控参数)+ 单位换算底座
字段库(本次新增的表与接口):
- `condition_field` 表 + `/api/condition-fields`:中文名/说明可编辑、可停用;
  `kind`/单位阶梯/`base_unit` 由代码注册表收敛(改类型 422,伪字段 422,
  越界单位 422),停用的字段不再进条件下拉,但既有策略仍按名字解析。
- 说明书里的数值条件按字段注册表补**基准单位**后缀(字段间比较不加,不猜单位)。

因子参数化(键即身份,冻结口径):
- 模板 + 参数注册表(`quant/factors.py`):`ParamSpec`(类型/范围/枚举/默认值/说明)+
  `FactorTemplate`(公式/依赖列/参数);规范键把**全部**参数写进名字,如
  `momentum(window=90,direction=lower_is_better)`,所以改参数 = 新建一个身份,
  旧因子/既有策略/已归档实验都不变义;`momentum(window=90)`(缺参数)明确拒绝 ——
  缺项要靠模板默认值补齐,而默认值是可改的代码细节,一旦改动会追溯性改义。
- 参数只在受控范围内取值(窗口 2~500、方向二选一),越界/未知模板/多给参数一律 422
  并列出允许范围,不静默截断、不悄悄取默认值;内置实例的启用开关由代码决定(422)。
- `/api/factors` 暴露 `template`/`params`/`param_specs`/`label`/`source`/`enabled`/
  `resolvable`;新增 `/api/factors/templates`、`POST /api/factors`、`PATCH /api/factors`;
  `get_factor = resolve_factor` 兼容全部旧调用点,参数化键也是一等条件字段。
- 迁移链:c5d6(存量策略陈旧说明重算)→ d6e7(condition_field)→ a7c1
  (factor_definition.enabled + name varchar(128))。

测试:新增 test_condition_fields.py / test_factor_params.py;全量 pytest 500 passed。
2026-10-01 16:33:32 +08:00

54 lines
2.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""字段库领域实体(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 对字符串的 >/≥/</≤ 一律返回 False,
# 若前端把「行业 > 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))