152 lines
4.4 KiB
Python
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"])}
|