Files
iddaai-be/ai-engine/tests/test_v26_shadow_engine.py
T
2026-04-21 16:53:56 +03:00

287 lines
9.5 KiB
Python

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()