This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user