@@ -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]
|
||||
Reference in New Issue
Block a user