# tests/factor/test_fundamental_p1_library.py """P1 批财务因子表达式库: 37 个注册(35 因子 + 2 互评变体)与契约锁定. 契约: - 表达式裸标识符 ⊆ adapter 特征列 + close/log/cs_rank(漏加列 = 拼写错) - 全部 cs_rank 一层截面;文档负 IC 因子表达式取负(高=好统一) - EV 群分母 = close×share_capital + ev_ex_mv;D12 是中性化控制变量非 alpha """ 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_P1_FACTORS from sanguo_factor.registry import list_factors, get_factor @pytest.fixture(autouse=True) def _ensure_fundamental_registered(): """其它测试模块清空 _REGISTRY 后只重挂 alpha/builtin(顺序依赖前科), 这里逐测试幂等重注册财务因子,保证本模块与顺序无关.""" fundamental_library._register_all() _ALLOWED = set(FEATURE_COLUMNS) | {"close", "cs_rank", "log"} def _p1_names() -> set[str]: return {name for name, _e, _d, _ic in FUNDAMENTAL_P1_FACTORS} def test_p1_37_factors_registered(): facs = {f["name"]: f for f in list_factors("fundamental")} p1 = _p1_names() assert len(p1) == 37, f"P1 应 37 个(35+2 变体),实际 {len(p1)}" for name in p1: assert name in facs, f"{name} 未注册" assert facs[name]["category"] == "fundamental" def test_p1_expressions_only_reference_feature_columns(): """契约: 表达式裸标识符 ⊆ adapter 特征列 + close/log/cs_rank.""" for name, expression, _d, _ic in FUNDAMENTAL_P1_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_p1_all_cross_sectional_rank_one_layer(): for name, expression, _d, _ic in FUNDAMENTAL_P1_FACTORS: assert expression.startswith("cs_rank("), f"{name}: {expression}" assert expression.endswith(")") def test_p1_negative_ic_factors_flipped(): """文档负 IC 的 P1 因子(B09/B12/B13/B14/B15/B18/C16/C18/E03/E10/A16)取负.""" flipped = {"fund_sale_expense_ratio", "fund_inventory_anomaly", "fund_cash_ibd_double", "fund_vsig", "fund_vsig_acc", "fund_vsig_cfo", "fund_gm_nm_scissors", "fund_nwc_growth", "fund_invest_growth", "fund_debt_issue", "fund_goodwill_growth"} for name in flipped: expr = get_factor(name)["expression"] assert "(-" in expr, f"{name} 应翻转: {expr}" def test_p1_ev_group_denominator(): """EV 群(D07/D08/D15)分母 = close×share_capital + ev_ex_mv(IBD−MON).""" for name in ("fund_ebit_over_ev", "fund_ebitda_over_ev", "fund_fcf_over_ev"): expr = get_factor(name)["expression"] assert "close * share_capital + ev_ex_mv" in expr, f"{name}: {expr}" def test_p1_valuation_uses_self_computed_mv(): """市值类因子(D09/D13/E03)统一 close×share_capital 自算(不读 valuation).""" for name in ("fund_gp_over_m", "fund_forward_ep", "fund_debt_issue"): expr = get_factor(name)["expression"] assert "close * share_capital" in expr, f"{name}: {expr}" def test_p1_ln_mv_is_control_variable(): """D12 = ln(MV) 中性化控制变量(非 alpha): log(close×share_capital).""" expr = get_factor("fund_ln_mv")["expression"] assert expr == "cs_rank(log(close * share_capital))" def test_p1_two_variants(): """互评变体: SUE 严窗列 + 资产增长负向对照(原始方向,不取负).""" assert get_factor("fund_sue_np_strict")["expression"] == "cs_rank(sue_np_strict)" # 负向对照 = 现有 fund_asset_growth(cs_rank(-asset_growth))的方向反转 assert get_factor("fund_asset_growth_neg")["expression"] == "cs_rank(asset_growth)" assert "-" not in get_factor("fund_asset_growth_neg")["expression"].replace( "cs_rank(", "").replace("asset_growth)", "") def test_p1_registration_idempotent(): """重复注册幂等(防重入).""" fundamental_library._register_all() fundamental_library._register_all() assert len(list_factors("fundamental")) == 75