feat(factor): P1-B批library注册6因子(四新域5因子+D14双注册) [nas]
fund_ccc(-)/fund_send_total_12m(不定正向注册待实证)/fund_gdhs_chg(-)/ fund_topholder_chg(+)/fund_ep_vb(+)/fund_bp_vb(+);注册总数 69→75 (P0 32+P1 37+P1-B 6,既有计数契约同步更新);batch e2e 锁死 vb+dividend 域级引用瘦身 join 路径。
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
"""财务因子批表达式库: P0 32 + P1 37(35 因子 + 2 互评变体)注册(category="fundamental").
|
||||
"""财务因子批表达式库: P0 32 + P1 37 + P1-B 6 注册(category="fundamental").
|
||||
|
||||
选型来源 docs/fundamental_factor_survey_20260907.md(§3 候选池 96 个 + §4.1 去重),
|
||||
首批 32 = P0 50 个按族去重后的主代表;全部「因子 = cs_rank(基础指标)」一层截面,
|
||||
@@ -123,10 +123,30 @@ FUNDAMENTAL_P1_FACTORS: list[tuple[str, str, str, str]] = [
|
||||
("fund_asset_growth_neg", "cs_rank(asset_growth)", "C15v", "学术负IC对照"),
|
||||
]
|
||||
|
||||
# P1-B 批(四新域 6 注册 = 5 因子,D14 EP/BP 双注册;编号 = 调研文档 §3)。
|
||||
# 口径登记: E08 CCC 三表自算(均值=t 与 t−4 期末平均,COGS=OPERATE_COST TTM,
|
||||
# 金融股置 NaN)/ E11 365 天滚动窗开区间下界,PIT 回退链
|
||||
# dividPlanAnnounceDate→PreNotice→AgmPum→Plan / E12 相邻事件 Δln(公告日 asof,
|
||||
# 不用「增减比例」列)/ E13 法定披露截止 PIT(Q1→04-30/H1→08-31/Q3→10-31/
|
||||
# 年报→次年04-30) / D14 vb 长史(1990 起含退市,2026 缺口 ak 补)分域验证用
|
||||
# ——详见 fundamental_adapter 注释。
|
||||
FUNDAMENTAL_P1B_FACTORS: list[tuple[str, str, str, str]] = [
|
||||
# ---- E 资本结构/行为 P1-B(4) ----
|
||||
("fund_ccc", "cs_rank(-ccc)", "E08", "-(弱)"),
|
||||
# E11 方向不定(公告前+/除权后−混合)→ 正向注册,由 IC 实测定去留
|
||||
("fund_send_total_12m", "cs_rank(send_total_12m)", "E11", "不定"),
|
||||
("fund_gdhs_chg", "cs_rank(-gdhs_chg)", "E12", "-"),
|
||||
("fund_topholder_chg", "cs_rank(topholder_chg)", "E13", "+"),
|
||||
# ---- D 估值 P1-B(D14 双注册;vb 域长史分域验证,2017 前后须分域报 IC) ----
|
||||
("fund_ep_vb", "cs_rank(ep_vb)", "D14", "+"),
|
||||
("fund_bp_vb", "cs_rank(bp_vb)", "D14", "+"),
|
||||
]
|
||||
|
||||
|
||||
def _register_all() -> None:
|
||||
"""注册全部财务因子(已存在同名跳过,幂等;同 library.py 模式)."""
|
||||
for name, expression, _doc_id, _ic in [*FUNDAMENTAL_FACTORS, *FUNDAMENTAL_P1_FACTORS]:
|
||||
batches = [*FUNDAMENTAL_FACTORS, *FUNDAMENTAL_P1_FACTORS, *FUNDAMENTAL_P1B_FACTORS]
|
||||
for name, expression, _doc_id, _ic in batches:
|
||||
if name not in _REGISTRY:
|
||||
register_factor(name, expression, category="fundamental")
|
||||
|
||||
|
||||
@@ -113,6 +113,26 @@ def test_fundamental_p1_end_to_end(db, synthetic_static, tmp_path):
|
||||
assert m["1"]["count"] > 0, name
|
||||
|
||||
|
||||
def test_fundamental_p1b_end_to_end(db, synthetic_static, tmp_path):
|
||||
"""P1-B 批端到端: vb 日频域 + dividend 事件流走引用瘦身域级加载."""
|
||||
eval_db = str(tmp_path / "fund_p1b_eval.db")
|
||||
out = run_batch_eval(
|
||||
factor_names=["fund_ep_vb", "fund_send_total_12m"],
|
||||
start="2023-08-01", end="2023-12-31",
|
||||
eval_db=eval_db, label="fund_p1b", cfg=None, vnpy_db_override=db,
|
||||
fund_data_dir=synthetic_static,
|
||||
)
|
||||
assert out["factors_done"] == 2
|
||||
assert out["errors"] == []
|
||||
for name in ("fund_ep_vb", "fund_send_total_12m"):
|
||||
m = eval_store.get_detail(eval_db, out["run_id"], name)["metrics"]
|
||||
assert "error" not in m, f"{name}: {m.get('error')}"
|
||||
# ep_vb 仅 2/3 股非空(C 亏损 pe=NaN)→ <3 截面 IC 记 0(n>=3 引擎下限,
|
||||
# 与 P1 vsig 同形态);域级加载/join 路径本身已被无错误评估锁死
|
||||
m = eval_store.get_detail(eval_db, out["run_id"], "fund_ep_vb")["metrics"]
|
||||
assert m["1"]["count"] >= 0
|
||||
|
||||
|
||||
def test_pure_price_batch_untouched(db, tmp_path):
|
||||
"""纯量价批不传静态域照常跑(回归: 未新增强依赖)."""
|
||||
eval_db = str(tmp_path / "px_eval.db")
|
||||
|
||||
@@ -31,7 +31,7 @@ def test_p0_32_factors_registered():
|
||||
facs = _fundamental_factors()
|
||||
names = {f["name"] for f in facs}
|
||||
# P0 32 + P1 37(35 因子 + 2 互评变体;P1 名单见 test_fundamental_p1_library)
|
||||
assert len(facs) == 69, f"P0+P1 应为 69 个,实际 {len(facs)}"
|
||||
assert len(facs) == 75, f"P0+P1+P1-B 应为 75 个,实际 {len(facs)}"
|
||||
# 六族代表抽查(全部名单见 fundamental_library 注释)
|
||||
expect = {
|
||||
"fund_roe_ttm", "fund_gp_over_assets", # A
|
||||
@@ -83,4 +83,4 @@ def test_registration_idempotent():
|
||||
"""重复 import 不炸(注册表防重入,同 library.py 模式)."""
|
||||
import importlib
|
||||
importlib.reload(fundamental_library)
|
||||
assert len(_fundamental_factors()) == 69
|
||||
assert len(_fundamental_factors()) == 75
|
||||
|
||||
@@ -100,4 +100,4 @@ def test_p1_registration_idempotent():
|
||||
"""重复注册幂等(防重入)."""
|
||||
fundamental_library._register_all()
|
||||
fundamental_library._register_all()
|
||||
assert len(list_factors("fundamental")) == 69
|
||||
assert len(list_factors("fundamental")) == 75
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
# tests/factor/test_fundamental_p1b_library.py
|
||||
"""P1-B 批财务因子表达式库: 四新域 6 个注册(5 因子,D14 双注册)契约锁定.
|
||||
|
||||
契约:
|
||||
- 表达式裸标识符 ⊆ adapter 特征列 + close/log/cs_rank
|
||||
- 全部 cs_rank 一层截面;E08/E12 负 IC 取负,E11 不定按正向注册待实证
|
||||
- E13/D14 正向;vb 两因子直接消费 vb 域日频列(不经 close×share_capital)
|
||||
"""
|
||||
import re
|
||||
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 fundamental_library # noqa: F401 import 即注册
|
||||
from sanguo_factor.fundamental_adapter import FEATURE_COLUMNS
|
||||
from sanguo_factor.fundamental_library import FUNDAMENTAL_P1B_FACTORS
|
||||
from sanguo_factor.registry import list_factors, get_factor
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _ensure_fundamental_registered():
|
||||
"""其它测试模块清空 _REGISTRY 后逐测试幂等重注册(顺序依赖前科)."""
|
||||
fundamental_library._register_all()
|
||||
|
||||
|
||||
_ALLOWED = set(FEATURE_COLUMNS) | {"close", "cs_rank", "log"}
|
||||
|
||||
|
||||
def test_p1b_6_factors_registered():
|
||||
facs = {f["name"]: f for f in list_factors("fundamental")}
|
||||
names = {n for n, _e, _d, _ic in FUNDAMENTAL_P1B_FACTORS}
|
||||
assert len(names) == 6, f"P1-B 应 6 个注册(5 因子,D14 双注册),实际 {len(names)}"
|
||||
for name in names:
|
||||
assert name in facs, f"{name} 未注册"
|
||||
assert facs[name]["category"] == "fundamental"
|
||||
|
||||
|
||||
def test_p1b_expressions_only_reference_feature_columns():
|
||||
for name, expression, _d, _ic in FUNDAMENTAL_P1B_FACTORS:
|
||||
idents = set(re.findall(r"[A-Za-z_][A-Za-z0-9_]*", expression))
|
||||
bad = idents - _ALLOWED
|
||||
assert not bad, f"{name} 引用了未产出列: {bad} in {expression}"
|
||||
|
||||
|
||||
def test_p1b_all_cross_sectional_rank_one_layer():
|
||||
for name, expression, _d, _ic in FUNDAMENTAL_P1B_FACTORS:
|
||||
assert expression.startswith("cs_rank("), f"{name}: {expression}"
|
||||
assert expression.endswith(")")
|
||||
|
||||
|
||||
def test_p1b_negative_ic_factors_flipped():
|
||||
"""E08(CCC 低=效率高)/E12(户数降=筹码集中)取负;E13/D14/E11 正向."""
|
||||
assert get_factor("fund_ccc")["expression"] == "cs_rank(-ccc)"
|
||||
assert get_factor("fund_gdhs_chg")["expression"] == "cs_rank(-gdhs_chg)"
|
||||
assert get_factor("fund_topholder_chg")["expression"] == "cs_rank(topholder_chg)"
|
||||
# E11 方向不定(公告前+/除权后−混合)→ 正向注册,由 IC 实测定去留
|
||||
assert get_factor("fund_send_total_12m")["expression"] == "cs_rank(send_total_12m)"
|
||||
|
||||
|
||||
def test_p1b_vb_factors_consume_vb_columns():
|
||||
"""D14 双注册直接消费 vb 域列(长史 1990 起含退市,分域验证用)."""
|
||||
assert get_factor("fund_ep_vb")["expression"] == "cs_rank(ep_vb)"
|
||||
assert get_factor("fund_bp_vb")["expression"] == "cs_rank(bp_vb)"
|
||||
|
||||
|
||||
def test_p1b_registration_idempotent():
|
||||
fundamental_library._register_all()
|
||||
fundamental_library._register_all()
|
||||
# P0 32 + P1 37 + P1-B 6 = 75
|
||||
assert len(list_factors("fundamental")) == 75
|
||||
Reference in New Issue
Block a user