"""回测路由。""" from __future__ import annotations import logging from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel, Field from src.api.deps import require_admin from src.llm.backtest import run_backtest logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/v1", tags=["backtest"]) class BacktestRequest(BaseModel): league_id: int | None = Field(None, description="联赛 ID") date_from: str | None = Field(None, description="起始日期 YYYY-MM-DD") date_to: str | None = Field(None, description="结束日期 YYYY-MM-DD") mode: str = Field("single", description="预测模式: single(快) / multi(多 agent)") limit: int = Field(20, ge=1, le=200, description="最大回测场数") model: str | None = Field(None, description="指定模型 (空=默认)") @router.post("/backtest", dependencies=[Depends(require_admin)]) async def backtest(req: BacktestRequest): """对历史比赛运行回测。 该接口会对每场已完赛比赛各发起一次 LLM 预测,成本高 —— 因此需要 管理员鉴权(require_admin)。 对每场已完赛比赛: 1. 用比赛之前的数据构建上下文 (防未来信息泄漏,cutoff=match_date-1天) 2. 调 LLM 预测(强制 use_cache=False,避免缓存命中导致反复 settle 同一行) 3. 用实际比分回填(settle) 4. 统计准确率 / RMSE / 校准度 限流:单请求上限 200 场(默认 20),避免一次打爆 LLM 额度。 回测写入 run_type='backtest',与实盘(live)互不覆盖(唯一键含 run_type)。 """ try: summary = await run_backtest( league_id=req.league_id, date_from=req.date_from, date_to=req.date_to, mode=req.mode, limit=req.limit, model=req.model, ) except Exception as e: logger.exception("backtest failed") raise HTTPException(500, "回测执行失败,请查看服务器日志") return { "summary": { "total": summary.total, "scored": summary.scored, "success": summary.success, "degraded": summary.degraded, "accuracy_1x2": summary.accuracy_1x2, "avg_score_rmse": summary.avg_score_rmse, "avg_subjective_confidence": summary.avg_subjective_confidence, }, "results": [ { "match_id": r.match_id, "league_code": r.league_code, "home_team": r.home_team, "away_team": r.away_team, "home_team_zh": r.home_team_zh, "away_team_zh": r.away_team_zh, "match_date": r.match_date, "actual_score": f"{r.actual_home}-{r.actual_away}", "actual_1x2": r.actual_1x2, "pred_home": r.pred_home, "pred_away": r.pred_away, "pred_1x2": r.pred_1x2, "subjective_confidence": r.subjective_confidence, "correct_1x2": r.correct_1x2, } for r in summary.results ], }