main
Deploy Iddaai Backend / build-and-deploy (push) Failing after 2m6s

This commit is contained in:
2026-05-12 02:43:02 +03:00
parent f8599bdb9a
commit b6d64b59bf
35 changed files with 1400 additions and 630 deletions
+110 -3
View File
@@ -19,6 +19,10 @@ class BettingBrain:
SOFT_DIVERGENCE = 0.14
EXTREME_MODEL_PROB = 0.85
EXTREME_GAP = 0.30
# Vetoes that is_value_sniper bypasses (does NOT bypass odds_below_minimum)
SNIPER_BYPASSABLE_VETOES = {"calibrated_confidence_too_low", "play_score_too_low"}
# Trap market: market implied probability massively exceeds historical band hit rate
TRAP_MARKET_GAP = 0.10
MARKET_PRIORS = {
"DC": 4.0,
@@ -59,8 +63,13 @@ class BettingBrain:
row for row in judged_rows.values()
if row.get("betting_brain", {}).get("action") == "WATCH"
]
no_value = [
row for row in judged_rows.values()
if row.get("betting_brain", {}).get("action") == "WATCH_NO_VALUE"
]
approved.sort(key=self._candidate_sort_key, reverse=True)
watchlist.sort(key=self._candidate_sort_key, reverse=True)
no_value.sort(key=self._candidate_sort_key, reverse=True)
original_main = guarded.get("main_pick") or {}
main_pick = None
@@ -78,6 +87,13 @@ class BettingBrain:
self._force_no_bet(main_pick, "betting_brain_watchlist")
decision = "WATCHLIST"
decision_reason = main_pick.get("betting_brain", {}).get("summary", "Interesting but not clean enough.")
elif no_value:
# B-1: model agrees with a low-odds market — surface it so the user
# sees the read, but explicitly mark as not-playable.
main_pick = dict(no_value[0])
self._force_no_bet(main_pick, "betting_brain_no_value_odds_below_minimum")
decision = "WATCH_NO_VALUE"
decision_reason = "Model favoriyle hemfikir ama oran bahis için çok düşük — bilgi amaçlı gösteriliyor."
elif original_main:
main_pick = dict(judged_rows.get(self._row_key(original_main), original_main))
self._force_no_bet(main_pick, "betting_brain_no_safe_pick")
@@ -103,7 +119,7 @@ class BettingBrain:
playable = decision == "BET" and bool(main_pick and main_pick.get("playable"))
advice = dict(guarded.get("bet_advice") or {})
advice["playable"] = playable
advice["suggested_stake_units"] = float(main_pick.get("stake_units", 0.0)) if playable else 0.0
advice["suggested_stake_units"] = float(main_pick.get("stake_units", 0.0)) if playable and main_pick else 0.0
advice["reason"] = "betting_brain_approved" if playable else "betting_brain_no_bet"
advice["decision"] = decision
advice["confidence_band"] = self._decision_band(main_pick)
@@ -199,6 +215,23 @@ class BettingBrain:
score += 11.0
positives.append("v25_v27_aligned")
# Trap market detection: market overpriced vs historical band hit rate
trap_market_flag = False
trap_market_gap = None
if isinstance(triple, dict):
band_rate_val = self._safe_float(triple.get("band_rate"))
implied_val = self._safe_float(triple.get("implied_prob"))
if (
band_rate_val is not None
and implied_val is not None
and band_sample >= self.MIN_BAND_SAMPLE
and (implied_val - band_rate_val) > self.TRAP_MARKET_GAP
):
trap_market_flag = True
trap_market_gap = round(implied_val - band_rate_val, 4)
score -= 14.0
issues.append("trap_market_market_overpriced")
if isinstance(triple, dict):
if triple_is_value:
score += 18.0
@@ -240,10 +273,28 @@ class BettingBrain:
if market in {"HT", "HTFT", "OE"} and score < 86.0 and not is_value_sniper:
vetoes.append("volatile_market_requires_exceptional_evidence")
# Sniper override: bypass eligible vetoes when value sniper triggered
sniper_bypassed: List[str] = []
if is_value_sniper and vetoes:
remaining = []
for v in vetoes:
if v in self.SNIPER_BYPASSABLE_VETOES:
sniper_bypassed.append(v)
else:
remaining.append(v)
vetoes = remaining
if sniper_bypassed:
positives.append("sniper_bypassed_soft_vetoes")
score = max(0.0, min(100.0, score))
action = "BET"
if vetoes:
action = "REJECT"
# B-1: when only veto is odds_below_minimum, switch to WATCH_NO_VALUE
# so user still sees model commentary instead of blank rejection.
if vetoes == ["odds_below_minimum"]:
action = "WATCH_NO_VALUE"
else:
action = "REJECT"
elif score < self.MIN_WATCH_SCORE and not is_value_sniper:
action = "REJECT"
elif score < self.MIN_BET_SCORE and not is_value_sniper:
@@ -256,6 +307,9 @@ class BettingBrain:
"positives": positives[:5],
"issues": issues[:6],
"vetoes": vetoes[:6],
"sniper_bypassed": sniper_bypassed,
"trap_market_flag": trap_market_flag,
"trap_market_gap": trap_market_gap,
"model_prob": round(model_prob, 4) if model_prob is not None else None,
"implied_prob": round(implied, 4),
"model_market_gap": round(model_gap, 4) if model_gap is not None else None,
@@ -290,9 +344,59 @@ class BettingBrain:
if isinstance(item, dict) and item.get("market"):
key = self._row_key(item)
rows[key] = self._merge_row(rows.get(key), item)
# B-2: ensure both MS sides (and DC sides) have an entry — give user the
# model's read on the opposite outcome even when upstream filtered it out.
self._inject_reference_rows(rows, package)
return list(rows.values())
def _inject_reference_rows(
self,
rows: Dict[str, Dict[str, Any]],
package: Dict[str, Any],
) -> None:
market_board = package.get("market_board") or {}
ms_board = market_board.get("MS") if isinstance(market_board, dict) else None
if not isinstance(ms_board, dict):
return
probs = ms_board.get("probs") if isinstance(ms_board.get("probs"), dict) else {}
if not probs:
return
# Pull MS odds from any existing MS row to estimate the missing side's odds
existing_odds_by_pick: Dict[str, float] = {}
for row in rows.values():
if str(row.get("market")) == "MS":
pick = str(row.get("pick"))
odd = self._safe_float(row.get("odds"), 0.0) or 0.0
if pick and odd > 1.0:
existing_odds_by_pick[pick] = odd
for pick in ("1", "X", "2"):
key = f"MS:{pick}"
if key in rows:
continue
prob = self._safe_float(probs.get(pick), 0.0)
if prob is None or prob <= 0.0:
continue
implied_odd = round(1.0 / prob, 2) if prob > 0.01 else 0.0
ref_odd = existing_odds_by_pick.get(pick) or implied_odd
rows[key] = {
"market": "MS",
"pick": pick,
"probability": round(prob, 4),
"confidence": round(prob * 100.0, 1),
"raw_confidence": round(prob * 100.0, 1),
"calibrated_confidence": round(prob * 100.0, 1),
"odds": ref_odd,
"is_underdog_reference": True,
"playable": False,
"stake_units": 0.0,
"bet_grade": "PASS",
"decision_reasons": ["underdog_reference_for_completeness"],
}
@staticmethod
def _merge_row(existing: Optional[Dict[str, Any]], incoming: Dict[str, Any]) -> Dict[str, Any]:
if existing is None:
@@ -331,6 +435,7 @@ class BettingBrain:
"odds_reliability": row.get("odds_reliability", 0.35),
"odds": row.get("odds", 0.0),
"reasons": reasons[:6],
"is_underdog_reference": bool(row.get("is_underdog_reference")),
"betting_brain": row.get("betting_brain"),
}
@@ -409,6 +514,8 @@ class BettingBrain:
return f"{market} {pick} approved: evidence is aligned enough for a controlled stake."
if action == "WATCH":
return f"{market} {pick} is interesting but not clean enough for stake."
if action == "WATCH_NO_VALUE":
return f"{market} {pick}: model favoriyle hemfikir, fakat oran ({', '.join(vetoes[:1]) or 'düşük'}) bahis için yetersiz."
if vetoes:
return f"{market} {pick} rejected: {', '.join(vetoes[:3])}."
if issues: