Files
sanguo_vnpy_v2/sanguo_factor/eval_store.py
T

152 lines
4.4 KiB
Python

# sanguo_factor/eval_store.py
"""批量评估结果落盘 factor_eval.db(eval_runs + eval_results).
metrics 按周期 {"1": {...}, "5": {...}, "10": {...}, "turnover": float} 存 JSON,
schema 平坦、加周期零迁移。
"""
import json
import os
import sqlite3
import uuid
from datetime import datetime
_SCHEMA = """
CREATE TABLE IF NOT EXISTS eval_runs(
run_id TEXT PRIMARY KEY,
label TEXT NOT NULL,
universe TEXT NOT NULL,
symbols_count INTEGER NOT NULL,
factors_total INTEGER NOT NULL,
start TEXT NOT NULL,
end TEXT NOT NULL,
status TEXT NOT NULL,
created_at TEXT NOT NULL,
finished_at TEXT,
factors_done INTEGER NOT NULL DEFAULT 0,
params_json TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS eval_results(
run_id TEXT NOT NULL,
factor TEXT NOT NULL,
category TEXT NOT NULL,
expression TEXT NOT NULL,
metrics_json TEXT NOT NULL,
PRIMARY KEY(run_id, factor)
);
"""
def default_eval_db_path() -> str:
"""SANGUO_FACTOR_EVAL_DB 覆盖;默认与 backtest_results.db 同目录."""
env = os.environ.get("SANGUO_FACTOR_EVAL_DB")
if env:
return env
from sanguo_data.config import load_config, find_config_path
cfg = load_config(find_config_path())
vnpy_db = cfg.data_paths.get("vnpy_db", "")
return os.path.join(os.path.dirname(os.path.abspath(vnpy_db)), "factor_eval.db")
def _conn(path: str) -> sqlite3.Connection:
conn = sqlite3.connect(path, timeout=30)
conn.execute("PRAGMA busy_timeout=30000")
return conn
def init_db(path: str) -> None:
os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
conn = _conn(path)
try:
conn.executescript(_SCHEMA)
conn.commit()
finally:
conn.close()
def create_run(path, label, universe, symbols_count, factors_total, start, end, params) -> str:
run_id = f"ev_{datetime.now():%Y%m%d_%H%M%S}_{uuid.uuid4().hex[:4]}"
conn = _conn(path)
try:
conn.execute(
"INSERT INTO eval_runs VALUES(?,?,?,?,?,?,?,?,?,?,0,?)",
(run_id, label, universe, symbols_count, factors_total, start, end,
"running", datetime.now().isoformat(timespec='seconds'), None,
json.dumps(params, ensure_ascii=False)),
)
conn.commit()
finally:
conn.close()
return run_id
def save_results(path, run_id, rows: list[dict]) -> None:
conn = _conn(path)
try:
conn.executemany(
"INSERT OR REPLACE INTO eval_results VALUES(?,?,?,?,?)",
[(run_id, r["factor"], r["category"], r["expression"],
json.dumps(r["metrics"], ensure_ascii=False)) for r in rows],
)
cur = conn.execute(
"SELECT COUNT(*) FROM eval_results WHERE run_id=?", (run_id,))
done = cur.fetchone()[0]
conn.execute("UPDATE eval_runs SET factors_done=? WHERE run_id=?", (done, run_id))
conn.commit()
finally:
conn.close()
def finish_run(path, run_id, status, factors_done) -> None:
conn = _conn(path)
try:
conn.execute(
"UPDATE eval_runs SET status=?, finished_at=?, factors_done=? WHERE run_id=?",
(status, datetime.now().isoformat(timespec='seconds'), factors_done, run_id),
)
conn.commit()
finally:
conn.close()
def list_runs(path) -> list[dict]:
conn = _conn(path)
conn.row_factory = sqlite3.Row
try:
rows = conn.execute(
"SELECT * FROM eval_runs ORDER BY created_at DESC").fetchall()
finally:
conn.close()
return [dict(r) for r in rows]
def get_rows(path, run_id, category=None, search=None) -> list[dict]:
q = "SELECT * FROM eval_results WHERE run_id=?"
args: list = [run_id]
if category:
q += " AND category=?"
args.append(category)
if search:
q += " AND (factor LIKE ? OR expression LIKE ?)"
args.extend([f"%{search}%", f"%{search}%"])
conn = _conn(path)
conn.row_factory = sqlite3.Row
try:
rows = conn.execute(q, args).fetchall()
finally:
conn.close()
return [{**dict(r), "metrics": json.loads(r["metrics_json"])} for r in rows]
def get_detail(path, run_id, factor) -> dict | None:
conn = _conn(path)
conn.row_factory = sqlite3.Row
try:
r = conn.execute(
"SELECT * FROM eval_results WHERE run_id=? AND factor=?",
(run_id, factor)).fetchone()
finally:
conn.close()
if r is None:
return None
return {**dict(r), "metrics": json.loads(r["metrics_json"])}