Files
sanguo_vnpy_v2/tests/api/test_factor_eval_routes.py
T

117 lines
4.8 KiB
Python

"""/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)