"""选股策略领域实体(2026-09 重构:原 StrategyDefinition → SelectionStrategy)。 策略库现在**只存选股条件组合**:股票池 + 因子 + 过滤条件。 资金 / 持仓数 / 持仓时间 / 调仓时机 / 费率 / 回测区间一律移到「回测组合」 (BacktestCombo)与「公共配置」(GlobalConfig),在回测时才确定。 仍映射到 `strategy` 表(id/name/description/spec_type/config_json/version/created_at), config_json 只存 universe/factors/conditions —— 迁移会把旧行的多余键剥掉。 """ from __future__ import annotations from datetime import datetime from pydantic import BaseModel, ConfigDict, Field, model_validator from app.domain.entities.research import ConditionSpec, FactorSpec, UniverseSpec class SelectionStrategy(BaseModel): """一个选股策略 = 选股条件组合(不含任何回测执行参数)。 `extra="forbid"`(2026-09 收尾):请求里若混入旧版的回测执行参数(selection / rebalance / costs / portfolio / initial_capital / period …),一律**报错**而不是 静默丢弃 —— 否则调用方会以为「在策略上设了费率/调仓」,实际服务端根本没存 (AGENT.md 禁止静默降级与假装支持)。回测参数的正确位置是 BacktestCombo + GlobalConfig。 兼容性:历史行残留的旧键由仓储 `_to_entity` 在**读出前**剔除,因此不受 forbid 影响。 """ model_config = ConfigDict(extra="forbid") id: str = "" name: str = Field(min_length=1, max_length=64) description: str = "" # 取值域收敛为 selection:本实体就是「选股策略」。历史 DB 列里的 "backtest" # 不会被读出(仓储 _to_entity 丢弃该键并回落到默认值),故收紧不会破坏旧数据。 spec_type: str = Field(default="selection", pattern="^selection$") universe: UniverseSpec = UniverseSpec() factors: list[FactorSpec] = Field(min_length=1, description="打分因子(至少 1 个)") conditions: list[ConditionSpec] = Field( default_factory=list, description="过滤条件(AND,universe 之后、因子排序之前)" ) version: str = "1" created_at: datetime | None = None @model_validator(mode="after") def _no_duplicate_factors(self) -> SelectionStrategy: names = [f.name for f in self.factors] if len(set(names)) != len(names): raise ValueError("factors 存在重复因子名") return self # 兼容别名:重构前到处引用的旧名字。新代码请用 SelectionStrategy; # 保留别名是为了让尚未迁移的导入点(agent 工具等)在过渡期不炸, # 最终会全部替换掉(见各调用点的 TODO)。 StrategyDefinition = SelectionStrategy