refactor: fix P0/P1/P2 security and performance issues
P0 fixes: - CORS: replace wildcard methods/headers with configurable lists - deps.py: remove unsafe global _warned_unset variable P1 fixes: - http_client: read default timeout from Settings - bzzoiro: replace sync urllib with async httpx - bzzoiro: normalize validation failures use warning level only - db pool: read pool config from Settings (default 5+10) - backtest: add asyncio.Semaphore(8) for concurrent execution - predict/context_builder: add backtest parameter for cutoff buffer P2 improvements: - injuries: enforce int conversion for player_id/fixture_id - injuries: use system temp dir for cache - utils.py: extract shared actual_1x2/is_correct_1x2 - validation: downgrade 1x2 mismatch log to debug - docker-compose: use env vars for all credentials - .env.example: add POSTGRES_USER/PASSWORD/PORT, API_PORT
This commit is contained in:
+35
-43
@@ -3,9 +3,11 @@
|
||||
核心机制:
|
||||
- build_context 已内置 before=match_date,天然防未来信息泄漏
|
||||
- 对历史比赛跑预测 → 用实际比分 settle → 统计准确率
|
||||
- 并发控制: asyncio.Semaphore 限制同时 LLM 调用数
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
@@ -17,6 +19,7 @@ from src.db.models import Match
|
||||
from src.db.unit_of_work import get_uow
|
||||
from src.llm.eval import settle_prediction
|
||||
from src.llm.predict import predict_match
|
||||
from src.llm.utils import actual_1x2
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -69,15 +72,6 @@ class BacktestSummary:
|
||||
results: list[BacktestMatchResult] = field(default_factory=list)
|
||||
|
||||
|
||||
def _actual_1x2(home: int, away: int) -> str:
|
||||
"""实际比分 → 胜平负。"""
|
||||
if home > away:
|
||||
return "1"
|
||||
if home == away:
|
||||
return "X"
|
||||
return "2"
|
||||
|
||||
|
||||
async def _get_historical_matches(
|
||||
db,
|
||||
*,
|
||||
@@ -156,44 +150,42 @@ async def run_backtest(
|
||||
|
||||
summary = BacktestSummary(total=len(candidates), scored=0)
|
||||
|
||||
for c in candidates:
|
||||
try:
|
||||
# 预测 (build_context 内部已用 before=match_date 防泄漏,
|
||||
# injuries_slice 也使用 as_of=match_date 过滤 retrieved_at)
|
||||
# 回测必须禁用结果缓存: 否则命中缓存会复用同一 prediction_id,
|
||||
# 导致 settle 反复覆盖同一条记录(见 P1-3)。
|
||||
result = await predict_match(c.match_id, mode=mode, model=model, use_cache=False)
|
||||
# P1-6: 并发控制,同时最多 8 场预测(避免 LLM API 限流)
|
||||
sem = asyncio.Semaphore(8)
|
||||
|
||||
# 用实际比分 settle
|
||||
await settle_prediction(result.prediction_id, c.home_goals, c.away_goals)
|
||||
async def _one(c: BacktestCandidate) -> BacktestMatchResult | None:
|
||||
async with sem:
|
||||
try:
|
||||
result = await predict_match(c.match_id, mode=mode, model=model, use_cache=False, backtest=True)
|
||||
await settle_prediction(result.prediction_id, c.home_goals, c.away_goals)
|
||||
actual = actual_1x2(c.home_goals, c.away_goals)
|
||||
return BacktestMatchResult(
|
||||
match_id=c.match_id,
|
||||
league_code=c.league_code,
|
||||
home_team=c.home_team,
|
||||
away_team=c.away_team,
|
||||
match_date=c.match_date.strftime("%Y-%m-%d") if c.match_date else "?",
|
||||
actual_home=c.home_goals,
|
||||
actual_away=c.away_goals,
|
||||
actual_1x2=actual,
|
||||
pred_home=result.pred_home_goals,
|
||||
pred_away=result.pred_away_goals,
|
||||
pred_1x2=result.pred_1x2,
|
||||
subjective_confidence=result.subjective_confidence,
|
||||
correct_1x2=result.pred_1x2 == actual,
|
||||
prediction_id=result.prediction_id,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("backtest match %s failed", c.match_id)
|
||||
return None
|
||||
|
||||
actual = _actual_1x2(c.home_goals, c.away_goals)
|
||||
correct = result.pred_1x2 == actual
|
||||
|
||||
bt = BacktestMatchResult(
|
||||
match_id=c.match_id,
|
||||
league_code=c.league_code,
|
||||
home_team=c.home_team,
|
||||
away_team=c.away_team,
|
||||
match_date=c.match_date.strftime("%Y-%m-%d") if c.match_date else "?",
|
||||
actual_home=c.home_goals,
|
||||
actual_away=c.away_goals,
|
||||
actual_1x2=actual,
|
||||
pred_home=result.pred_home_goals,
|
||||
pred_away=result.pred_away_goals,
|
||||
pred_1x2=result.pred_1x2,
|
||||
subjective_confidence=result.subjective_confidence,
|
||||
correct_1x2=correct,
|
||||
prediction_id=result.prediction_id,
|
||||
)
|
||||
summary.results.append(bt)
|
||||
# 并行执行,保持结果顺序
|
||||
results = await asyncio.gather(*[_one(c) for c in candidates])
|
||||
for r in results:
|
||||
if r is not None:
|
||||
summary.results.append(r)
|
||||
summary.scored += 1
|
||||
|
||||
except Exception:
|
||||
# 用 exception 而非 warning:保留堆栈,否则集成层缺陷(如惰性加载
|
||||
# 在 session 外触发)会只剩一行无堆栈的 warning,极难定位。
|
||||
logger.exception("backtest match %s failed", c.match_id)
|
||||
|
||||
# 汇总统计
|
||||
if summary.scored > 0:
|
||||
correct_count = sum(1 for r in summary.results if r.correct_1x2)
|
||||
|
||||
Reference in New Issue
Block a user