# tests/factor/test_fundamental_batch.py """财务因子批端到端: 合成行情库 + 合成静态域 → batch_eval 出 IC 指标.""" import sqlite3 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 alpha_datasets # 挂载量价因子 from sanguo_factor import fundamental_library # noqa: F401 挂载财务因子(import 即注册) from sanguo_factor.batch_eval import run_batch_eval from sanguo_factor import eval_store @pytest.fixture(autouse=True) def _ensure_fundamental_registered(): """其它测试模块清空 _REGISTRY 后只重挂 alpha/builtin(顺序依赖前科), 这里逐测试幂等重注册财务因子,保证本模块与顺序无关.""" fundamental_library._register_all() _DDL = """ CREATE TABLE dbbardata( symbol TEXT, exchange TEXT, datetime TEXT, interval TEXT, volume REAL, turnover REAL, open_interest REAL, open_price REAL, high_price REAL, low_price REAL, close_price REAL) """ _STOCKS = [("600000", "SSE"), ("000001", "SZSE"), ("300001", "SZSE")] @pytest.fixture(scope="module") def db(tmp_path_factory): """3 只 × 2022-06~2024-01 合成日线(与合成财务域同代码;趋势构造截面差异).""" alpha_datasets.mount_all() p = tmp_path_factory.mktemp("fb") db = str(p / "qt.db") conn = sqlite3.connect(db) conn.execute(_DDL) import pandas as pd days = pd.bdate_range("2022-06-01", "2024-01-05") drift = {"600000": 0.0012, "000001": -0.0006, "300001": 0.0004} base = {"600000": 10.0, "000001": 5.0, "300001": 20.0} for i, day in enumerate(days): d = day.strftime("%Y-%m-%d") rows = [] for sym, ex in _STOCKS: px = base[sym] * (1.0 + drift[sym]) ** i rows.append((sym, ex, f"{d} 00:00:00", "d", 100.0, px * 100.0, 0, px, px, px, px)) conn.executemany("INSERT INTO dbbardata VALUES(?,?,?,?,?,?,?,?,?,?,?)", rows) conn.commit() conn.close() return db def test_fundamental_end_to_end(db, synthetic_static, tmp_path): eval_db = str(tmp_path / "fund_eval.db") out = run_batch_eval( factor_names=["fund_roe_ttm", "fund_tacc", "fund_ep_ttm", "fund_forecast_type"], start="2023-02-01", end="2023-12-31", eval_db=eval_db, label="fund_t", cfg=None, vnpy_db_override=db, fund_data_dir=synthetic_static, ) assert out["factors_done"] == 4 assert out["errors"] == [] for name in ("fund_roe_ttm", "fund_tacc", "fund_ep_ttm", "fund_forecast_type"): m = eval_store.get_detail(eval_db, out["run_id"], name)["metrics"] assert "error" not in m, f"{name}: {m.get('error')}" # 截面 3 只 → 逐日 rank IC 可算(预告因子 2023-07-15 后才有效, count 少但非零) assert isinstance(m["1"]["ic_mean"], float) or m["1"]["ic_mean"] is None assert m["1"]["count"] >= 0 def test_mixed_batch_price_plus_fundamental(db, synthetic_static, tmp_path): """量价+财务同批混跑: 白名单 join 只在含财务因子时发生,互不干扰.""" eval_db = str(tmp_path / "mix_eval.db") out = run_batch_eval( factor_names=["ma_20", "fund_sue_np"], start="2023-02-01", end="2023-06-30", eval_db=eval_db, label="mix_t", cfg=None, vnpy_db_override=db, fund_data_dir=synthetic_static, ) assert out["errors"] == [] rows = eval_store.get_rows(eval_db, out["run_id"]) assert {r["factor"] for r in rows} == {"ma_20", "fund_sue_np"} # SUE 在 2023-06-30 前可见的报告期均无完整 8 期窗 → 全 NaN → eliminated 非 error m = eval_store.get_detail(eval_db, out["run_id"], "fund_sue_np")["metrics"] assert "error" not in m def test_fundamental_p1_end_to_end(db, synthetic_static, tmp_path): """P1 批端到端: VSIG(16 季窗)+EV 群(分母含 ev_ex_mv)+兑现差走引用瘦身 join.""" eval_db = str(tmp_path / "fund_p1_eval.db") out = run_batch_eval( factor_names=["fund_vsig", "fund_ebit_over_ev", "fund_forecast_beat", "fund_profit_streak"], start="2023-02-01", end="2023-12-31", eval_db=eval_db, label="fund_p1", cfg=None, vnpy_db_override=db, fund_data_dir=synthetic_static, ) assert out["factors_done"] == 4 assert out["errors"] == [] for name in ("fund_vsig", "fund_ebit_over_ev", "fund_forecast_beat", "fund_profit_streak"): m = eval_store.get_detail(eval_db, out["run_id"], name)["metrics"] assert "error" not in m, f"{name}: {m.get('error')}" # EV 群/持续盈利全截面有值 → IC 样本点非零;vsig 仅 2/3 股非空(B 窗含 # NaN)→ <3 截面 IC 记 0(n>=3 引擎下限),路径本身已被无错误评估锁死 for name in ("fund_ebit_over_ev", "fund_profit_streak"): m = eval_store.get_detail(eval_db, out["run_id"], name)["metrics"] 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") out = run_batch_eval( factor_names=["roc_5"], start="2023-02-01", end="2023-03-31", eval_db=eval_db, label="px_t", cfg=None, vnpy_db_override=db, ) assert out["errors"] == [] assert out["factors_done"] == 1