""" Pydantic v2 response schemas for the V2 Betting Engine. Strictly mirrors the NestJS DTO contract for SingleMatchPredictionPackage. """ from __future__ import annotations from typing import Any from pydantic import BaseModel, Field # ── Sub-models ────────────────────────────────────────────────────────────── class MatchInfo(BaseModel): match_id: str match_name: str = "" home_team: str = "" away_team: str = "" league: str = "" match_date_ms: int = 0 class DataQuality(BaseModel): label: str = Field(default="MEDIUM", description="HIGH | MEDIUM | LOW") score: float = Field(default=0.5, ge=0.0, le=1.0) flags: list[str] = Field(default_factory=list) home_lineup_count: int = 0 away_lineup_count: int = 0 class RiskAssessment(BaseModel): level: str = Field(default="MEDIUM", description="LOW | MEDIUM | HIGH | EXTREME") score: float = Field(default=0.0, ge=0.0, le=1.0) is_surprise_risk: bool = False surprise_type: str | None = None warnings: list[str] = Field(default_factory=list) class PickDetail(BaseModel): market: str = Field(..., description="MS, OU25, BTTS, DC, HT, HTFT, etc.") pick: str = Field(..., description="1, X, 2, Over, Under, Yes, No, 1/1, etc.") probability: float = Field(..., ge=0.0, le=1.0) confidence: float = Field(default=0.0, description="Percentage 0-100") odds: float | None = Field(default=None, gt=0.0) raw_confidence: float = 0.0 calibrated_confidence: float = 0.0 min_required_confidence: float = 0.0 edge: float = Field(default=0.0, description="Model prob minus implied prob") play_score: float = Field(default=0.0, ge=0.0, le=100.0) playable: bool = False bet_grade: str = Field(default="PASS", description="A | B | C | PASS") stake_units: float = Field(default=0.0, ge=0.0) decision_reasons: list[str] = Field(default_factory=list) class BetAdvice(BaseModel): playable: bool = False suggested_stake_units: float = 0.0 reason: str = "no_playable_pick" class BetSummaryRow(BaseModel): market: str pick: str raw_confidence: float = 0.0 calibrated_confidence: float = 0.0 bet_grade: str = "PASS" playable: bool = False stake_units: float = 0.0 play_score: float = 0.0 reasons: list[str] = Field(default_factory=list) class ScoreScenario(BaseModel): score: str prob: float class ScorePrediction(BaseModel): ft: str = "0-0" ht: str = "0-0" xg_home: float = 0.0 xg_away: float = 0.0 xg_total: float = 0.0 class EngineBreakdown(BaseModel): team: float = 0.0 player: float = 0.0 odds: float = 0.0 referee: float = 0.0 class MarketProbs(BaseModel): pick: str = "" confidence: float = 0.0 probs: dict[str, float] = Field(default_factory=dict) # ── Root Response ─────────────────────────────────────────────────────────── class PredictionResponse(BaseModel): """ Root API contract. Every field matches the NestJS `SingleMatchPredictionPackage` DTO exactly. """ model_version: str = "v2.betting_engine" match_info: MatchInfo data_quality: DataQuality = Field(default_factory=DataQuality) risk: RiskAssessment = Field(default_factory=RiskAssessment) engine_breakdown: EngineBreakdown = Field(default_factory=EngineBreakdown) main_pick: PickDetail | None = None value_pick: PickDetail | None = None bet_advice: BetAdvice = Field(default_factory=BetAdvice) bet_summary: list[BetSummaryRow] = Field(default_factory=list) supporting_picks: list[PickDetail] = Field(default_factory=list) aggressive_pick: PickDetail | None = None scenario_top5: list[ScoreScenario] = Field(default_factory=list) score_prediction: ScorePrediction = Field(default_factory=ScorePrediction) market_board: dict[str, Any] = Field(default_factory=dict) reasoning_factors: list[str] = Field(default_factory=list)