"""因子目录用例:注册表投影 + 参数化实例的新建/停用(M7.1 起,2026-10 参数化)。 ## 目录与代码的分工(这是本模块的核心规矩) - **能不能算** = 代码注册表(`quant/factors.py`)唯一决定。库里的行只要能解析出 `(模板, 参数)` 就算得出来;解析不出来的行(历史手工登记)保留但在目录里标 `resolvable=False`,引用时抛 `FactorError`(不假装支持)。 - **有哪些因子** = 目录(DB)。内置实例由代码投影进来;**参数化实例**(如 `momentum(window=90,direction=higher_is_better)`)由人在目录里创建 —— 参数写在名字里, 所以「同一个模板的多个参数版本」天然并存,且任何一个都冻结了自己的口径。 - **口径文案**(description/formula/brief/frequency/lookback/direction/requires)永远按 代码收敛:只要这行算得出来,它的文案就是引擎的文案。手改会被改回 —— 因为 「文档写一套、代码跑另一套」是本仓库明令禁止的;要改口径就改代码。 - **唯一人配的字段**是 `enabled`(是否出现在因子下拉里):只对参数化实例生效; 内置实例的开关同样由代码收敛(恒 True)。停用不影响已引用它的策略/归档解析 —— 历史口径不能被一个开关改义。 稳态(目录 == 代码投影)零写入,不会每次 GET 都刷库。 """ from __future__ import annotations from collections.abc import Mapping from typing import Any from app.domain.entities.factor import FactorDefinition from app.domain.repositories.factor import FactorRepository from app.quant.factors import ( FactorError, build_factor_def, canonical_key, get_template, is_resolvable, list_factors, resolve_factor, ) # 「引擎口径」字段:一旦与代码不一致,目录就在撒谎,必须按代码改回。 # enabled 也在其中 —— 但它只对**注册表实例**收敛(见 sync_registry_factors)。 REGISTRY_FIELDS = ( "description", "formula", "brief", "frequency", "lookback", "direction", "requires", "enabled", ) # 参数化实例同样要收敛的字段(不含 enabled:开关是人配的)。 _ENGINE_FIELDS = tuple(f for f in REGISTRY_FIELDS if f != "enabled") def _registry_names() -> set[str]: return {d.name for d in list_factors()} def _wanted_rows(current: list[FactorDefinition]) -> list[FactorDefinition]: """应然状态:内置实例按代码;能解析出来的参数化实例口径也按代码(开关保留)。""" registry = _registry_names() wanted = [FactorDefinition.from_registry_def(d) for d in list_factors()] for row in current: if row.name in registry: continue # 内置实例已由上面的投影覆盖 try: defn, _fn = resolve_factor(row.name) except FactorError: continue # 算不出来的手登记行:不碰(只有人写的备注,没有可收敛的引擎口径) want = FactorDefinition.from_factor_def(defn, enabled=row.enabled) want = want.model_copy(update={"created_at": row.created_at, "version": row.version}) wanted.append(want) return wanted def _differs(current: FactorDefinition | None, want: FactorDefinition, fields) -> bool: if current is None: return True return any(getattr(current, name) != getattr(want, name) for name in fields) def sync_registry_factors(repo: FactorRepository, session) -> int: """把代码投影进目录(幂等);返回本次写入的行数,稳态为 0。""" current = repo.list() by_name = {f.name: f for f in current} registry = _registry_names() stale: list[FactorDefinition] = [] for want in _wanted_rows(current): row = by_name.get(want.name) fields = REGISTRY_FIELDS if want.name in registry else _ENGINE_FIELDS if row is None or _differs(row, want, fields): stale.append(want) if not stale: return 0 n = repo.upsert_many(stale) session.commit() return n def create_parameterized_factor( repo: FactorRepository, session, *, template: str, params: Mapping[str, Any] | None = None, ) -> FactorDefinition: """从模板 + 参数创建一个**新的参数化因子实例**(参数写在名字里,冻结口径)。 参数缺省项取模板默认值;越界/未知参数/重复的参数组合一律报错(不静默纠正)。 """ tpl = get_template(template) # 未知模板 → FactorError key = canonical_key(tpl, params or {}) # 越界/未知参数 → FactorError if repo.get(key) is not None: raise ValueError(f"该参数组合的因子已存在:{key}(参数相同不会重复创建)") defn = build_factor_def(tpl, params or {}, name=key, source="custom") entity = FactorDefinition.from_factor_def(defn, enabled=True) repo.upsert_many([entity]) session.commit() return entity def set_factor_enabled( repo: FactorRepository, session, *, name: str, enabled: bool, ) -> FactorDefinition: """启用/停用目录里的因子(只影响「能不能被选中」,不影响历史解析)。""" if name in _registry_names(): raise ValueError( f"「{name}」是代码注册表里的内置因子,开关由代码决定,不能在目录里停用;" "如果要一个不同参数的版本,请从模板新建参数化因子。" ) row = repo.get(name) if row is None: raise LookupError(f"因子 {name} 不在目录里") if not is_resolvable(name): raise ValueError(f"因子 {name} 引擎算不出来(未注册模板/参数非法),不能启用或停用") updated = row.model_copy(update={"enabled": enabled}) repo.upsert_many([updated]) session.commit() return updated # 兼容旧名(语义相同:把注册表同步进目录)。 seed_registry_factors = sync_registry_factors