# 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"])}