"""极简基线预测:主客场场均进球估计(不调用 LLM,不产生费用)。 用于与 LLM 预测做 eval 对比。这是最简单的统计基线,仅供研究参考, 文档与 reasoning 均明确标注「非投注建议」。 """ from __future__ import annotations import hashlib import logging from datetime import datetime, timedelta, timezone from sqlalchemy import case, func, select from src.db.base import AsyncSession, AsyncSessionLocal from src.db.models import Match from src.llm.predict import PredictResult, _insert_or_find_by_fingerprint logger = logging.getLogger(__name__) async def _avg_goals( db: AsyncSession, *, team_id: int, side: str, league_id: int, before: datetime | None, ) -> float: """某队在该联赛已完赛场次的场均进球(side=home/away)。""" if side == "home": goals_col = Match.home_goals team_col = Match.home_team_id else: goals_col = Match.away_goals team_col = Match.away_team_id stmt = ( select(func.avg(goals_col).label("avg_goals"), func.count().label("cnt")) .where( Match.match_status == "finished", team_col == team_id, Match.league_id == league_id, goals_col.is_not(None), ) ) if before is not None: stmt = stmt.where(Match.match_date < before) row = (await db.execute(stmt)).one() return float(row.avg_goals) if row.avg_goals is not None and row.cnt > 0 else 0.0 async def predict_baseline( match_id: int, *, backtest: bool = False, cutoff_at: datetime | None = None, ) -> PredictResult: """极简基线预测:主场场均进球 vs 客场场均进球。 返回 PredictResult(D2 统一结果类型): provider=model="baseline", 不调用 LLM,latency_ms≈0。 P3-2:baseline 落库下沉到服务层 —— 直接在服务层完成落库并回填真实 prediction_id,路由层不再需要特殊的 _persist_baseline,与 single/multi 路径统一(result.prediction_id 即可用)。对外 JSON 不变。 """ from src.db.unit_of_work import get_uow async with AsyncSessionLocal() as db: match = await db.get(Match, match_id) if match is None: raise ValueError(f"match {match_id} not found") before = None if backtest and match.match_date: from datetime import timedelta before = match.match_date - timedelta(days=1) elif cutoff_at is not None: before = cutoff_at home_avg = await _avg_goals( db, team_id=match.home_team_id, side="home", league_id=match.league_id, before=before, ) away_avg = await _avg_goals( db, team_id=match.away_team_id, side="away", league_id=match.league_id, before=before, ) pred_home = max(0, min(10, round(home_avg))) pred_away = max(0, min(10, round(away_avg))) # 主场轻微加成(可选,这里保持极简不额外加权) if pred_home > pred_away: pred_1x2 = "1" elif pred_home < pred_away: pred_1x2 = "2" else: pred_1x2 = "X" # P0-03: 基线指纹——基于主客场场均进球数据(context_hash) + 截止时间 context_hash = hashlib.sha256( f"{home_avg:.4f}:{away_avg:.4f}:{before.isoformat() if before else 'none'}".encode("utf-8") ).hexdigest() values = { "match_id": match_id, "provider": "baseline", "model": "baseline", "mode": "baseline", "run_type": "live", "prompt_version": "baseline_v1", "prompt_hash": hashlib.sha256(b"baseline_v1").hexdigest(), "system_prompt_hash": hashlib.sha256(b"baseline").hexdigest(), "temperature": 0.0, "context_hash": context_hash, "agent_ids": [], "prompt_tokens": 0, "completion_tokens": 0, "latency_ms": 0, "pred_home_goals": float(pred_home), "pred_away_goals": float(pred_away), "pred_1x2": pred_1x2, "subjective_confidence": 0.5, "reasoning": ( f"基线估计(非投注建议): 主队主场场均进球 {home_avg:.2f} → 预测 {pred_home}; " f"客队客场场均进球 {away_avg:.2f} → 预测 {pred_away}。" ), "raw_response": {"home_avg": round(home_avg, 2), "away_avg": round(away_avg, 2)}, "status": "success", } # P0-03:服务层幂等插入,回填真实 prediction_id(与 single/multi 统一)。 async with get_uow() as session: pred = await _insert_or_find_by_fingerprint(session, values=values) prediction_id = pred.id return PredictResult( prediction_id=prediction_id, provider="baseline", model="baseline", prompt_version="baseline_v1", mode="baseline", pred_home_goals=float(pred_home), pred_away_goals=float(pred_away), alt_pred_home_goals=None, alt_pred_away_goals=None, pred_1x2=pred_1x2, subjective_confidence=0.5, reasoning=values["reasoning"], context="", # baseline 不构建 LLM 上下文 status="success", latency_ms=0, prompt_tokens=0, completion_tokens=0, raw=values["raw_response"], )