"""/factor/eval/* 端点:leaderboard 扁平化/tiles/排序/详情/提交校验.""" import sys, os sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))) import pytest from fastapi import FastAPI from fastapi.testclient import TestClient from sanguo_factor import eval_store from sanguo_api import routes_factor from sanguo_api.app import create_app from sanguo_api.auth import hash_password @pytest.fixture() def eval_db(tmp_path): db = str(tmp_path / "factor_eval.db") eval_store.init_db(db) routes_factor.set_eval_db_path(db) return db @pytest.fixture() def client(tmp_path): db_path = str(tmp_path / "test.db") app = create_app( db_path=db_path, auth_config={ "username": "admin", "password_hash": hash_password("admin"), "jwt_secret": "test-secret", "expire_minutes": 60, }, max_workers=1, ) # Set eval db path eval_db_path = str(tmp_path / "factor_eval.db") eval_store.init_db(eval_db_path) from sanguo_orchestrator.runner import resolve_eval_db routes_factor.set_eval_db_path(resolve_eval_db(db_path)) return TestClient(app) @pytest.fixture() def token(client): return client.post("/api/v1/auth/login", json={"username": "admin", "password": "admin"}).json()["token"] def _seed(db): run_id = eval_store.create_run(db, label="冒烟", universe="custom", symbols_count=50, factors_total=3, start="2024-01-01", end="2024-12-31", params={}) eval_store.save_results(db, run_id, [ {"factor": "alpha2", "category": "alpha101", "expression": "e2", "metrics": {"turnover": 0.2, "1": {"ic_mean": 0.05, "icir": 0.4, "t_stat": 3.0, "win_rate": 0.6, "ls_annual": 0.12, "conclusion": "effective", "monthly_ic": [{"month": "2024-01", "ic": 0.05}]}, "5": {}, "10": {}}}, {"factor": "kmid", "category": "alpha158", "expression": "ek", "metrics": {"turnover": 0.5, "1": {"ic_mean": -0.01, "icir": -0.05, "t_stat": -0.4, "win_rate": 0.45, "ls_annual": -0.02, "conclusion": "eliminated", "monthly_ic": []}, "5": {}, "10": {}}}, {"factor": "alpha6", "category": "alpha101", "expression": "e6", "metrics": {"turnover": 0.3, "1": {"ic_mean": 0.03, "icir": 0.2, "t_stat": 1.7, "win_rate": 0.55, "ls_annual": 0.06, "conclusion": "watch", "monthly_ic": []}, "5": {}, "10": {}}}, ]) eval_store.finish_run(db, run_id, "done", 3) return run_id def test_leaderboard_default_latest_run(client, token, eval_db): _seed(eval_db) r = client.get("/api/v1/factor/eval/leaderboard", headers={"Authorization": f"Bearer {token}"}) assert r.status_code == 200 body = r.json() assert body["tiles"]["factors_total"] == 3 assert body["tiles"]["effective"] == 1 assert body["tiles"]["watch"] == 1 assert body["tiles"]["top_ls_factor"] == "alpha2" # 默认 abs_icir desc: alpha2(0.4) > alpha6(0.2) > kmid(0.05) assert [x["factor"] for x in body["rows"]] == ["alpha2", "alpha6", "kmid"] row = body["rows"][0] for key in ("factor", "category", "expression", "ic_mean", "icir", "t_stat", "win_rate", "ls_annual", "turnover", "conclusion", "monthly_ic"): assert key in row def test_leaderboard_filter_and_period(client, token, eval_db): _seed(eval_db) r = client.get("/api/v1/factor/eval/leaderboard", params={"category": "alpha158"}, headers={"Authorization": f"Bearer {token}"}) assert [x["factor"] for x in r.json()["rows"]] == ["kmid"] r = client.get("/api/v1/factor/eval/leaderboard", params={"search": "alpha6"}, headers={"Authorization": f"Bearer {token}"}) assert [x["factor"] for x in r.json()["rows"]] == ["alpha6"] def test_detail(client, token, eval_db): _seed(eval_db) r = client.get("/api/v1/factor/eval/detail", params={"factor": "kmid"}, headers={"Authorization": f"Bearer {token}"}) assert r.status_code == 200 assert r.json()["category"] == "alpha158" assert "1" in r.json()["metrics"] assert client.get("/api/v1/factor/eval/detail", params={"factor": "nope"}, headers={"Authorization": f"Bearer {token}"}).status_code == 404 def test_runs_list(client, token, eval_db): _seed(eval_db) r = client.get("/api/v1/factor/eval/runs", headers={"Authorization": f"Bearer {token}"}) assert len(r.json()["runs"]) == 1 def test_submit_validation_error(client, token): # categories 与 factors 双空 → 422/400 r = client.post("/api/v1/factor/eval/submit", json={"start": "2018-01-01", "end": "2026-06-30"}, headers={"Authorization": f"Bearer {token}"}) assert r.status_code in (400, 422)