mirror of
https://ghfast.top/https://github.com/aeroxw/easy-tdx.git
synced 2026-09-12 19:14:16 +08:00
feat(backtest): 回测结果对比页
选 2-4 个已完成的回测 task,叠加净值曲线 + 横向指标对比。 后端(最小增量): - task_runner.list_recent(limit):返回最近 N 个任务摘要(LRU 倒序) - GET /backtest/tasks?limit=20:任务摘要列表端点(不含完整 result) - TaskSummary / TaskListResponse schema 前端(/compare 对比页): - CompareView:左栏勾选已完成 task,右栏叠加对比 - CompareChart:多 task 净值叠加图(归一化为初始=1) - CompareTable:多 task 指标横向对比表(8 项指标) - 按 result 结构判断可对比性(仅单标的 BacktestResult 可对比) 复用现有 task_runner LRU 表,无新增存储。测试:823 passed(+2 列表端点)
This commit is contained in:
@@ -185,6 +185,23 @@ class TaskStateResponse(BaseModel):
|
||||
elapsed: float = 0.0
|
||||
|
||||
|
||||
class TaskSummary(BaseModel):
|
||||
"""任务摘要(列表用,不含完整 result)。"""
|
||||
|
||||
task_id: str
|
||||
status: Literal["pending", "running", "done", "failed"]
|
||||
description: str = ""
|
||||
created_at: float = 0.0
|
||||
elapsed: float = 0.0
|
||||
|
||||
|
||||
class TaskListResponse(BaseModel):
|
||||
"""任务摘要列表响应。"""
|
||||
|
||||
tasks: list[TaskSummary]
|
||||
count: int
|
||||
|
||||
|
||||
# ── 结果序列化 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
@@ -22,8 +22,10 @@ from easy_tdx.web.backtest_schemas import (
|
||||
OptimizeBacktestRequest,
|
||||
PortfolioBacktestRequest,
|
||||
StrategySchemaResponse,
|
||||
TaskListResponse,
|
||||
TaskStateResponse,
|
||||
TaskSubmitResponse,
|
||||
TaskSummary,
|
||||
serialize_result,
|
||||
)
|
||||
from easy_tdx.web.deps import get_client
|
||||
@@ -101,6 +103,29 @@ async def run_backtest_async(
|
||||
return TaskSubmitResponse(task_id=task_id, status=status)
|
||||
|
||||
|
||||
@router.get("/backtest/tasks", response_model=TaskListResponse)
|
||||
async def list_tasks(limit: int = 20) -> TaskListResponse:
|
||||
"""列出最近 N 个任务摘要(按最近使用倒序,不含完整 result)。
|
||||
|
||||
供对比页选择要对比的 task;选中后再逐个调 /tasks/{task_id} 拉详情。
|
||||
"""
|
||||
import time
|
||||
|
||||
runner = get_runner()
|
||||
states = runner.list_recent(limit)
|
||||
summaries = [
|
||||
TaskSummary(
|
||||
task_id=s.task_id,
|
||||
status=s.status,
|
||||
description=s.description,
|
||||
created_at=s.created_at,
|
||||
elapsed=(s.finished_at or time.time()) - (s.started_at or s.created_at),
|
||||
)
|
||||
for s in states
|
||||
]
|
||||
return TaskListResponse(tasks=summaries, count=len(summaries))
|
||||
|
||||
|
||||
@router.get("/backtest/tasks/{task_id}", response_model=TaskStateResponse)
|
||||
async def get_task(task_id: str) -> TaskStateResponse:
|
||||
"""查询后台回测任务状态。done 时 result 字段含完整回测结果。"""
|
||||
|
||||
@@ -131,6 +131,17 @@ class BacktestTaskRunner:
|
||||
with self._lock:
|
||||
return self._tasks.get(task_id)
|
||||
|
||||
def list_recent(self, limit: int = 20) -> list[TaskState]:
|
||||
"""返回最近 N 个任务(按完成/创建时间倒序,LRU 表尾=最近)。
|
||||
|
||||
Args:
|
||||
limit: 最多返回的任务数(默认 20)。
|
||||
"""
|
||||
with self._lock:
|
||||
# OrderedDict 尾部是最近使用的(done 时 move_to_end);倒序取
|
||||
items = list(reversed(self._tasks.values()))
|
||||
return items[:limit]
|
||||
|
||||
def status(self, task_id: str) -> TaskStatus | None:
|
||||
"""取任务状态字符串,不存在返回 None。"""
|
||||
state = self.peek(task_id)
|
||||
|
||||
Reference in New Issue
Block a user