Files
Profeto/src/api/routes/backtest.py
T
WorkBuddy c89bfe2af7 fix(P2): no_data 结构化、权重校验、鉴权与前端竞态
P2-1 no_data 门控依赖文案子串(脆弱):
- context_builder 新增 SliceResult(text/has_data/n_records),
  5 个切片函数改为显式声明 has_data
- base._slice_has_data() 优先取结构化结果,str 返回仍走文案回退
  (兼容既有测试 mock 与自定义切片)
- build_context 的 has_stats/has_injuries 直接取切片声明

P2-2 agent_weights 无校验即落库:
- validation 新增 AgentWeightsSchema / validate_agent_weights:
  未知专家名丢弃、越界值钳制、总和非 1 时归一化
- orchestrator 落库前对 agent_weights 做校验

P2-3 1x2 与比分不一致被静默修正:
- 仍以比分修正,但补 logger.warning 暴露 LLM 自相矛盾

P2-5/P2-6 prompt 缓存不可刷新 + 缓存键不含模板内容:
- 新增 clear_prompt_cache() 供改模板后显式失效
- 缓存键纳入模板内容 hash,模板一改缓存自动失效

P2-7 ingest/backtest/settle 接口无鉴权:
- 新增 require_admin_key 依赖(X-API-Key),
  ADMIN_API_KEY 未设置时放行并告警(不破坏本地开发)
- 挂到 3 个 ingest 接口 + backtest + eval/settle

P2-8 前端请求竞态 + 未使用游标分页:
- Matches.tsx 用递增 seq 丢弃过期响应,避免旧筛选结果覆盖新筛选
- 接入后端已有的 cursor 分页 + 「加载更多」按钮

附带: .env.example 补齐 LLM_TIMEOUT / 分档模型 / ADMIN_API_KEY;
tests 新增 10 个用例覆盖 P2-1/2/3。
2026-09-15 16:54:05 +08:00

79 lines
2.7 KiB
Python

"""回测路由。"""
from __future__ import annotations
import logging
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from src.api.deps import require_admin_key
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_key)])
async def backtest(req: BacktestRequest):
"""对历史比赛运行回测。
该接口会对每场已完赛比赛各发起一次 LLM 预测,成本高 —— 因此需要
`X-API-Key` 鉴权(见审查报告 P2-7)。
对每场已完赛比赛:
1. 用比赛之前的数据构建上下文 (防未来信息泄漏)
2. 调 LLM 预测
3. 用实际比分回填
4. 统计准确率 / RMSE / 校准度
"""
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,
"accuracy_1x2": summary.accuracy_1x2,
"avg_score_rmse": summary.avg_score_rmse,
"avg_subjective_confidence": summary.avg_subjective_confidence,
"calibration": summary.calibration,
},
"results": [
{
"match_id": r.match_id,
"league_code": r.league_code,
"home_team": r.home_team,
"away_team": r.away_team,
"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
],
}