From 59da65b839da9ac3cafa57b64a04adbdf2c6bcc9 Mon Sep 17 00:00:00 2001 From: claude_dev Date: Sat, 11 Jul 2026 22:10:48 +0800 Subject: [PATCH] =?UTF-8?q?fix(backtest):=20=E7=BB=93=E6=9E=9C=E9=A1=B5?= =?UTF-8?q?=E6=8C=87=E6=A0=87=E5=85=A8=E6=98=BE"=E2=80=94"=20+=20=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E9=87=8D=E5=90=AF=E5=90=8E404?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 两个根因: 1. relative_metrics 缺字段(routes.py): get_result 的 relative_fields 列表 漏了 total_return/annual_return/sharpe_ratio/max_drawdown 4 个字段, 导致 这 4 个指标永远进不了响应 → 前端 MetricCards 显"—"。补全为 11 字段。 2. 任务 task_id 不持久(runner/cta_engine): submit_cta 用 cta_{symbol}_{id(params)} (内存地址) 作 runner id, 而 run_cta_backtest 内部另生成 cta_{uuid} 存 DB, 两者持久层不相交 → 重启后 pool 内存映射丢失, get_result(runner_id) 的 load_result_by_task_id 查不到 → /result 404 → 前端指标全 0 + 图表无数据。 改为 submit 前置生成稳定 uuid, 透传给 run_cta_backtest 复用, 使 runner-id == DB task_id (单一 id, 重启可查)。 附: Dashboard 最近任务"策略/因子"列 min-width 150→190(长策略名不再截断) 测试: test_runner task_id 断言同步新格式 --- frontend/src/views/Dashboard.vue | 2 +- sanguo_api/routes.py | 3 ++- sanguo_backtest/cta_engine.py | 10 +++++++--- sanguo_orchestrator/runner.py | 11 +++++++---- tests/orchestrator/test_runner.py | 6 ++++-- 5 files changed, 21 insertions(+), 11 deletions(-) diff --git a/frontend/src/views/Dashboard.vue b/frontend/src/views/Dashboard.vue index dc8c847..3ced6ca 100644 --- a/frontend/src/views/Dashboard.vue +++ b/frontend/src/views/Dashboard.vue @@ -108,7 +108,7 @@ function statusLabel(s: string): string { - + diff --git a/sanguo_api/routes.py b/sanguo_api/routes.py index fa51c01..f62971a 100644 --- a/sanguo_api/routes.py +++ b/sanguo_api/routes.py @@ -139,8 +139,9 @@ def get_result(task_id: str): # Extract relative metrics from statistics relative_metrics = {} relative_fields = [ + "total_return", "annual_return", "sharpe_ratio", "max_drawdown", "alpha", "beta", "sortino_ratio", "information_ratio", - "annual_volatility", "benchmark_return", "benchmark_volatility" + "annual_volatility", "benchmark_return", "benchmark_volatility", ] for field in relative_fields: if field in r.statistics: diff --git a/sanguo_backtest/cta_engine.py b/sanguo_backtest/cta_engine.py index dd2ba87..ff315c4 100644 --- a/sanguo_backtest/cta_engine.py +++ b/sanguo_backtest/cta_engine.py @@ -48,7 +48,7 @@ def guess_exchange(symbol: str) -> Exchange: return Exchange("SSE") -def run_cta_backtest(strategy_class, symbol: str, params: dict, start: str, end: str, cfg, db_path: str, benchmark: str = "hs300") -> BacktestResult: +def run_cta_backtest(strategy_class, symbol: str, params: dict, start: str, end: str, cfg, db_path: str, benchmark: str = "hs300", task_id: str | None = None) -> BacktestResult: """ Run CTA strategy backtest using vnpy_ctastrategy BacktestingEngine. @@ -60,12 +60,16 @@ def run_cta_backtest(strategy_class, symbol: str, params: dict, start: str, end: end: Backtest end date (YYYY-MM-DD format) cfg: Configuration object (may contain data paths) db_path: SQLite database path for saving results + benchmark: Benchmark code (hs300/zz500) + task_id: Optional task ID from runner (reused as the persisted task_id so + runner-id == DB task_id; if omitted a fresh uuid is generated). Returns: BacktestResult: Result object with backtest statistics and status """ - # Generate unique task ID - task_id = f"cta_{uuid.uuid4().hex[:8]}" + # Use runner-provided task_id (durable, single id across pool/DB/URL) or generate + if not task_id: + task_id = f"cta_{uuid.uuid4().hex[:8]}" try: # Lazy import of BacktestingEngine (local env may not have vnpy_ctastrategy) diff --git a/sanguo_orchestrator/runner.py b/sanguo_orchestrator/runner.py index 5148490..d0be289 100644 --- a/sanguo_orchestrator/runner.py +++ b/sanguo_orchestrator/runner.py @@ -3,6 +3,7 @@ Orchestrator for task coordination and execution Manages backtesting tasks with lazy imports """ import asyncio +import uuid from concurrent.futures import Future from .pool import TaskPool from .task import TaskState @@ -32,7 +33,9 @@ class Orchestrator: async def submit_cta(self, strategy_class, symbol: str, params: dict, start: str, end: str, cfg, benchmark: str = "hs300") -> str: """Submit a CTA backtesting task asynchronously""" - task_id = f"cta_{symbol}_{id(params)}" + # Stable uuid up front → reused as the persisted DB task_id, so runner-id == + # DB task_id (durable across restarts; previously used id(params) memory addr). + task_id = f"cta_{uuid.uuid4().hex[:8]}" self.pool.submit(task_id, "cta") self._pending[task_id] = dict( strategy_class=strategy_class, @@ -48,7 +51,7 @@ class Orchestrator: spec = self._pending[task_id] fut: Future = self.pool.submit_work( task_id, _cta_worker, spec["strategy_class"], spec["symbol"], - spec["params"], spec["start"], spec["end"], spec["cfg"], spec["benchmark"], self.db_path + spec["params"], spec["start"], spec["end"], spec["cfg"], spec["benchmark"], self.db_path, task_id ) task = self.pool.get_task(task_id) @@ -165,10 +168,10 @@ class Orchestrator: # Module-level worker functions (must be top-level for ProcessPoolExecutor pickle) -def _cta_worker(strategy_class, symbol: str, params: dict, start: str, end: str, cfg, benchmark: str, db_path: str) -> any: +def _cta_worker(strategy_class, symbol: str, params: dict, start: str, end: str, cfg, benchmark: str, db_path: str, task_id: str) -> any: """Worker for CTA backtest (lazy import, spawn-friendly)""" from sanguo_backtest.cta_engine import run_cta_backtest - return run_cta_backtest(strategy_class, symbol, params, start, end, cfg, db_path, benchmark=benchmark) + return run_cta_backtest(strategy_class, symbol, params, start, end, cfg, db_path, benchmark=benchmark, task_id=task_id) def _opt_worker(strategy_class, symbol: str, grid: dict, start: str, end: str, cfg, db_path: str) -> any: diff --git a/tests/orchestrator/test_runner.py b/tests/orchestrator/test_runner.py index ca1e1f7..9837aeb 100644 --- a/tests/orchestrator/test_runner.py +++ b/tests/orchestrator/test_runner.py @@ -52,7 +52,9 @@ class TestOrchestrator: assert orchestrator._pending[task_id]["end"] == end assert orchestrator._pending[task_id]["cfg"] == cfg - assert task_id.startswith("cta_AAPL_") + # Durable uuid format: cta_<8hex> (reused as persisted DB task_id) + assert task_id.startswith("cta_") + assert task_id != "cta_AAPL_" and len(task_id) == len("cta_") + 8 @patch('sanguo_orchestrator.runner.TaskPool') def test_get_status(self, mock_pool_class): @@ -144,7 +146,7 @@ class TestOrchestratorAsync: Mock(), "600000", {}, "2024-01-01", "2024-06-30", cfg=Mock() ) - assert task_id.startswith("cta_600000") + assert task_id.startswith("cta_") and len(task_id) == len("cta_") + 8 orchestrator.pool.executor.submit.assert_called_once() @pytest.mark.asyncio