main
Deploy Iddaai Backend / build-and-deploy (push) Successful in 37s

This commit is contained in:
2026-05-17 02:17:22 +03:00
parent 17ace9bd12
commit 94c7a4481a
53 changed files with 29602 additions and 7832 deletions
+444
View File
@@ -0,0 +1,444 @@
"""Coupon Mixin — multi-match coupon builder + daily bankers.
Auto-extracted mixin module — split from services/single_match_orchestrator.py.
All methods here are composed into SingleMatchOrchestrator via inheritance.
`self` attributes (self.dsn, self.enrichment, self.v25_predictor, etc.) are
initialised in the main __init__.
"""
from __future__ import annotations
import json
import re
import time
import math
import os
import pickle
from collections import defaultdict
from typing import Any, Dict, List, Optional, Set, Tuple, overload
import pandas as pd
import numpy as np
import psycopg2
from psycopg2.extras import RealDictCursor
from data.db import get_clean_dsn
from schemas.prediction import FullMatchPrediction
from schemas.match_data import MatchData
from models.v25_ensemble import V25Predictor, get_v25_predictor
try:
from models.v27_predictor import V27Predictor, compute_divergence, compute_value_edge
except ImportError:
class V27Predictor: # type: ignore[no-redef]
def __init__(self): self.models = {}
def load_models(self): return False
def predict_all(self, features): return {}
def compute_divergence(*args, **kwargs):
return {}
def compute_value_edge(*args, **kwargs):
return {}
from features.odds_band_analyzer import OddsBandAnalyzer
try:
from models.basketball_v25 import (
BasketballMatchPrediction,
get_basketball_v25_predictor,
)
except ImportError:
BasketballMatchPrediction = Any # type: ignore[misc]
def get_basketball_v25_predictor() -> Any:
raise ImportError("Basketball predictor is not available")
from core.engines.player_predictor import PlayerPrediction, get_player_predictor
from services.feature_enrichment import FeatureEnrichmentService
from services.betting_brain import BettingBrain
from services.v26_shadow_engine import V26ShadowEngine, get_v26_shadow_engine
from services.match_commentary import generate_match_commentary
from utils.top_leagues import load_top_league_ids
from utils.league_reliability import load_league_reliability
from config.config_loader import build_threshold_dict, get_threshold_default
from models.calibration import get_calibrator
class CouponMixin:
def build_coupon(
self,
match_ids: List[str],
strategy: str = "BALANCED",
max_matches: Optional[int] = None,
min_confidence: Optional[float] = None,
) -> Dict[str, Any]:
strategy_name = (strategy or "BALANCED").upper()
strategy_config = {
"SAFE": {"max_matches": 4, "min_conf": 66.0},
"BALANCED": {"max_matches": 5, "min_conf": 58.0},
"AGGRESSIVE": {"max_matches": 8, "min_conf": 52.0},
"VALUE": {"max_matches": 8, "min_conf": 48.0},
"MIRACLE": {"max_matches": 10, "min_conf": 44.0},
}
cfg = strategy_config.get(strategy_name, strategy_config["BALANCED"])
max_allowed = max_matches if max_matches is not None else cfg["max_matches"]
min_conf = min_confidence if min_confidence is not None else cfg["min_conf"]
candidates: List[Dict[str, Any]] = []
rejected: List[Dict[str, Any]] = []
for match_id in match_ids:
package = self.analyze_match(match_id)
if not package:
rejected.append({"match_id": match_id, "reason": "match_not_found"})
continue
risk_level = str(package.get("risk", {}).get("level", "MEDIUM")).upper()
data_quality = str(package.get("data_quality", {}).get("label", "MEDIUM")).upper()
match_candidates: List[Dict[str, Any]] = []
seen_keys: Set[Tuple[str, str]] = set()
bet_summary = package.get("bet_summary") or []
raw_picks = []
for candidate in [
package.get("main_pick"),
package.get("value_pick"),
*(package.get("supporting_picks") or []),
]:
if isinstance(candidate, dict):
raw_picks.append(candidate)
for candidate in bet_summary:
if isinstance(candidate, dict):
raw_picks.append(candidate)
for candidate in raw_picks:
market = str(candidate.get("market") or "")
pick = str(candidate.get("pick") or "")
if not market or not pick:
continue
dedupe_key = (market, pick)
if dedupe_key in seen_keys:
continue
seen_keys.add(dedupe_key)
calibrated_conf = float(
candidate.get("calibrated_confidence", candidate.get("confidence", 0.0))
or 0.0
)
odds = float(candidate.get("odds", 0.0) or 0.0)
probability = float(candidate.get("probability", 0.0) or 0.0)
play_score = float(candidate.get("play_score", 0.0) or 0.0)
ev_edge = float(
candidate.get("ev_edge", candidate.get("edge", 0.0)) or 0.0
)
playable = bool(candidate.get("playable"))
bet_grade = str(candidate.get("bet_grade", "PASS")).upper()
if odds <= 1.01:
continue
strict_candidate = (
playable
and calibrated_conf >= min_conf
and bet_grade != "PASS"
)
if strategy_name == "SAFE":
strict_pass = strict_candidate
if odds > 2.35 or play_score < 60.0 or risk_level in {"HIGH", "EXTREME"}:
strict_pass = False
if data_quality == "LOW" or ev_edge < 0.01 or bet_grade == "PASS":
strict_pass = False
strict_score = (
calibrated_conf * 1.10
+ play_score * 0.90
+ (ev_edge * 180.0)
- abs(odds - 1.55) * 12.0
)
soft_pass = (
calibrated_conf >= max(min_conf - 10.0, 56.0)
and odds <= 2.70
and play_score >= 50.0
and risk_level != "EXTREME"
and data_quality != "LOW"
and ev_edge >= -0.01
)
soft_score = (
calibrated_conf
+ play_score * 0.85
+ (ev_edge * 140.0)
- abs(odds - 1.65) * 9.0
)
elif strategy_name == "BALANCED":
strict_pass = strict_candidate
if odds > 3.40 or play_score < 52.0 or risk_level == "EXTREME":
strict_pass = False
if ev_edge < 0.0 or bet_grade == "PASS":
strict_pass = False
strict_score = (
calibrated_conf
+ play_score
+ (ev_edge * 220.0)
+ min(odds, 3.0) * 3.0
)
soft_pass = (
calibrated_conf >= max(min_conf - 10.0, 48.0)
and odds <= 4.20
and play_score >= 44.0
and risk_level != "EXTREME"
and ev_edge >= -0.015
)
soft_score = (
calibrated_conf * 0.95
+ play_score * 0.90
+ (ev_edge * 180.0)
+ min(odds, 3.5) * 3.5
)
elif strategy_name == "AGGRESSIVE":
strict_pass = strict_candidate
if odds < 1.35 or odds > 7.50 or play_score < 46.0:
strict_pass = False
if risk_level == "EXTREME" or bet_grade == "PASS":
strict_pass = False
strict_score = (
calibrated_conf * 0.85
+ play_score * 0.75
+ (ev_edge * 260.0)
+ min(odds, 6.0) * 7.0
)
soft_pass = (
calibrated_conf >= max(min_conf - 10.0, 42.0)
and 1.25 <= odds <= 8.50
and play_score >= 40.0
and risk_level != "EXTREME"
and ev_edge >= -0.02
)
soft_score = (
calibrated_conf * 0.80
+ play_score * 0.70
+ (ev_edge * 210.0)
+ min(odds, 7.0) * 7.5
)
elif strategy_name == "VALUE":
strict_pass = strict_candidate
if odds < 1.55 or play_score < 48.0 or ev_edge < 0.03:
strict_pass = False
if risk_level == "EXTREME" or data_quality == "LOW" or bet_grade == "PASS":
strict_pass = False
strict_score = (
calibrated_conf * 0.75
+ play_score * 0.85
+ (ev_edge * 320.0)
+ min(odds, 6.5) * 8.0
)
soft_pass = (
calibrated_conf >= max(min_conf - 10.0, 40.0)
and odds >= 1.35
and play_score >= 40.0
and risk_level != "EXTREME"
and data_quality != "LOW"
and ev_edge >= 0.0
)
soft_score = (
calibrated_conf * 0.70
+ play_score * 0.80
+ (ev_edge * 260.0)
+ min(odds, 7.0) * 7.0
)
else: # MIRACLE
strict_pass = strict_candidate
if odds < 2.10 or play_score < 40.0 or ev_edge < 0.01:
strict_pass = False
if risk_level == "EXTREME" or bet_grade == "PASS":
strict_pass = False
strict_score = (
calibrated_conf * 0.55
+ play_score * 0.60
+ (ev_edge * 260.0)
+ min(odds, 10.0) * 10.0
)
soft_pass = (
calibrated_conf >= max(min_conf - 10.0, 36.0)
and odds >= 1.60
and play_score >= 34.0
and risk_level != "EXTREME"
and ev_edge >= -0.02
)
soft_score = (
calibrated_conf * 0.50
+ play_score * 0.55
+ (ev_edge * 200.0)
+ min(odds, 10.0) * 9.0
)
fallback_pass = (
calibrated_conf >= max(min_conf - 14.0, 34.0)
and odds >= 1.20
and play_score >= 32.0
and risk_level != "EXTREME"
)
fallback_score = (
calibrated_conf * 0.60
+ play_score * 0.65
+ (ev_edge * 120.0)
+ min(odds, 6.0) * 4.0
)
strategy_score = strict_score
selection_mode = "strict"
if strict_pass:
pass
elif soft_pass:
strategy_score = soft_score
selection_mode = "soft"
elif fallback_pass:
strategy_score = fallback_score
selection_mode = "fallback"
else:
continue
match_candidates.append(
{
"match_id": package["match_info"]["match_id"],
"match_name": package["match_info"]["match_name"],
"market": market,
"pick": pick,
"probability": probability,
"confidence": calibrated_conf,
"odds": odds,
"risk_level": risk_level,
"data_quality": data_quality,
"bet_grade": bet_grade,
"playable": playable,
"play_score": round(play_score, 1),
"ev_edge": round(ev_edge, 4),
"selection_mode": selection_mode,
"strategy_score": round(strategy_score, 3),
}
)
if not match_candidates:
rejected.append(
{
"match_id": match_id,
"reason": "no_strategy_fit",
"threshold": min_conf,
}
)
continue
match_candidates.sort(
key=lambda item: (
float(item.get("strategy_score", 0.0)),
float(item.get("confidence", 0.0)),
float(item.get("ev_edge", 0.0)),
),
reverse=True,
)
candidates.append(match_candidates[0])
candidates.sort(
key=lambda item: (
float(item.get("strategy_score", 0.0)),
float(item.get("confidence", 0.0)),
float(item.get("ev_edge", 0.0)),
),
reverse=True,
)
selected = candidates[: max(1, max_allowed)]
total_odds = 1.0
win_probability = 1.0
for pick in selected:
odd = float(pick.get("odds") or 1.0)
prob = float(pick.get("probability") or 0.0)
total_odds *= odd if odd > 1.0 else 1.0
win_probability *= prob
return {
"strategy": strategy_name,
"generated_at": __import__("datetime").datetime.utcnow().isoformat() + "Z",
"match_count": len(selected),
"bets": selected,
"total_odds": round(total_odds, 2),
"expected_win_rate": round(win_probability, 4),
"rejected_matches": rejected,
}
def get_daily_bankers_live(self, count: int = 3) -> List[Dict[str, Any]]:
with psycopg2.connect(self.dsn) as conn:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute(
"""
SELECT id
FROM live_matches
WHERE mst_utc > EXTRACT(EPOCH FROM NOW()) * 1000
AND mst_utc < EXTRACT(EPOCH FROM NOW() + INTERVAL '24 hours') * 1000
ORDER BY mst_utc ASC
LIMIT 60
""",
)
ids = [row["id"] for row in cur.fetchall()]
if not ids:
return []
coupon = self.build_coupon(
match_ids=ids,
strategy="SAFE",
max_matches=max(1, count),
min_confidence=78.0,
)
return coupon.get("bets", [])[: max(1, count)]
def get_daily_bankers(self, count: int = 3) -> List[Dict[str, Any]]:
"""
Identifies the safest, highest value bets for the next 24 hours.
"""
now_ms = int(time.time() * 1000)
horizon_ms = now_ms + (24 * 60 * 60 * 1000)
with psycopg2.connect(self.dsn) as conn:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute("""
SELECT m.id, m.match_name, m.mst_utc
FROM matches m
WHERE m.mst_utc >= %s AND m.mst_utc <= %s
AND m.status = 'NS'
AND EXISTS (SELECT 1 FROM odd_categories oc WHERE oc.match_id = m.id)
ORDER BY m.mst_utc ASC
LIMIT 50
""", (now_ms, horizon_ms))
matches = cur.fetchall()
potential_bankers = []
print(f"🔍 Scanning {len(matches)} upcoming matches for Bankers...")
for match in matches:
try:
data = self._load_match_data(match['id'])
if data is None: continue
result = self.analyze_match(match['id'])
if result and 'main_pick' in result:
pick = result['main_pick']
conf = pick.get('calibrated_confidence', pick.get('confidence', 0))
odds = pick.get('odds', 0)
market = pick.get('market', '')
pick_name = pick.get('pick', '')
# Banker Criteria: High Confidence (>75%) AND Decent Odds (>1.30)
if conf >= 75.0 and odds >= 1.30:
score = conf * (odds - 1.0)
potential_bankers.append({
"match_id": match['id'],
"match_name": match['match_name'] or f"{data.home_team_name} vs {data.away_team_name}",
"league": data.league_name,
"pick": f"{market} - {pick_name}",
"confidence": conf,
"odds": odds,
"value_score": score
})
except Exception:
pass
potential_bankers.sort(key=lambda x: x['value_score'], reverse=True)
return potential_bankers[:count]