gg
This commit is contained in:
@@ -90,8 +90,10 @@ class _RouterCursor:
|
||||
def _build_orchestrator() -> SingleMatchOrchestrator:
|
||||
orchestrator = SingleMatchOrchestrator.__new__(SingleMatchOrchestrator)
|
||||
orchestrator.v25_predictor = MagicMock()
|
||||
orchestrator.v26_shadow_engine = None
|
||||
orchestrator.basketball_predictor = MagicMock()
|
||||
orchestrator.dsn = "postgresql://unit-test"
|
||||
orchestrator.engine_mode = "v25"
|
||||
orchestrator.league_reliability = {}
|
||||
orchestrator.market_calibration = {
|
||||
"MS": 0.82,
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user