"""合成层 v2c(Rolling): 滚动制度因子注册——每年一份权重制度,档案扫描批量挂载. v2c 语义: 权重不再是全期单档案,而是按年滚动——每年一份"制度"档案 weight_profiles/{scope}_roll_{year}.json(数据拟合产出,scope=quant12/all18), 注册为 composite_v2c_{scope}_{year};跑批按年段评估后离线拼接(本模块只管 注册机制,不做拼接). 文件名 → 因子名约定: year 取档案名尾部 4 位数字(20 开头),scope 取 _roll_ 前段;不合约定的文件(v2a/v2b 档案等)不纳入扫描.依赖完全复用 composite_ weighting(v2a)管线: load_profile 校验 + register_composite_from_profile 三源表匹配/0 权项跳过/幂等注册._register_all 先幂等重挂 v2a 依赖链 (v1.1 源+neu+v2a/v2b 默认档案),再逐滚动档案注册;单档案失败 raise 且带 文件名——制度期缺失会让年段拼接静默少一段,宁可失败不静默吞. """ import re from pathlib import Path from . import composite_weighting from .composite_weighting import load_profile, register_composite_from_profile # 滚动档案目录: 相对本文件定位(与代码同仓同版本,不依赖 cwd) _PROFILE_DIR = Path(__file__).parent / "weight_profiles" # 约定名: {scope}_roll_{year}.json(year=尾部 4 位数字,20 开头) _ROLLING_NAME_RE = re.compile(r"^(?P[a-z0-9]+)_roll_(?P20\d{2})\.json$") def _iter_rolling_profiles() -> list[tuple[str, Path]]: """扫描 weight_profiles/ 下约定名滚动档案,返回 (因子名, 路径) 列表. 排序稳定 (scope 字典序, year 升序);非约定名文件(v2a/v2b 档案/非 json) 不纳入——因子名排序与 (scope,year) 元组序在 scope 互为前缀时会分叉, 故显式按元组排,不依赖名字串序. """ entries: list[tuple[str, str, Path]] = [] for path in _PROFILE_DIR.iterdir(): m = _ROLLING_NAME_RE.match(path.name) if m is not None: entries.append((m["scope"], m["year"], path)) entries.sort(key=lambda e: (e[0], e[1])) return [(f"composite_v2c_{scope}_{year}", path) for scope, year, path in entries] def _register_all() -> None: """幂等注册全部滚动制度因子(先幂等重挂 v2a 依赖链,再逐滚动档案注册). 单档案加载/注册失败 raise 且带文件名(防静默丢制度期);registry 被清后 可独立重建(v2a 的 _register_all 幂等重挂 v1.1 源+neu+默认档案). """ composite_weighting._register_all() for name, path in _iter_rolling_profiles(): try: register_composite_from_profile(load_profile(str(path)), name) except Exception as e: raise ValueError(f"滚动制度档案加载/注册失败({path.name}): {e}") from e # 模块导入时自动注册(与 composite_weighting 同模式);失败给可读错误(fail-fast) try: _register_all() except Exception as e: raise ImportError(f"合成层 v2c 滚动制度因子注册失败({_PROFILE_DIR}): {e}") from e