From 94de7191f58a92214ad52d72285830d4c1f42904 Mon Sep 17 00:00:00 2001 From: claude_dev Date: Fri, 14 Aug 2026 08:29:20 +0800 Subject: [PATCH] =?UTF-8?q?feat(backtest):=20=E4=BB=BB=E5=8A=A1=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E6=97=B6=E9=97=B4=E8=A1=A8task=5Fsubmissions+?= =?UTF-8?q?=E6=8C=89task=5Fid=E5=88=A0=E7=BB=93=E6=9E=9C(=E8=A1=8C+?= =?UTF-8?q?=E6=9B=B2=E7=BA=BF=E6=96=87=E4=BB=B6+=E6=8F=90=E4=BA=A4?= =?UTF-8?q?=E8=AE=B0=E5=BD=95);=20record=5Fsubmission/load=5Fsubmissions/d?= =?UTF-8?q?elete=5Fresult=5Fby=5Ftask=5Fid=20[vps]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sanguo_backtest/result_store.py | 74 +++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/sanguo_backtest/result_store.py b/sanguo_backtest/result_store.py index eaaa06b..b1dcdc1 100644 --- a/sanguo_backtest/result_store.py +++ b/sanguo_backtest/result_store.py @@ -1,4 +1,5 @@ """Backtest result storage using SQLite + parquet files.""" +import os import sqlite3 import json import logging @@ -65,6 +66,13 @@ _SCHEMA = """CREATE TABLE IF NOT EXISTS backtest_stats ( trades_path TEXT, error_msg TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 提交时间表:任务提交即写一行(backtest_stats.created_at 是存结果时=完成时间, +-- 不是真创建时间)。完成后两表相减即真实耗时。 +CREATE TABLE IF NOT EXISTS task_submissions ( + task_id TEXT PRIMARY KEY, + submitted_at TIMESTAMP );""" @@ -133,6 +141,72 @@ def save_result(result: BacktestResult, db_path: str, file_dir: Optional[str] = conn.close() +def record_submission(task_id: str, db_path: str) -> str: + """Record task submission time (UTC, same format as CURRENT_TIMESTAMP). + + 提交即写:真创建时间。失败只 warning 不抛——时间戳缺失只影响展示, + 不应影响任务提交本身。 + """ + from datetime import datetime, timezone + ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S") + try: + conn = _connect(db_path) + try: + conn.execute( + "INSERT OR REPLACE INTO task_submissions (task_id, submitted_at) VALUES (?, ?)", + (task_id, ts), + ) + conn.commit() + finally: + conn.close() + except Exception as e: + logger.warning("record_submission %s failed: %s", task_id, e) + return ts + + +def load_submissions(db_path: str) -> dict[str, str]: + """Return {task_id: submitted_at(UTC)} for all recorded submissions.""" + conn = _connect(db_path) + try: + return { + row[0]: row[1] + for row in conn.execute("SELECT task_id, submitted_at FROM task_submissions") + } + finally: + conn.close() + + +def delete_result_by_task_id(task_id: str, db_path: str) -> int: + """Delete all persisted traces of a task: backtest_stats rows + + equity/trades JSON files + task_submissions row. + + Returns deleted backtest_stats row count. Missing files tolerated + (historical records may reference stale paths after host migration). + """ + conn = _connect(db_path) + try: + rows = conn.execute( + "SELECT equity_path, trades_path FROM backtest_stats WHERE task_id=?", + (task_id,), + ).fetchall() + for equity_path, trades_path in rows: + for path in (equity_path, trades_path): + if not path: + continue + try: + os.remove(path) + except OSError: + pass + n = conn.execute( + "DELETE FROM backtest_stats WHERE task_id=?", (task_id,) + ).rowcount + conn.execute("DELETE FROM task_submissions WHERE task_id=?", (task_id,)) + conn.commit() + return n + finally: + conn.close() + + def _safe_read_json(path: Optional[str]) -> Optional[pd.DataFrame]: """Read a JSON equity/trades file; return None if missing or unreadable.