feat(factor): 合成层v2c滚动重估——12个年度制度档案(quant12/all18×2021-2026,3年窗拟合+新旧各半平滑)+composite_rolling扫描注册+0权源跳过瘦身(all18_2021十八项降十二项),9新测试全量277绿 [nas]
This commit is contained in:
@@ -4,3 +4,4 @@ from . import alpha_datasets # noqa: F401 挂载 Alpha101/158(导入即注册)
|
||||
from . import fundamental_library # noqa: F401 财务因子 P0 批(导入即注册)
|
||||
from . import composite_library # noqa: F401 合成层 v1(导入即注册,源依赖前三个)
|
||||
from . import composite_weighting # noqa: F401 合成层 v2a(权重档案,导入即注册)
|
||||
from . import composite_rolling # noqa: F401 合成层 v2c(滚动制度,导入即注册)
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
"""合成层 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<scope>[a-z0-9]+)_roll_(?P<year>20\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
|
||||
@@ -116,6 +116,9 @@ def register_composite_from_profile(profile: WeightProfile, name: str) -> None:
|
||||
raise ValueError——先于幂等跳过,漂移不被静默吞掉);项形态按源 category 走
|
||||
rank_term/_embed_term(复用 v1.1 两函数,口径不漂移);源因子必须已注册
|
||||
(get_factor,缺则 raise).
|
||||
|
||||
resolved 后权重为 0 的源项跳过不入式(v2c 滚动档案存在 neu 源全 0 的
|
||||
制度期,0*w 项纯耗求值、cs_neutralize OLS 白算;全零档案 raise ValueError).
|
||||
"""
|
||||
declared = {src: d for src, (d, _w) in profile.sources.items()}
|
||||
matched: list[tuple[str, str]] | None = None
|
||||
@@ -134,9 +137,18 @@ def register_composite_from_profile(profile: WeightProfile, name: str) -> None:
|
||||
if name in _REGISTRY:
|
||||
return
|
||||
|
||||
# 全零档案非法: resolve 归一会除零,先给可读 ValueError(L2 override 也可能压成全 0)
|
||||
effective = {src: w for src, (_d, w) in profile.sources.items()}
|
||||
for ov in profile.overrides:
|
||||
effective[ov["source"]] = float(ov["weight"])
|
||||
if sum(effective.values()) <= 0:
|
||||
raise ValueError(f"档案全部源有效权重为 0(全零档案非法): {name}")
|
||||
|
||||
weights = resolve_weights(profile)
|
||||
terms = []
|
||||
for src_name, direction in matched: # 按匹配源表顺序拼装
|
||||
if weights[src_name] <= 0: # 0 权项跳过不入式(见 docstring)
|
||||
continue
|
||||
src = get_factor(src_name)
|
||||
if src is None:
|
||||
raise ValueError(f"合成源未注册: {src_name}")
|
||||
@@ -145,6 +157,8 @@ def register_composite_from_profile(profile: WeightProfile, name: str) -> None:
|
||||
else:
|
||||
term = composite_library.rank_term(src["expression"], direction)
|
||||
terms.append((term, weights[src_name]))
|
||||
if not terms:
|
||||
raise ValueError(f"档案过滤 0 权后无有效项: {name}")
|
||||
register_factor(name, build_weighted_expression(terms), category="composite")
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"profile_id": "all18-roll-2021",
|
||||
"name": "v2c滚动 all18 2021制度",
|
||||
"method": "icir_fit_rolling3y_smoothed",
|
||||
"fit_window": {
|
||||
"start": "2018-01",
|
||||
"end": "2020-12",
|
||||
"smooth": "新旧各半"
|
||||
},
|
||||
"sources": {
|
||||
"alpha16": {
|
||||
"direction": "+",
|
||||
"weight": 0.130152
|
||||
},
|
||||
"alpha42": {
|
||||
"direction": "+",
|
||||
"weight": 0.033644
|
||||
},
|
||||
"klow": {
|
||||
"direction": "-",
|
||||
"weight": 0.077528
|
||||
},
|
||||
"alpha2": {
|
||||
"direction": "+",
|
||||
"weight": 0.086917
|
||||
},
|
||||
"alpha12": {
|
||||
"direction": "+",
|
||||
"weight": 0.132173
|
||||
},
|
||||
"vol_ma5": {
|
||||
"direction": "-",
|
||||
"weight": 0.060808
|
||||
},
|
||||
"wvma_20": {
|
||||
"direction": "-",
|
||||
"weight": 0.063921
|
||||
},
|
||||
"fund_nsi_neu": {
|
||||
"direction": "+",
|
||||
"weight": 0.0
|
||||
},
|
||||
"vma_60": {
|
||||
"direction": "+",
|
||||
"weight": 0.098737
|
||||
},
|
||||
"fund_gp_over_m_neu": {
|
||||
"direction": "+",
|
||||
"weight": 0.0
|
||||
},
|
||||
"cord_5": {
|
||||
"direction": "-",
|
||||
"weight": 0.104439
|
||||
},
|
||||
"alpha81": {
|
||||
"direction": "-",
|
||||
"weight": 0.06114
|
||||
},
|
||||
"fund_bp_neu": {
|
||||
"direction": "+",
|
||||
"weight": 0.0
|
||||
},
|
||||
"fund_sue_np_neu": {
|
||||
"direction": "+",
|
||||
"weight": 0.0
|
||||
},
|
||||
"fund_gdhs_chg_neu": {
|
||||
"direction": "+",
|
||||
"weight": 0.0
|
||||
},
|
||||
"fund_growth_scissors_neu": {
|
||||
"direction": "+",
|
||||
"weight": 0.0
|
||||
},
|
||||
"alpha83": {
|
||||
"direction": "+",
|
||||
"weight": 0.117903
|
||||
},
|
||||
"kup": {
|
||||
"direction": "-",
|
||||
"weight": 0.032637
|
||||
}
|
||||
},
|
||||
"overrides": [],
|
||||
"created": "2026-09-11"
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"profile_id": "all18-roll-2022",
|
||||
"name": "v2c滚动 all18 2022制度",
|
||||
"method": "icir_fit_rolling3y_smoothed",
|
||||
"fit_window": {
|
||||
"start": "2019-01",
|
||||
"end": "2021-12",
|
||||
"smooth": "新旧各半"
|
||||
},
|
||||
"sources": {
|
||||
"alpha16": {
|
||||
"direction": "+",
|
||||
"weight": 0.13266
|
||||
},
|
||||
"alpha42": {
|
||||
"direction": "+",
|
||||
"weight": 0.029096
|
||||
},
|
||||
"klow": {
|
||||
"direction": "-",
|
||||
"weight": 0.093628
|
||||
},
|
||||
"alpha2": {
|
||||
"direction": "+",
|
||||
"weight": 0.069661
|
||||
},
|
||||
"alpha12": {
|
||||
"direction": "+",
|
||||
"weight": 0.107469
|
||||
},
|
||||
"vol_ma5": {
|
||||
"direction": "-",
|
||||
"weight": 0.068048
|
||||
},
|
||||
"wvma_20": {
|
||||
"direction": "-",
|
||||
"weight": 0.062336
|
||||
},
|
||||
"fund_nsi_neu": {
|
||||
"direction": "+",
|
||||
"weight": 0.017251
|
||||
},
|
||||
"vma_60": {
|
||||
"direction": "+",
|
||||
"weight": 0.08382
|
||||
},
|
||||
"fund_gp_over_m_neu": {
|
||||
"direction": "+",
|
||||
"weight": 0.011238
|
||||
},
|
||||
"cord_5": {
|
||||
"direction": "-",
|
||||
"weight": 0.079221
|
||||
},
|
||||
"alpha81": {
|
||||
"direction": "-",
|
||||
"weight": 0.061465
|
||||
},
|
||||
"fund_bp_neu": {
|
||||
"direction": "+",
|
||||
"weight": 0.013903
|
||||
},
|
||||
"fund_sue_np_neu": {
|
||||
"direction": "+",
|
||||
"weight": 0.0
|
||||
},
|
||||
"fund_gdhs_chg_neu": {
|
||||
"direction": "+",
|
||||
"weight": 0.022258
|
||||
},
|
||||
"fund_growth_scissors_neu": {
|
||||
"direction": "+",
|
||||
"weight": 0.002767
|
||||
},
|
||||
"alpha83": {
|
||||
"direction": "+",
|
||||
"weight": 0.095155
|
||||
},
|
||||
"kup": {
|
||||
"direction": "-",
|
||||
"weight": 0.050024
|
||||
}
|
||||
},
|
||||
"overrides": [],
|
||||
"created": "2026-09-11"
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"profile_id": "all18-roll-2023",
|
||||
"name": "v2c滚动 all18 2023制度",
|
||||
"method": "icir_fit_rolling3y_smoothed",
|
||||
"fit_window": {
|
||||
"start": "2020-01",
|
||||
"end": "2022-12",
|
||||
"smooth": "新旧各半"
|
||||
},
|
||||
"sources": {
|
||||
"alpha16": {
|
||||
"direction": "+",
|
||||
"weight": 0.120745
|
||||
},
|
||||
"alpha42": {
|
||||
"direction": "+",
|
||||
"weight": 0.032799
|
||||
},
|
||||
"klow": {
|
||||
"direction": "-",
|
||||
"weight": 0.098778
|
||||
},
|
||||
"alpha2": {
|
||||
"direction": "+",
|
||||
"weight": 0.050731
|
||||
},
|
||||
"alpha12": {
|
||||
"direction": "+",
|
||||
"weight": 0.094002
|
||||
},
|
||||
"vol_ma5": {
|
||||
"direction": "-",
|
||||
"weight": 0.074204
|
||||
},
|
||||
"wvma_20": {
|
||||
"direction": "-",
|
||||
"weight": 0.044468
|
||||
},
|
||||
"fund_nsi_neu": {
|
||||
"direction": "+",
|
||||
"weight": 0.036712
|
||||
},
|
||||
"vma_60": {
|
||||
"direction": "+",
|
||||
"weight": 0.076656
|
||||
},
|
||||
"fund_gp_over_m_neu": {
|
||||
"direction": "+",
|
||||
"weight": 0.023663
|
||||
},
|
||||
"cord_5": {
|
||||
"direction": "-",
|
||||
"weight": 0.068403
|
||||
},
|
||||
"alpha81": {
|
||||
"direction": "-",
|
||||
"weight": 0.056433
|
||||
},
|
||||
"fund_bp_neu": {
|
||||
"direction": "+",
|
||||
"weight": 0.030378
|
||||
},
|
||||
"fund_sue_np_neu": {
|
||||
"direction": "+",
|
||||
"weight": 0.0
|
||||
},
|
||||
"fund_gdhs_chg_neu": {
|
||||
"direction": "+",
|
||||
"weight": 0.04279
|
||||
},
|
||||
"fund_growth_scissors_neu": {
|
||||
"direction": "+",
|
||||
"weight": 0.002767
|
||||
},
|
||||
"alpha83": {
|
||||
"direction": "+",
|
||||
"weight": 0.077659
|
||||
},
|
||||
"kup": {
|
||||
"direction": "-",
|
||||
"weight": 0.068812
|
||||
}
|
||||
},
|
||||
"overrides": [],
|
||||
"created": "2026-09-11"
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"profile_id": "all18-roll-2024",
|
||||
"name": "v2c滚动 all18 2024制度",
|
||||
"method": "icir_fit_rolling3y_smoothed",
|
||||
"fit_window": {
|
||||
"start": "2021-01",
|
||||
"end": "2023-12",
|
||||
"smooth": "新旧各半"
|
||||
},
|
||||
"sources": {
|
||||
"alpha16": {
|
||||
"direction": "+",
|
||||
"weight": 0.114329
|
||||
},
|
||||
"alpha42": {
|
||||
"direction": "+",
|
||||
"weight": 0.043816
|
||||
},
|
||||
"klow": {
|
||||
"direction": "-",
|
||||
"weight": 0.099951
|
||||
},
|
||||
"alpha2": {
|
||||
"direction": "+",
|
||||
"weight": 0.041571
|
||||
},
|
||||
"alpha12": {
|
||||
"direction": "+",
|
||||
"weight": 0.096394
|
||||
},
|
||||
"vol_ma5": {
|
||||
"direction": "-",
|
||||
"weight": 0.082495
|
||||
},
|
||||
"wvma_20": {
|
||||
"direction": "-",
|
||||
"weight": 0.034207
|
||||
},
|
||||
"fund_nsi_neu": {
|
||||
"direction": "+",
|
||||
"weight": 0.041572
|
||||
},
|
||||
"vma_60": {
|
||||
"direction": "+",
|
||||
"weight": 0.083153
|
||||
},
|
||||
"fund_gp_over_m_neu": {
|
||||
"direction": "+",
|
||||
"weight": 0.02445
|
||||
},
|
||||
"cord_5": {
|
||||
"direction": "-",
|
||||
"weight": 0.069871
|
||||
},
|
||||
"alpha81": {
|
||||
"direction": "-",
|
||||
"weight": 0.052601
|
||||
},
|
||||
"fund_bp_neu": {
|
||||
"direction": "+",
|
||||
"weight": 0.033987
|
||||
},
|
||||
"fund_sue_np_neu": {
|
||||
"direction": "+",
|
||||
"weight": 0.0
|
||||
},
|
||||
"fund_gdhs_chg_neu": {
|
||||
"direction": "+",
|
||||
"weight": 0.03597
|
||||
},
|
||||
"fund_growth_scissors_neu": {
|
||||
"direction": "+",
|
||||
"weight": 0.0
|
||||
},
|
||||
"alpha83": {
|
||||
"direction": "+",
|
||||
"weight": 0.066094
|
||||
},
|
||||
"kup": {
|
||||
"direction": "-",
|
||||
"weight": 0.079539
|
||||
}
|
||||
},
|
||||
"overrides": [],
|
||||
"created": "2026-09-11"
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"profile_id": "all18-roll-2025",
|
||||
"name": "v2c滚动 all18 2025制度",
|
||||
"method": "icir_fit_rolling3y_smoothed",
|
||||
"fit_window": {
|
||||
"start": "2022-01",
|
||||
"end": "2024-12",
|
||||
"smooth": "新旧各半"
|
||||
},
|
||||
"sources": {
|
||||
"alpha16": {
|
||||
"direction": "+",
|
||||
"weight": 0.119655
|
||||
},
|
||||
"alpha42": {
|
||||
"direction": "+",
|
||||
"weight": 0.050787
|
||||
},
|
||||
"klow": {
|
||||
"direction": "-",
|
||||
"weight": 0.088173
|
||||
},
|
||||
"alpha2": {
|
||||
"direction": "+",
|
||||
"weight": 0.04371
|
||||
},
|
||||
"alpha12": {
|
||||
"direction": "+",
|
||||
"weight": 0.100069
|
||||
},
|
||||
"vol_ma5": {
|
||||
"direction": "-",
|
||||
"weight": 0.072462
|
||||
},
|
||||
"wvma_20": {
|
||||
"direction": "-",
|
||||
"weight": 0.03619
|
||||
},
|
||||
"fund_nsi_neu": {
|
||||
"direction": "+",
|
||||
"weight": 0.040747
|
||||
},
|
||||
"vma_60": {
|
||||
"direction": "+",
|
||||
"weight": 0.088396
|
||||
},
|
||||
"fund_gp_over_m_neu": {
|
||||
"direction": "+",
|
||||
"weight": 0.025284
|
||||
},
|
||||
"cord_5": {
|
||||
"direction": "-",
|
||||
"weight": 0.074449
|
||||
},
|
||||
"alpha81": {
|
||||
"direction": "-",
|
||||
"weight": 0.056022
|
||||
},
|
||||
"fund_bp_neu": {
|
||||
"direction": "+",
|
||||
"weight": 0.038387
|
||||
},
|
||||
"fund_sue_np_neu": {
|
||||
"direction": "+",
|
||||
"weight": 0.000165
|
||||
},
|
||||
"fund_gdhs_chg_neu": {
|
||||
"direction": "+",
|
||||
"weight": 0.032269
|
||||
},
|
||||
"fund_growth_scissors_neu": {
|
||||
"direction": "+",
|
||||
"weight": 0.00216
|
||||
},
|
||||
"alpha83": {
|
||||
"direction": "+",
|
||||
"weight": 0.048876
|
||||
},
|
||||
"kup": {
|
||||
"direction": "-",
|
||||
"weight": 0.082198
|
||||
}
|
||||
},
|
||||
"overrides": [],
|
||||
"created": "2026-09-11"
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"profile_id": "all18-roll-2026",
|
||||
"name": "v2c滚动 all18 2026制度",
|
||||
"method": "icir_fit_rolling3y_smoothed",
|
||||
"fit_window": {
|
||||
"start": "2023-01",
|
||||
"end": "2025-12",
|
||||
"smooth": "新旧各半"
|
||||
},
|
||||
"sources": {
|
||||
"alpha16": {
|
||||
"direction": "+",
|
||||
"weight": 0.138156
|
||||
},
|
||||
"alpha42": {
|
||||
"direction": "+",
|
||||
"weight": 0.047246
|
||||
},
|
||||
"klow": {
|
||||
"direction": "-",
|
||||
"weight": 0.071891
|
||||
},
|
||||
"alpha2": {
|
||||
"direction": "+",
|
||||
"weight": 0.051341
|
||||
},
|
||||
"alpha12": {
|
||||
"direction": "+",
|
||||
"weight": 0.094452
|
||||
},
|
||||
"vol_ma5": {
|
||||
"direction": "-",
|
||||
"weight": 0.061948
|
||||
},
|
||||
"wvma_20": {
|
||||
"direction": "-",
|
||||
"weight": 0.047066
|
||||
},
|
||||
"fund_nsi_neu": {
|
||||
"direction": "+",
|
||||
"weight": 0.036158
|
||||
},
|
||||
"vma_60": {
|
||||
"direction": "+",
|
||||
"weight": 0.086164
|
||||
},
|
||||
"fund_gp_over_m_neu": {
|
||||
"direction": "+",
|
||||
"weight": 0.023599
|
||||
},
|
||||
"cord_5": {
|
||||
"direction": "-",
|
||||
"weight": 0.078083
|
||||
},
|
||||
"alpha81": {
|
||||
"direction": "-",
|
||||
"weight": 0.063933
|
||||
},
|
||||
"fund_bp_neu": {
|
||||
"direction": "+",
|
||||
"weight": 0.039418
|
||||
},
|
||||
"fund_sue_np_neu": {
|
||||
"direction": "+",
|
||||
"weight": 0.003018
|
||||
},
|
||||
"fund_gdhs_chg_neu": {
|
||||
"direction": "+",
|
||||
"weight": 0.033661
|
||||
},
|
||||
"fund_growth_scissors_neu": {
|
||||
"direction": "+",
|
||||
"weight": 0.006269
|
||||
},
|
||||
"alpha83": {
|
||||
"direction": "+",
|
||||
"weight": 0.041295
|
||||
},
|
||||
"kup": {
|
||||
"direction": "-",
|
||||
"weight": 0.076302
|
||||
}
|
||||
},
|
||||
"overrides": [],
|
||||
"created": "2026-09-11"
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"profile_id": "quant12-roll-2021",
|
||||
"name": "v2c滚动 quant12 2021制度",
|
||||
"method": "icir_fit_rolling3y_smoothed",
|
||||
"fit_window": {
|
||||
"start": "2018-01",
|
||||
"end": "2020-12",
|
||||
"smooth": "新旧各半"
|
||||
},
|
||||
"sources": {
|
||||
"alpha16": {
|
||||
"direction": "+",
|
||||
"weight": 0.130152
|
||||
},
|
||||
"alpha42": {
|
||||
"direction": "+",
|
||||
"weight": 0.033644
|
||||
},
|
||||
"klow": {
|
||||
"direction": "-",
|
||||
"weight": 0.077528
|
||||
},
|
||||
"alpha2": {
|
||||
"direction": "+",
|
||||
"weight": 0.086917
|
||||
},
|
||||
"alpha12": {
|
||||
"direction": "+",
|
||||
"weight": 0.132173
|
||||
},
|
||||
"vol_ma5": {
|
||||
"direction": "-",
|
||||
"weight": 0.060808
|
||||
},
|
||||
"wvma_20": {
|
||||
"direction": "-",
|
||||
"weight": 0.063921
|
||||
},
|
||||
"vma_60": {
|
||||
"direction": "+",
|
||||
"weight": 0.098737
|
||||
},
|
||||
"cord_5": {
|
||||
"direction": "-",
|
||||
"weight": 0.104439
|
||||
},
|
||||
"alpha81": {
|
||||
"direction": "-",
|
||||
"weight": 0.06114
|
||||
},
|
||||
"alpha83": {
|
||||
"direction": "+",
|
||||
"weight": 0.117903
|
||||
},
|
||||
"kup": {
|
||||
"direction": "-",
|
||||
"weight": 0.032637
|
||||
}
|
||||
},
|
||||
"overrides": [],
|
||||
"created": "2026-09-11"
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"profile_id": "quant12-roll-2022",
|
||||
"name": "v2c滚动 quant12 2022制度",
|
||||
"method": "icir_fit_rolling3y_smoothed",
|
||||
"fit_window": {
|
||||
"start": "2019-01",
|
||||
"end": "2021-12",
|
||||
"smooth": "新旧各半"
|
||||
},
|
||||
"sources": {
|
||||
"alpha16": {
|
||||
"direction": "+",
|
||||
"weight": 0.141856
|
||||
},
|
||||
"alpha42": {
|
||||
"direction": "+",
|
||||
"weight": 0.031276
|
||||
},
|
||||
"klow": {
|
||||
"direction": "-",
|
||||
"weight": 0.101203
|
||||
},
|
||||
"alpha2": {
|
||||
"direction": "+",
|
||||
"weight": 0.074433
|
||||
},
|
||||
"alpha12": {
|
||||
"direction": "+",
|
||||
"weight": 0.114894
|
||||
},
|
||||
"vol_ma5": {
|
||||
"direction": "-",
|
||||
"weight": 0.073232
|
||||
},
|
||||
"wvma_20": {
|
||||
"direction": "-",
|
||||
"weight": 0.066625
|
||||
},
|
||||
"vma_60": {
|
||||
"direction": "+",
|
||||
"weight": 0.089695
|
||||
},
|
||||
"cord_5": {
|
||||
"direction": "-",
|
||||
"weight": 0.084816
|
||||
},
|
||||
"alpha81": {
|
||||
"direction": "-",
|
||||
"weight": 0.066004
|
||||
},
|
||||
"alpha83": {
|
||||
"direction": "+",
|
||||
"weight": 0.101269
|
||||
},
|
||||
"kup": {
|
||||
"direction": "-",
|
||||
"weight": 0.054696
|
||||
}
|
||||
},
|
||||
"overrides": [],
|
||||
"created": "2026-09-11"
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"profile_id": "quant12-roll-2023",
|
||||
"name": "v2c滚动 quant12 2023制度",
|
||||
"method": "icir_fit_rolling3y_smoothed",
|
||||
"fit_window": {
|
||||
"start": "2020-01",
|
||||
"end": "2022-12",
|
||||
"smooth": "新旧各半"
|
||||
},
|
||||
"sources": {
|
||||
"alpha16": {
|
||||
"direction": "+",
|
||||
"weight": 0.139807
|
||||
},
|
||||
"alpha42": {
|
||||
"direction": "+",
|
||||
"weight": 0.037986
|
||||
},
|
||||
"klow": {
|
||||
"direction": "-",
|
||||
"weight": 0.11437
|
||||
},
|
||||
"alpha2": {
|
||||
"direction": "+",
|
||||
"weight": 0.058717
|
||||
},
|
||||
"alpha12": {
|
||||
"direction": "+",
|
||||
"weight": 0.108835
|
||||
},
|
||||
"vol_ma5": {
|
||||
"direction": "-",
|
||||
"weight": 0.085931
|
||||
},
|
||||
"wvma_20": {
|
||||
"direction": "-",
|
||||
"weight": 0.051465
|
||||
},
|
||||
"vma_60": {
|
||||
"direction": "+",
|
||||
"weight": 0.088757
|
||||
},
|
||||
"cord_5": {
|
||||
"direction": "-",
|
||||
"weight": 0.079192
|
||||
},
|
||||
"alpha81": {
|
||||
"direction": "-",
|
||||
"weight": 0.065336
|
||||
},
|
||||
"alpha83": {
|
||||
"direction": "+",
|
||||
"weight": 0.089913
|
||||
},
|
||||
"kup": {
|
||||
"direction": "-",
|
||||
"weight": 0.07969
|
||||
}
|
||||
},
|
||||
"overrides": [],
|
||||
"created": "2026-09-11"
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"profile_id": "quant12-roll-2024",
|
||||
"name": "v2c滚动 quant12 2024制度",
|
||||
"method": "icir_fit_rolling3y_smoothed",
|
||||
"fit_window": {
|
||||
"start": "2021-01",
|
||||
"end": "2023-12",
|
||||
"smooth": "新旧各半"
|
||||
},
|
||||
"sources": {
|
||||
"alpha16": {
|
||||
"direction": "+",
|
||||
"weight": 0.132345
|
||||
},
|
||||
"alpha42": {
|
||||
"direction": "+",
|
||||
"weight": 0.050697
|
||||
},
|
||||
"klow": {
|
||||
"direction": "-",
|
||||
"weight": 0.115682
|
||||
},
|
||||
"alpha2": {
|
||||
"direction": "+",
|
||||
"weight": 0.04811
|
||||
},
|
||||
"alpha12": {
|
||||
"direction": "+",
|
||||
"weight": 0.111555
|
||||
},
|
||||
"vol_ma5": {
|
||||
"direction": "-",
|
||||
"weight": 0.095477
|
||||
},
|
||||
"wvma_20": {
|
||||
"direction": "-",
|
||||
"weight": 0.03959
|
||||
},
|
||||
"vma_60": {
|
||||
"direction": "+",
|
||||
"weight": 0.096227
|
||||
},
|
||||
"cord_5": {
|
||||
"direction": "-",
|
||||
"weight": 0.080856
|
||||
},
|
||||
"alpha81": {
|
||||
"direction": "-",
|
||||
"weight": 0.060884
|
||||
},
|
||||
"alpha83": {
|
||||
"direction": "+",
|
||||
"weight": 0.076523
|
||||
},
|
||||
"kup": {
|
||||
"direction": "-",
|
||||
"weight": 0.092053
|
||||
}
|
||||
},
|
||||
"overrides": [],
|
||||
"created": "2026-09-11"
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"profile_id": "quant12-roll-2025",
|
||||
"name": "v2c滚动 quant12 2025制度",
|
||||
"method": "icir_fit_rolling3y_smoothed",
|
||||
"fit_window": {
|
||||
"start": "2022-01",
|
||||
"end": "2024-12",
|
||||
"smooth": "新旧各半"
|
||||
},
|
||||
"sources": {
|
||||
"alpha16": {
|
||||
"direction": "+",
|
||||
"weight": 0.139073
|
||||
},
|
||||
"alpha42": {
|
||||
"direction": "+",
|
||||
"weight": 0.058994
|
||||
},
|
||||
"klow": {
|
||||
"direction": "-",
|
||||
"weight": 0.102338
|
||||
},
|
||||
"alpha2": {
|
||||
"direction": "+",
|
||||
"weight": 0.050774
|
||||
},
|
||||
"alpha12": {
|
||||
"direction": "+",
|
||||
"weight": 0.116229
|
||||
},
|
||||
"vol_ma5": {
|
||||
"direction": "-",
|
||||
"weight": 0.084095
|
||||
},
|
||||
"wvma_20": {
|
||||
"direction": "-",
|
||||
"weight": 0.042046
|
||||
},
|
||||
"vma_60": {
|
||||
"direction": "+",
|
||||
"weight": 0.102671
|
||||
},
|
||||
"cord_5": {
|
||||
"direction": "-",
|
||||
"weight": 0.08647
|
||||
},
|
||||
"alpha81": {
|
||||
"direction": "-",
|
||||
"weight": 0.065105
|
||||
},
|
||||
"alpha83": {
|
||||
"direction": "+",
|
||||
"weight": 0.056727
|
||||
},
|
||||
"kup": {
|
||||
"direction": "-",
|
||||
"weight": 0.095478
|
||||
}
|
||||
},
|
||||
"overrides": [],
|
||||
"created": "2026-09-11"
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"profile_id": "quant12-roll-2026",
|
||||
"name": "v2c滚动 quant12 2026制度",
|
||||
"method": "icir_fit_rolling3y_smoothed",
|
||||
"fit_window": {
|
||||
"start": "2023-01",
|
||||
"end": "2025-12",
|
||||
"smooth": "新旧各半"
|
||||
},
|
||||
"sources": {
|
||||
"alpha16": {
|
||||
"direction": "+",
|
||||
"weight": 0.161035
|
||||
},
|
||||
"alpha42": {
|
||||
"direction": "+",
|
||||
"weight": 0.055084
|
||||
},
|
||||
"klow": {
|
||||
"direction": "-",
|
||||
"weight": 0.083813
|
||||
},
|
||||
"alpha2": {
|
||||
"direction": "+",
|
||||
"weight": 0.059831
|
||||
},
|
||||
"alpha12": {
|
||||
"direction": "+",
|
||||
"weight": 0.110114
|
||||
},
|
||||
"vol_ma5": {
|
||||
"direction": "-",
|
||||
"weight": 0.072211
|
||||
},
|
||||
"wvma_20": {
|
||||
"direction": "-",
|
||||
"weight": 0.054842
|
||||
},
|
||||
"vma_60": {
|
||||
"direction": "+",
|
||||
"weight": 0.100444
|
||||
},
|
||||
"cord_5": {
|
||||
"direction": "-",
|
||||
"weight": 0.09101
|
||||
},
|
||||
"alpha81": {
|
||||
"direction": "-",
|
||||
"weight": 0.074519
|
||||
},
|
||||
"alpha83": {
|
||||
"direction": "+",
|
||||
"weight": 0.048139
|
||||
},
|
||||
"kup": {
|
||||
"direction": "-",
|
||||
"weight": 0.088959
|
||||
}
|
||||
},
|
||||
"overrides": [],
|
||||
"created": "2026-09-11"
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
# tests/factor/test_composite_rolling.py
|
||||
"""合成层 v2c(Rolling): 滚动制度因子——档案扫描/命名/0 权项过滤/失败留痕/幂等重建/回归."""
|
||||
import importlib
|
||||
import json
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "vnpy_v4.4.0")))
|
||||
|
||||
import pytest
|
||||
|
||||
from sanguo_factor import composite_library, composite_weighting, composite_rolling
|
||||
from sanguo_factor.composite_weighting import (
|
||||
WeightProfile, load_profile, resolve_weights, register_composite_from_profile,
|
||||
)
|
||||
from sanguo_factor.registry import get_factor, _REGISTRY
|
||||
|
||||
# 12 滚动档案 × scope(quant12/all18) × 年(2021-2026);排序稳定 = (scope 字典序, year 升序)
|
||||
EXPECTED_NAMES = ([f"composite_v2c_all18_{y}" for y in range(2021, 2027)]
|
||||
+ [f"composite_v2c_quant12_{y}" for y in range(2021, 2027)])
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _ensure_v2c_registered():
|
||||
"""其它测试模块清空 _REGISTRY 后幂等重挂(与 test_composite_weighting 同模式)."""
|
||||
composite_rolling._register_all()
|
||||
|
||||
|
||||
def _split_top_terms(expr: str) -> list[str]:
|
||||
"""按括号深度 0 切分合成表达式顶层加法项(term 内部可含 + 与括号)."""
|
||||
body = expr[expr.index("(") + 1: expr.rindex(") / ")]
|
||||
parts, depth, start, i = [], 0, 0, 0
|
||||
while i < len(body):
|
||||
if body[i] == "(":
|
||||
depth += 1
|
||||
elif body[i] == ")":
|
||||
depth -= 1
|
||||
elif depth == 0 and body.startswith(" + ", i):
|
||||
parts.append(body[start:i])
|
||||
start = i + 3
|
||||
i += 3
|
||||
continue
|
||||
i += 1
|
||||
parts.append(body[start:])
|
||||
return parts
|
||||
|
||||
|
||||
# ==================== 档案扫描与命名 ====================
|
||||
|
||||
def test_iter_rolling_profiles_discovers_12_stable_order():
|
||||
"""扫描 weight_profiles/ 认 12 个 {scope}_roll_{year}.json,排序 (scope, year) 稳定;
|
||||
v2a/v2b 等非约定名文件不纳入."""
|
||||
got = composite_rolling._iter_rolling_profiles()
|
||||
assert [name for name, _p in got] == EXPECTED_NAMES
|
||||
assert all(p.name.endswith(".json") and "_roll_" in p.name for _n, p in got)
|
||||
|
||||
|
||||
def test_all_12_rolling_factors_registered_composite():
|
||||
"""导入即注册: 12 个 composite_v2c_* 因子全在 registry,category=composite."""
|
||||
for name in EXPECTED_NAMES:
|
||||
fac = get_factor(name)
|
||||
assert fac is not None, f"{name} 未注册"
|
||||
assert fac["category"] == "composite"
|
||||
|
||||
|
||||
# ==================== 0 权项过滤(表达式项数) ====================
|
||||
|
||||
def test_quant12_rolling_expression_exact_12_terms():
|
||||
"""quant12 滚动档案权重全正: 表达式恰 12 项,无 0.000000 项."""
|
||||
expr = get_factor("composite_v2c_quant12_2021")["expression"]
|
||||
assert len(_split_top_terms(expr)) == 12
|
||||
assert "0.000000 * " not in expr
|
||||
|
||||
|
||||
def test_all18_2021_drops_zero_weight_neu_terms():
|
||||
"""all18 2021 制度 6 个 neu 源 0 权被跳过: 表达式恰 12 项,neu 源因子串不出现."""
|
||||
expr = get_factor("composite_v2c_all18_2021")["expression"]
|
||||
assert len(_split_top_terms(expr)) == 12
|
||||
assert "0.000000 * " not in expr
|
||||
profile = load_profile(str(next(
|
||||
p for n, p in composite_rolling._iter_rolling_profiles()
|
||||
if n == "composite_v2c_all18_2021")))
|
||||
zero_srcs = [s for s, (_d, w) in profile.sources.items() if w <= 0]
|
||||
assert len(zero_srcs) == 6 # 设计行为: 2021 neu 全 0
|
||||
for src in zero_srcs:
|
||||
assert get_factor(src)["expression"] not in expr, f"0 权源 {src} 不应入式"
|
||||
|
||||
|
||||
def test_all_rolling_terms_count_matches_positive_weights():
|
||||
"""派生断言(不钉死档案数值): 12 份滚动档案顶层项数 == 正权源数,
|
||||
每个正权源以 6 位定点权重入式(all18_2022..2024 含零星 0 权 neu 源同样被跳过)."""
|
||||
for name, path in composite_rolling._iter_rolling_profiles():
|
||||
weights = resolve_weights(load_profile(str(path)))
|
||||
positive = {s: w for s, w in weights.items() if w > 0}
|
||||
expr = get_factor(name)["expression"]
|
||||
terms = _split_top_terms(expr)
|
||||
assert len(terms) == len(positive), f"{name} 项数 {len(terms)} != 正权源数 {len(positive)}"
|
||||
for src, w in positive.items():
|
||||
assert any(t.startswith(f"{w:.6f} * (") for t in terms), \
|
||||
f"{name} 缺 {src} 权重项"
|
||||
|
||||
|
||||
# ==================== 全零防线与失败留痕 ====================
|
||||
|
||||
def test_register_all_zero_profile_raises_value_error():
|
||||
"""全零档案非法: 直接构造 WeightProfile(L1 全 0,绕过 load 校验)注册 → ValueError,
|
||||
且不注册(可读错误优于 ZeroDivisionError)."""
|
||||
name = "composite_v2c_zerotest"
|
||||
_REGISTRY.pop(name, None)
|
||||
sources = {n: (d, 0.0) for n, d in composite_library.QUANT_SOURCES}
|
||||
with pytest.raises(ValueError, match="全零"):
|
||||
register_composite_from_profile(
|
||||
WeightProfile("t-zero", "t", "manual", sources, (), {}), name)
|
||||
assert get_factor(name) is None
|
||||
|
||||
|
||||
def test_register_failure_message_carries_filename(tmp_path, monkeypatch):
|
||||
"""单档案加载失败 raise 且带文件名(防静默丢制度期)."""
|
||||
bad = tmp_path / "quant12_roll_2020.json"
|
||||
bad.write_text(json.dumps({"profile_id": "broken", "name": "t", "method": "m"}),
|
||||
encoding="utf-8") # 缺 sources
|
||||
monkeypatch.setattr(composite_rolling, "_iter_rolling_profiles",
|
||||
lambda: [("composite_v2c_quant12_2020", bad)])
|
||||
with pytest.raises(ValueError, match="quant12_roll_2020.json"):
|
||||
composite_rolling._register_all()
|
||||
|
||||
|
||||
# ==================== 幂等重建与回归 ====================
|
||||
|
||||
def test_registry_cleared_reload_rebuilds_independently():
|
||||
"""registry 清空后 reload composite_rolling 可独立重建: v2c 12 个 +
|
||||
依赖链(v1.1 源/neu/v2a/v2b)全部重挂(幂等)."""
|
||||
_REGISTRY.clear()
|
||||
assert get_factor("composite_v2c_quant12_2024") is None
|
||||
importlib.reload(composite_rolling)
|
||||
for name in EXPECTED_NAMES:
|
||||
assert get_factor(name) is not None, f"{name} 未重建"
|
||||
for dep in ("alpha16", "fund_bp_neu",
|
||||
composite_weighting._DEFAULT_COMPOSITE_NAME,
|
||||
composite_weighting._V2B_COMPOSITE_NAME):
|
||||
assert get_factor(dep) is not None, f"依赖 {dep} 未重挂"
|
||||
|
||||
|
||||
def test_v2a_v2b_default_factors_unaffected():
|
||||
"""回归: v2c 上线后既有 v2a/v2b 默认因子不变(全正权档案,项数 12/18,权重入式)."""
|
||||
v2a = get_factor(composite_weighting._DEFAULT_COMPOSITE_NAME)
|
||||
assert v2a is not None
|
||||
assert len(_split_top_terms(v2a["expression"])) == 12
|
||||
v2b = get_factor(composite_weighting._V2B_COMPOSITE_NAME)
|
||||
assert v2b is not None
|
||||
assert len(_split_top_terms(v2b["expression"])) == 18
|
||||
weights = resolve_weights(
|
||||
load_profile(str(composite_weighting._DEFAULT_PROFILE_PATH)))
|
||||
for src, w in weights.items():
|
||||
assert f"{w:.6f} * (" in v2a["expression"], f"v2a 缺 {src} 项"
|
||||
Reference in New Issue
Block a user