# tests/factor/test_fundamental_library.py """财务因子表达式库: 32 个 P0 因子注册 + 表达式↔adapter 特征列契约锁定.""" 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.registry import list_factors, get_factor @pytest.fixture(autouse=True) def _ensure_fundamental_registered(): """其它测试模块清空 _REGISTRY 后只重挂 alpha/builtin(顺序依赖前科), 这里逐测试幂等重注册财务因子,保证本模块与顺序无关.""" fundamental_library._register_all() # 表达式可引用的列 = adapter 特征列 + 行情列 close(估值类 ÷ close×share_capital) # + 表达式函数名(P1 起用到 log) _ALLOWED = set(FEATURE_COLUMNS) | {"close", "cs_rank", "log"} def _fundamental_factors() -> list[dict]: return list_factors("fundamental") 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) == 75, f"P0+P1+P1-B 应为 75 个,实际 {len(facs)}" # 六族代表抽查(全部名单见 fundamental_library 注释) expect = { "fund_roe_ttm", "fund_gp_over_assets", # A "fund_tacc", "fund_nonrec_ratio", # B "fund_rev_q_yoy", "fund_np_q_yoy", "fund_asset_growth", # C "fund_ep_ttm", "fund_bp", "fund_cp", # D "fund_nsi", "fund_ibd_ratio", # E "fund_sue_np", "fund_forecast_type", # F } assert expect <= names def test_expressions_only_reference_feature_columns(): """契约: 表达式裸标识符 ⊆ adapter 特征列 + close/cs_rank(漏加列=拼写错).""" for f in _fundamental_factors(): idents = set(re.findall(r"[A-Za-z_][A-Za-z0-9_]*", f["expression"])) bad = idents - _ALLOWED assert not bad, f"{f['name']} 引用了未产出列: {bad} in {f['expression']}" def test_all_factors_are_cross_sectional_rank(): """P0 设计: 因子 = cs_rank(基础指标) 一层(负 IC 因子取负定向,高=好).""" for f in _fundamental_factors(): assert f["expression"].startswith("cs_rank("), f["name"] assert f["expression"].endswith(")") def test_negative_ic_factors_flipped(): """文档负 IC 因子(B01/B04/B05/B07/B08/B11/C15/E01/E02/E05/E09)表达式含负号.""" flipped = {"fund_tacc", "fund_nonrec_ratio", "fund_impairment_ratio", "fund_invest_income_dep", "fund_receivables_anomaly", "fund_other_rece_ratio", "fund_asset_growth", "fund_nsi", "fund_equity_fin_intensity", "fund_ibd_ratio", "fund_goodwill_ratio"} for name in flipped: expr = get_factor(name)["expression"] assert "(-" in expr or expr.startswith("cs_rank(-"), f"{name} 应翻转: {expr}" def test_valuation_factors_use_close_times_share_capital(): """估值族市值 = close × share_capital 自算(规避 valuation 中文列名表).""" for name in ("fund_ep_ttm", "fund_ep_deduct_ttm", "fund_bp", "fund_cp", "fund_equity_fin_intensity"): expr = get_factor(name)["expression"] assert "close * share_capital" in expr, f"{name}: {expr}" def test_registration_idempotent(): """重复 import 不炸(注册表防重入,同 library.py 模式).""" import importlib importlib.reload(fundamental_library) assert len(_fundamental_factors()) == 75