diff --git a/ai-engine/services/betting_brain.py b/ai-engine/services/betting_brain.py index df187ff..0981a1e 100644 --- a/ai-engine/services/betting_brain.py +++ b/ai-engine/services/betting_brain.py @@ -263,7 +263,14 @@ class BettingBrain: "OE": -12.0, } - def judge(self, package: Dict[str, Any]) -> Dict[str, Any]: + def judge( + self, + package: Dict[str, Any], + ms_real_odds: Optional[Dict[str, float]] = None, + ) -> Dict[str, Any]: + # V35c: real bookmaker MS odds (from odds_data) for reference rows — + # the brain must never display synthetic 1/p "fair odds" as offered. + self._ms_real_odds = ms_real_odds if isinstance(ms_real_odds, dict) else {} v27_engine = package.get("v27_engine") if not isinstance(v27_engine, dict): return package @@ -916,9 +923,13 @@ class BettingBrain: prob = self._safe_float(probs.get(pick), 0.0) if prob is None or prob <= 0.0: continue - implied_odd = round(1.0 / prob, 2) if prob > 0.01 else 0.0 - ref_odd = existing_odds_by_pick.get(pick) or implied_odd - rows[key] = { + # V35c: only REAL bookmaker odds may be displayed. The old fallback + # showed synthetic fair-odds (1/prob) as "Oran" — users could read + # it as an offered price (e.g. X shown at 4.53 while the bulletin + # offered ~3.58). No real price → odds 0.0 and the FE renders "-". + real = self._safe_float(getattr(self, "_ms_real_odds", {}).get(pick), 0.0) or 0.0 + ref_odd = existing_odds_by_pick.get(pick) or (real if real > 1.01 else 0.0) + row = { "market": "MS", "pick": pick, "probability": round(prob, 4), @@ -932,6 +943,12 @@ class BettingBrain: "bet_grade": "PASS", "decision_reasons": ["underdog_reference_for_completeness"], } + if ref_odd > 1.01: + # honest economics vs the real price (vig shows as it truly is) + row["implied_prob"] = round(1.0 / ref_odd, 4) + row["ev_edge"] = round(prob * ref_odd - 1.0, 4) + row["edge"] = row["ev_edge"] + rows[key] = row @staticmethod def _merge_row(existing: Optional[Dict[str, Any]], incoming: Dict[str, Any]) -> Dict[str, Any]: diff --git a/ai-engine/services/orchestrator/upper_brain.py b/ai-engine/services/orchestrator/upper_brain.py index 84ae345..3473fed 100644 --- a/ai-engine/services/orchestrator/upper_brain.py +++ b/ai-engine/services/orchestrator/upper_brain.py @@ -60,8 +60,23 @@ from models.calibration import get_calibrator class UpperBrainMixin: - def _apply_upper_brain_guards(self, package: Dict[str, Any]) -> Dict[str, Any]: - return BettingBrain().judge(package) + def _apply_upper_brain_guards( + self, package: Dict[str, Any], data: Any = None + ) -> Dict[str, Any]: + # V35c: hand the brain the REAL bookmaker MS odds so its reference rows + # can never display synthetic 1/p prices as if they were offered. + ms_real_odds = None + if data is not None: + try: + odds = getattr(data, "odds_data", None) or {} + ms_real_odds = { + "1": self._real_market_odds(odds, "ms_h"), + "X": self._real_market_odds(odds, "ms_d"), + "2": self._real_market_odds(odds, "ms_a"), + } + except Exception: + ms_real_odds = None + return BettingBrain().judge(package, ms_real_odds=ms_real_odds) v27_engine = package.get("v27_engine") if not isinstance(v27_engine, dict) or not v27_engine.get("triple_value"): diff --git a/ai-engine/services/single_match_orchestrator.py b/ai-engine/services/single_match_orchestrator.py index f2c0b44..79d3a0c 100755 --- a/ai-engine/services/single_match_orchestrator.py +++ b/ai-engine/services/single_match_orchestrator.py @@ -668,7 +668,28 @@ class SingleMatchOrchestrator( base_package.setdefault("analysis_details", {}) base_package["analysis_details"]["v27_loaded"] = False - base_package = self._apply_upper_brain_guards(base_package) + base_package = self._apply_upper_brain_guards(base_package, data) + + # V35c: the brain rebuilt main/value/supporting/bet_summary AFTER the + # market anchor ran inside _build_prediction_package — re-stamp the + # calibrated display fields (Güven/CI/Model%/edge) so they stay + # consistent, BEFORE the commentary reads the package. + self._apply_anchor_to_picks( + base_package.get("market_board") or {}, + base_package.get("main_pick"), + base_package.get("value_pick"), + base_package.get("aggressive_pick"), + base_package.get("supporting_picks"), + base_package.get("bet_summary"), + ) + _mp = base_package.get("main_pick") + _advice = base_package.get("bet_advice") + if isinstance(_mp, dict) and isinstance(_advice, dict) and _mp.get("confidence_band"): + _advice["confidence_band"] = _mp["confidence_band"] + # no fabricated value bets: a value pick must carry measured positive edge + _vp = base_package.get("value_pick") + if isinstance(_vp, dict) and float(_vp.get("ev_edge", 0.0) or 0.0) <= 0.0: + base_package["value_pick"] = None # ── Match Commentary: human-readable summary ────────────── try: