import sys import unittest from pathlib import Path from types import SimpleNamespace AI_ENGINE_ROOT = Path(__file__).resolve().parents[1] if str(AI_ENGINE_ROOT) not in sys.path: sys.path.insert(0, str(AI_ENGINE_ROOT)) from services.v26_shadow_engine import V26ShadowEngine def _build_prediction(): return SimpleNamespace( risk_level="MEDIUM", risk_score=42.0, is_surprise_risk=False, surprise_type="", surprise_score=0.0, surprise_comment="", surprise_reasons=[], risk_warnings=[], team_confidence=71.0, player_confidence=64.0, odds_confidence=75.0, referee_confidence=58.0, predicted_ft_score="2-1", predicted_ht_score="1-0", home_xg=1.72, away_xg=1.08, total_xg=2.8, ft_scores_top5=[ {"score": "2-1", "prob": 0.093}, {"score": "1-1", "prob": 0.086}, ], ms_home_prob=0.52, ms_draw_prob=0.24, ms_away_prob=0.24, ) def _build_data(referee_name="Ref A", lineup_source="confirmed_live", league_id="league1"): return SimpleNamespace( match_id="m1", home_team_name="Home", away_team_name="Away", league_id=league_id, league_name="League", match_date_ms=1710000000000, sport="football", home_lineup=["h"] * 11, away_lineup=["a"] * 11, lineup_source=lineup_source, referee_name=referee_name, odds_data={ "ms_h": 2.1, "ms_d": 3.4, "ms_a": 3.7, "dc_1x": 1.28, "dc_x2": 1.68, "dc_12": 1.34, "ou15_o": 1.24, "ou15_u": 4.1, "ou25_o": 1.77, "ou25_u": 2.05, "ou35_o": 2.95, "ou35_u": 1.4, "btts_y": 1.74, "btts_n": 2.04, "ht_h": 2.72, "ht_d": 2.05, "ht_a": 4.8, "ht_ou05_o": 1.38, "ht_ou05_u": 2.85, "ht_ou15_o": 2.48, "ht_ou15_u": 1.48, "oe_odd": 1.92, "oe_even": 1.9, "cards_o": 1.98, "cards_u": 1.84, "hcap_h": 3.3, "hcap_d": 3.7, "hcap_a": 1.93, "htft_11": 3.8, "htft_1x": 5.1, "htft_12": 16.5, "htft_x1": 5.6, "htft_xx": 4.8, "htft_x2": 7.4, "htft_21": 22.0, "htft_2x": 12.0, "htft_22": 6.2, }, ) class V26ShadowEngineTests(unittest.TestCase): def setUp(self): self.engine = V26ShadowEngine() self.engine.top_league_ids = {"top1"} self.prediction = _build_prediction() self.quality = { "label": "HIGH", "score": 0.88, "home_lineup_count": 11, "away_lineup_count": 11, "lineup_source": "confirmed_live", "flags": [], } self.v25_signal = { "MS": {"probs": {"1": 0.46, "X": 0.27, "2": 0.27}}, "HT": {"probs": {"1": 0.39, "X": 0.41, "2": 0.20}}, "HTFT": {"probs": {"1/1": 0.22, "X/X": 0.18, "2/2": 0.14}}, "HCAP": {"probs": {"1": 0.21, "X": 0.19, "2": 0.60}}, "CARDS": {"probs": {"Under": 0.53, "Over": 0.47}}, } def test_build_package_exposes_shadow_metadata(self): package = self.engine.build_package( data=_build_data(), prediction=self.prediction, v25_signal=self.v25_signal, quality=self.quality, ) self.assertEqual(package["model_version"], "v26.shadow.2") self.assertIn("calibration_version", package) self.assertIn("decision_trace_id", package) self.assertIn("market_reliability", package) self.assertTrue(package["bet_summary"]) def test_cards_defaults_to_pass_when_referee_missing(self): package = self.engine.build_package( data=_build_data(referee_name=None), prediction=self.prediction, v25_signal=self.v25_signal, quality=self.quality, ) cards = next(item for item in package["bet_summary"] if item["market"] == "CARDS") self.assertFalse(cards["playable"]) self.assertEqual(cards["bet_grade"], "PASS") def test_select_main_pick_prioritizes_ms_when_playable(self): rows = [ { "market": "OU25", "pick": "2.5 Üst", "playable": True, "selection_score": 86.0, "play_score": 83.0, "edge": 0.15, "calibrated_confidence": 72.0, }, { "market": "MS", "pick": "1", "playable": True, "selection_score": 81.0, "play_score": 82.0, "edge": 0.08, "calibrated_confidence": 64.0, }, ] main_pick = self.engine._select_main_pick(rows) self.assertIsNotNone(main_pick) self.assertEqual(main_pick["market"], "MS") self.assertEqual(main_pick["pick_reason"], "ms_priority_market") def test_build_package_exposes_surprise_pick_when_reversal_is_hot(self): prediction = _build_prediction() prediction.is_surprise_risk = True prediction.surprise_score = 82.0 prediction.surprise_type = "favorite_reversal" v25_signal = dict(self.v25_signal) v25_signal["HTFT"] = { "probs": { "1/2": 0.24, "X/2": 0.14, "1/1": 0.12, "X/X": 0.10, } } package = self.engine.build_package( data=_build_data(), prediction=prediction, v25_signal=v25_signal, quality=self.quality, ) self.assertIn("surprise_hunter", package) self.assertIn("surprise_pick", package) self.assertTrue(package["surprise_hunter"]["playable"]) self.assertEqual(package["surprise_pick"]["market"], "HTFT") self.assertEqual(package["surprise_pick"]["strategy_channel"], "surprise_sidecar") self.assertEqual(package["surprise_hunter"]["strategy_channel"], "surprise_sidecar") self.assertGreaterEqual(package["surprise_pick"]["surprise_score"], 66.0) self.assertEqual(package["main_pick"]["strategy_channel"], "standard") self.assertNotEqual(package["main_pick"].get("strategy_channel"), package["surprise_pick"].get("strategy_channel")) self.assertNotEqual(package["main_pick"].get("pick_reason"), "favorite_reversal_signal") def test_top_league_policy_suppresses_early_and_extra_goal_markets(self): package = self.engine.build_package( data=_build_data(league_id="top1"), prediction=self.prediction, v25_signal=self.v25_signal, quality=self.quality, ) summary = {item["market"]: item for item in package["bet_summary"]} self.assertFalse(summary["HT_OU05"]["playable"]) self.assertTrue( "top_league_early_market_suppressed" in summary["HT_OU05"]["reasons"] or "top_league_ht_ou05_over_disabled" in summary["HT_OU05"]["reasons"] ) playable_goal_cluster = [ item for item in package["bet_summary"] if item["market"] in {"OU15", "OU25", "OU35", "BTTS"} and item["playable"] ] self.assertLessEqual(len(playable_goal_cluster), 1) def test_scoreline_consistency_blocks_conflicting_markets(self): rows = [ { "market": "MS", "raw_pick": "1", "pick": "1", "playable": True, "bet_grade": "A", "stake_units": 1.0, "decision_reasons": [], }, { "market": "BTTS", "raw_pick": "Yes", "pick": "KG Var", "playable": True, "bet_grade": "A", "stake_units": 1.0, "decision_reasons": [], }, { "market": "OU25", "raw_pick": "Over", "pick": "2.5 Üst", "playable": True, "bet_grade": "A", "stake_units": 1.0, "decision_reasons": [], }, { "market": "OU25", "raw_pick": "Under", "pick": "2.5 Alt", "playable": True, "bet_grade": "A", "stake_units": 1.0, "decision_reasons": [], }, ] prediction = _build_prediction() prediction.predicted_ft_score = "1-0" prediction.predicted_ht_score = "1-0" controlled = self.engine._apply_scoreline_consistency_controls(rows, prediction) by_market_pick = {(row["market"], row["raw_pick"]): row for row in controlled} self.assertTrue(by_market_pick[("MS", "1")]["playable"]) self.assertIn( "scoreline_scenario_aligned", by_market_pick[("MS", "1")]["decision_reasons"], ) self.assertFalse(by_market_pick[("BTTS", "Yes")]["playable"]) self.assertFalse(by_market_pick[("OU25", "Over")]["playable"]) self.assertTrue(by_market_pick[("OU25", "Under")]["playable"]) self.assertIn( "scoreline_scenario_conflict", by_market_pick[("BTTS", "Yes")]["decision_reasons"], ) if __name__ == "__main__": unittest.main()