gg
Deploy Iddaai Backend / build-and-deploy (push) Successful in 1m6s

This commit is contained in:
2026-05-29 11:59:51 +03:00
parent 659110c806
commit b5cb412236
4 changed files with 736 additions and 64 deletions
+337 -59
View File
@@ -12,14 +12,33 @@ from typing import Any, Dict, List, Optional, Tuple
class BettingBrain:
MIN_ODDS = 1.30
MIN_BET_SCORE = 72.0
MIN_WATCH_SCORE = 62.0
MIN_BET_SCORE = 62.0
MIN_WATCH_SCORE = 52.0
MIN_BAND_SAMPLE = 8
HARD_DIVERGENCE = 0.22
SOFT_DIVERGENCE = 0.14
EXTREME_MODEL_PROB = 0.85
EXTREME_GAP = 0.30
SNIPER_BYPASSABLE_VETOES = {"play_score_too_low"}
# V31d: value-tier underdogs are bet on the odds-premium edge, NOT on
# high win-probability. These two vetoes encode a favorite-picking rule
# (demand >45% confidence) that structurally excludes every profitable
# underdog, so we waive them when a row matches an MS value tier.
# Genuine safety vetoes (extreme_neg_ev, ev_too_high_trap, htft_reversal
# _risk_high, v25_v27_hard_disagreement, low_reliability_hard_block) are
# NOT in this set and still reject.
VALUE_TIER_BYPASSABLE_VETOES = {"calibrated_confidence_too_low", "play_score_too_low"}
VALUE_TIER_NAMES = {"premium", "strong", "standard"}
# V31d: value-regime score floors (replaces favorite-confidence scoring
# for value-tier matches). premium clears MIN_BET_SCORE(62) → BET;
# strong/standard are capped below it → WATCH (visible, not staked)
# because the 60-day data shows those bands break even.
VALUE_TIER_BASE_SCORE = {"premium": 70.0, "strong": 56.0, "standard": 54.0}
# V31d: flat, small stake for value-tier underdogs. Hit rate ~20% with
# long losing streaks (60d: up to 35 in a row) — the edge is in FREQUENCY,
# not per-bet size. Keep stake small to survive variance. Tunable: raise
# only if the bettor's bankroll/risk appetite allows deeper drawdowns.
VALUE_TIER_STAKE_UNITS = 0.5
TRAP_MARKET_GAP = 0.10
MARKET_MIN_CONFIDENCE = {
@@ -39,31 +58,181 @@ class BettingBrain:
SNIPER_BLOCKED_MARKETS = {"HT", "HTFT", "OE", "CARDS", "HT_OU05", "HT_OU15"}
# Markets that lose money under every filter combination per the
# diagnostic backtest (1000 matches). Until calibration is rebuilt for
# these specifically, force NO_BET. Re-evaluate after each backtest run.
MUTED_MARKETS = {"BTTS"}
# V30: NO markets muted — backtest tüm marketlerin gerçek ROI'sini görmeli.
# Tier sistemi zaten filtreleme yapıyor; mute etmek veri kaybına yol açar.
MUTED_MARKETS = set()
# Per-market optimal filter envelopes derived from the diagnostic
# backtest grid search (reports/filter_optimization_patch.json). Any
# pick falling OUTSIDE this envelope is vetoed. Tightens the playable
# band to the ROI-positive zone identified empirically.
# ═══════════════════════════════════════════════════════════════════
# V31d: KANITA DAYALI KADEMELİ DEĞER SİSTEMİ (Evidence-Based Tiers)
# ═══════════════════════════════════════════════════════════════════
# User directive: show 3 quality levels so the bettor picks by risk
# appetite ("hangi bahis hangi oranda tutuyor"). Each MS underdog tier
# carries a `value_tier` label that propagates to the UI. Only the
# PREMIUM band is auto-staked (BET); strong/standard surface as WATCH
# (full analysis shown, not staked) because the data says they break
# even — see new_gate_sim.py.
#
# Each entry: {min_conf, min_edge, max_edge, min_odds, max_odds,
# min_reliability, require_v27_agree}
MARKET_OPTIMAL_FILTERS = {
"MS": {
"min_edge": -0.05, "max_edge": 0.15,
"min_odds": 1.20, "max_odds": 10.0,
"min_reliability": 0.0, "require_v27_agree": True,
},
"OU25": {
"min_edge": -1.0, "max_edge": 0.15,
"min_odds": 1.80, "max_odds": 10.0,
"min_reliability": 0.0, "require_v27_agree": False,
},
# Validated on 60-day, 72,582-settled-row multi-pick backtest
# (ms_envelope.py + new_gate_sim.py, span 2026-04-17..05-28):
# PREMIUM (6.0-7.5, gap≥0, protective vetoes kept):
# 602 bets, +32.7% ROI, +39.4u, 20.6% hit, avgOdd 6.50
# ALL 6 weeks positive (+13.7%..+52.9%); OOS(>05-24) +47.4%;
# survives dropping top-5 wins (+24%). 14.3 bets/day.
# STRONG (5.0-6.0, gap≥0): ~breakeven (-1%) → WATCH, not staked.
# STANDARD (3.0-5.0, gap≥0): +0.5% breakeven → WATCH, volume zone.
#
# WHY 6.0-7.5 (not 6.0-50.0 as in V31c): the edge is concentrated.
# odds 6.0-7.0 +35% | 7.0-8.0 ~breakeven | 8.0+ NEGATIVE (longshot
# graveyard, -10..-26% ROI). The old wide premium tier let losing
# longshots in. Above 7.5 the model's edge evaporates.
#
# ROOT-CAUSE FIX (the volume crisis): underdogs were structurally
# un-bettable. Two vetoes (calibrated_confidence_too_low,
# play_score_too_low) auto-REJECTED every dog because they demand
# >45% model confidence — a FAVORITE-picking rule. A 6.5 dog wins
# ~20% of the time; that IS the edge (odds premium), not a defect.
# For value-tier matches we bypass those two vetoes and score from
# the validated tier quality instead of win-probability. Genuine
# protections stay: extreme_neg_ev, ev_too_high_trap, htft_reversal
# _risk_high, v25_v27_hard_disagreement. Result: 28 → 602 staked
# bets (22x volume), -1.6u → +39.4u profit. ALL rich analysis data
# (market_board, v25/v27, triple_value, probs) is untouched — only
# the `playable` flag changes.
#
# MULTI-LEG VERDICT (definitive): parlays DESTROY edge.
# 1-leg +3.4% → 2-leg -32% → 3-leg -67% → 4-leg -83%.
# System must bet SINGLES only. No combo recommendations.
#
# Non-MS markets: ultrastrict tiers (rarely pass BET) → info-only.
# All non-MS configurations showed negative ROI in backtest.
# ═══════════════════════════════════════════════════════════════════
MARKET_ODDS_TIERS = {
# ── MS (Match Score / 1X2) — the ONLY profitable market ────────
"MS": [
# PREMIUM — the validated edge. 6.0-7.5 odds, model >= market.
# 60d: 602 bets, +32.7% ROI, all weeks positive. AUTO-STAKED.
# Low hit (~20%) → high variance; stake stays small (see _brain_stake).
{"min_odds": 6.00, "max_odds": 7.50, "min_edge": -0.20,
"max_edge": 0.25, "min_reliability": 0.30,
"min_model_gap": 0.0,
"require_v27_agree": False, "require_no_trap": False,
"value_tier": "premium",
"label": "ms_underdog_premium"},
# STRONG — 5.0-6.0. Breakeven (-1%) → WATCH (visible, not staked).
{"min_odds": 5.00, "max_odds": 6.00, "min_edge": -0.20,
"max_edge": 0.25, "min_reliability": 0.30,
"min_model_gap": 0.0,
"require_v27_agree": False, "require_no_trap": False,
"value_tier": "strong",
"label": "ms_underdog_strong"},
# STANDARD — 3.0-5.0 volume zone. +0.5% breakeven → WATCH.
{"min_odds": 3.00, "max_odds": 5.00, "min_edge": -0.18,
"max_edge": 0.25, "min_reliability": 0.30,
"min_model_gap": 0.0,
"require_v27_agree": False, "require_no_trap": False,
"value_tier": "standard",
"label": "ms_underdog_standard"},
],
# ── Non-MS markets: visible but NOT playable ───────────────────
# All non-MS markets showed negative ROI in 50K-row backtest.
# Tiers exist so the model's read is surfaced (bet_summary),
# but criteria are strict enough that almost nothing passes BET.
# The user sees info; the system doesn't lose money on them.
"DC": [
{"min_odds": 1.15, "max_odds": 1.60, "min_edge": 0.02,
"max_edge": 0.12, "min_reliability": 0.55,
"max_model_gap": -0.02,
"require_v27_agree": False, "require_no_trap": True,
"label": "dc_ultrastrict"},
],
"OU25": [
{"min_odds": 1.60, "max_odds": 2.20, "min_edge": 0.02,
"max_edge": 0.10, "min_reliability": 0.55,
"max_model_gap": -0.03,
"require_v27_agree": False, "require_no_trap": True,
"label": "ou25_ultrastrict"},
],
"OU35": [
{"min_odds": 1.50, "max_odds": 2.50, "min_edge": 0.02,
"max_edge": 0.12, "min_reliability": 0.50,
"max_model_gap": -0.02,
"require_v27_agree": False, "require_no_trap": True,
"label": "ou35_ultrastrict"},
],
"BTTS": [
{"min_odds": 1.60, "max_odds": 2.10, "min_edge": 0.02,
"max_edge": 0.10, "min_reliability": 0.55,
"max_model_gap": -0.03,
"require_v27_agree": False, "require_no_trap": True,
"label": "btts_ultrastrict"},
],
"HT": [
{"min_odds": 2.00, "max_odds": 3.50, "min_edge": 0.02,
"max_edge": 0.12, "min_reliability": 0.50,
"max_model_gap": -0.02,
"require_v27_agree": False, "require_no_trap": True,
"label": "ht_ultrastrict"},
],
"OU15": [
{"min_odds": 1.30, "max_odds": 2.00, "min_edge": 0.02,
"max_edge": 0.12, "min_reliability": 0.50,
"max_model_gap": -0.02,
"require_v27_agree": False, "require_no_trap": True,
"label": "ou15_ultrastrict"},
],
"HTFT": [
{"min_odds": 4.00, "max_odds": 15.00, "min_edge": 0.03,
"max_edge": 0.15, "min_reliability": 0.45,
"require_v27_agree": False, "require_no_trap": True,
"label": "htft_ultrastrict"},
],
"OE": [
{"min_odds": 1.80, "max_odds": 2.10, "min_edge": 0.02,
"max_edge": 0.08, "min_reliability": 0.55,
"max_model_gap": -0.03,
"require_v27_agree": False, "require_no_trap": True,
"label": "oe_ultrastrict"},
],
"HT_OU05": [
{"min_odds": 1.30, "max_odds": 2.00, "min_edge": 0.02,
"max_edge": 0.12, "min_reliability": 0.50,
"max_model_gap": -0.02,
"require_v27_agree": False, "require_no_trap": True,
"label": "ht_ou05_ultrastrict"},
],
"HT_OU15": [
{"min_odds": 1.60, "max_odds": 3.00, "min_edge": 0.02,
"max_edge": 0.12, "min_reliability": 0.50,
"max_model_gap": -0.02,
"require_v27_agree": False, "require_no_trap": True,
"label": "ht_ou15_ultrastrict"},
],
"CARDS": [
{"min_odds": 1.60, "max_odds": 2.50, "min_edge": 0.02,
"max_edge": 0.10, "min_reliability": 0.50,
"max_model_gap": -0.02,
"require_v27_agree": False, "require_no_trap": True,
"label": "cards_ultrastrict"},
],
}
# Legacy flat envelope (backward compat for markets not in tiered system)
MARKET_OPTIMAL_FILTERS = {}
MARKET_PRIORS = {
"DC": 4.0,
"OU15": 3.0,
@@ -197,7 +366,7 @@ class BettingBrain:
rejected = [d for d in decisions if d.get("action") == "REJECT"]
guarded["betting_brain"] = {
"version": "judge-v2-score-coherent",
"version": "judge-v31d-evidence-tiers",
"decision": decision,
"reason": decision_reason,
"main_pick_key": main_key or None,
@@ -240,6 +409,21 @@ class BettingBrain:
triple_is_value = bool((triple or {}).get("is_value"))
consensus = str((package.get("v27_engine") or {}).get("consensus") or "").upper()
# V29c: Compute trap_market_flag early (needed by tier require_no_trap)
trap_market_flag = False
trap_market_gap = None
if isinstance(triple, dict):
_band_rate = self._safe_float(triple.get("band_rate"))
_implied = self._safe_float(triple.get("implied_prob"))
if (
_band_rate is not None
and _implied is not None
and band_sample >= self.MIN_BAND_SAMPLE
and (_implied - _band_rate) > self.TRAP_MARKET_GAP
):
trap_market_flag = True
trap_market_gap = round(_implied - _band_rate, 4)
positives: List[str] = []
issues: List[str] = []
vetoes: List[str] = []
@@ -256,7 +440,7 @@ class BettingBrain:
if market in self.SNIPER_BLOCKED_MARKETS:
is_value_sniper = False
if is_value_sniper:
score += 20.0
score += 8.0 # V29b: reduced from 20, tiers do the real filtering
positives.append("value_sniper_override")
score += max(0.0, min(20.0, calibrated_conf * 0.22))
@@ -276,7 +460,7 @@ class BettingBrain:
if odds_rel < 0.30:
score -= 22.0
issues.append("very_low_reliability_league")
if market in {"MS", "DC", "OU25", "BTTS"} and not is_value_sniper:
if market in {"MS", "DC", "OU25", "BTTS"}: # V29: hard veto, no sniper bypass
vetoes.append("low_reliability_league_hard_block")
elif odds_rel < 0.45:
score -= 12.0
@@ -305,38 +489,79 @@ class BettingBrain:
# ev_edge < 0 = "model market'in altında olasılık veriyor" = vig'i
# yiyemeyeceğimiz negative-EV bahis. Hard veto: oynama.
# Sniper override hâlâ geçer (yüksek convicted alternatif pick'ler).
if ev_edge < 0.0 and not is_value_sniper:
vetoes.append("negative_ev_edge")
issues.append(f"ev_edge={ev_edge:.3f}_below_zero")
# V29b: negative_ev_edge hard veto REMOVED — tier system handles
# edge bounds per-market via min_edge. MS underdog tier allows
# ev >= -0.15, so a universal ev<0 veto would kill profitable bets.
if ev_edge < -0.20: # Only veto truly extreme negative edge
vetoes.append("extreme_negative_ev_edge")
issues.append(f"ev_edge={ev_edge:.3f}_extreme_negative")
# Trap edge: bizim diagnostic backtest'te ev_edge >= 0.20 olan tüm
# bahisler kaybediyordu (n=10, hepsi -%25+ ROI). Model market'i bu
# kadar yanlış buluyorsa muhtemelen modelin kendisinin yanlış olduğu
# bir senaryo (eksik info, tuhaf maç, vs.) — oynama.
if ev_edge >= 0.20 and not is_value_sniper:
if ev_edge >= 0.30: # V29b: raised from 0.20, tiers cap at 0.25
vetoes.append("ev_edge_too_high_trap")
issues.append(f"ev_edge={ev_edge:.3f}_trap_range")
# ── MUTED MARKETS (grid search showed no profitable config) ──
if market in self.MUTED_MARKETS and not is_value_sniper:
if market in self.MUTED_MARKETS: # V29: hard veto, no sniper bypass
vetoes.append("market_muted_by_backtest")
issues.append(f"market_{market}_muted")
# ── PER-MARKET OPTIMAL ENVELOPE (from grid search) ──
envelope = self.MARKET_OPTIMAL_FILTERS.get(market)
if envelope and not is_value_sniper:
if ev_edge < envelope["min_edge"]:
# ── V30: ODDS-TIERED ENVELOPE (from 7K backtest grid search) ──
# Each market has multiple odds zones with different filters.
# If a bet doesn't fit ANY tier, it gets vetoed.
# V30: added model_gap filtering — data shows model>market is
# inversely correlated with winning for BTTS/OU25.
tiers = self.MARKET_ODDS_TIERS.get(market, [])
# Also check legacy flat envelope for backward compat
legacy_env = self.MARKET_OPTIMAL_FILTERS.get(market)
tier_matched = False
tier_label = None
tier_value = None # V31c: quality tier (premium/strong/standard)
if tiers:
for tier in tiers:
if not (tier["min_odds"] <= odds <= tier["max_odds"]):
continue
if ev_edge < tier["min_edge"] or ev_edge > tier["max_edge"]:
continue
if odds_rel < tier["min_reliability"]:
continue
if tier.get("require_v27_agree") and consensus != "AGREE":
continue
if tier.get("require_no_trap") and trap_market_flag:
continue
# V30: model-market gap filter
if model_gap is not None:
if "min_model_gap" in tier and model_gap < tier["min_model_gap"]:
continue
if "max_model_gap" in tier and model_gap > tier["max_model_gap"]:
continue
tier_matched = True
tier_label = tier.get("label")
tier_value = tier.get("value_tier") # V31c
break
if not tier_matched:
vetoes.append("outside_all_odds_tiers")
issues.append(f"no_profitable_tier_for_{market}_at_odds_{odds:.2f}")
elif legacy_env:
if ev_edge < legacy_env["min_edge"]:
vetoes.append("outside_envelope_edge_low")
if ev_edge > envelope["max_edge"]:
if ev_edge > legacy_env["max_edge"]:
vetoes.append("outside_envelope_edge_high")
if odds and odds < envelope["min_odds"]:
if odds and odds < legacy_env["min_odds"]:
vetoes.append("outside_envelope_odds_low")
if odds and odds > envelope["max_odds"]:
if odds and odds > legacy_env["max_odds"]:
vetoes.append("outside_envelope_odds_high")
if odds_rel < envelope["min_reliability"]:
if odds_rel < legacy_env["min_reliability"]:
vetoes.append("outside_envelope_reliability_low")
if envelope["require_v27_agree"] and consensus != "AGREE":
if legacy_env.get("require_v27_agree") and consensus != "AGREE":
vetoes.append("outside_envelope_v27_must_agree")
# V31d: a matched value tier is the validated profitable signal.
# It unlocks the value-betting regime (veto bypass + score floor).
is_value_tier = tier_value in self.VALUE_TIER_NAMES
if divergence is not None:
if divergence >= self.HARD_DIVERGENCE and not is_value_sniper:
score -= 42.0
@@ -348,22 +573,10 @@ 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")
# Trap market score penalty (flag computed above, before tier check)
if trap_market_flag:
score -= 14.0
issues.append("trap_market_market_overpriced")
if isinstance(triple, dict):
if triple_is_value:
@@ -465,6 +678,41 @@ class BettingBrain:
if sniper_bypassed:
positives.append("sniper_bypassed_soft_vetoes")
# ── V31d: VALUE-TIER REGIME ──────────────────────────────────────
# A matched MS value tier is the validated profitable signal (60d:
# premium 6.0-7.5 → +32.7% ROI). Underdogs are bet on the odds
# premium, not on win-probability, so:
# (1) waive the two favorite-confidence vetoes (genuine safety
# vetoes — extreme_neg_ev, ev_too_high_trap, htft_reversal
# _risk_high, v25_v27_hard_disagreement, low_reliability_hard
# — are NOT waived and still reject);
# (2) replace the favorite-confidence SCORE with a value floor so
# premium can clear MIN_BET_SCORE while strong/standard stay
# WATCH-level. All rich analysis output is untouched.
value_tier_bypassed: List[str] = []
if is_value_tier:
if vetoes:
remaining = []
for v in vetoes:
if v in self.VALUE_TIER_BYPASSABLE_VETOES:
value_tier_bypassed.append(v)
else:
remaining.append(v)
vetoes = remaining
if value_tier_bypassed:
positives.append("value_tier_bypassed_favorite_vetoes")
# Value-regime score: floor by tier quality + small +EV nudge.
value_score = self.VALUE_TIER_BASE_SCORE.get(tier_value, 50.0)
value_score += max(-5.0, min(10.0, ev_edge * 35.0))
if odds_rel >= 0.45:
value_score += 3.0
# Only premium is auto-staked; cap the rest below MIN_BET_SCORE
# so they surface as WATCH (visible analysis, not a staked bet).
if tier_value != "premium":
value_score = min(value_score, 60.0)
score = value_score
positives.append(f"value_tier_{tier_value}")
score = max(0.0, min(100.0, score))
action = "BET"
if vetoes:
@@ -487,8 +735,12 @@ class BettingBrain:
"issues": issues[:6],
"vetoes": vetoes[:6],
"sniper_bypassed": sniper_bypassed,
"value_tier_bypassed": value_tier_bypassed, # V31d
"is_value_tier": is_value_tier, # V31d
"trap_market_flag": trap_market_flag,
"trap_market_gap": trap_market_gap,
"tier_label": tier_label,
"value_tier": tier_value, # V31c: premium/strong/standard
"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,
@@ -501,10 +753,15 @@ class BettingBrain:
if action != "BET":
self._force_no_bet(row, f"betting_brain_{action.lower()}")
else:
row["is_guaranteed"] = bool(score >= 82.0)
# V31d: value-tier underdogs are high-variance (~20% hit) — never
# label them "guaranteed" no matter how high the value score is.
row["is_guaranteed"] = bool(score >= 82.0) and not is_value_tier
row["pick_reason"] = "betting_brain_approved"
row["stake_units"] = self._brain_stake(row, score)
row["bet_grade"] = "A" if score >= 82.0 else "B"
# V31c: bet_grade now reflects value_tier so the UI can show
# the bettor which quality band a pick belongs to.
row["bet_grade"] = self._grade_from_tier(tier_value, score)
row["value_tier"] = tier_value
row["playable"] = True
self._append_reason(row, f"betting_brain_{action.lower()}_{round(score)}")
@@ -600,12 +857,14 @@ class BettingBrain:
def _summary_item(self, row: Dict[str, Any]) -> Dict[str, Any]:
reasons = list(row.get("decision_reasons") or row.get("reasons") or [])
brain = row.get("betting_brain") or {}
return {
"market": row.get("market"),
"pick": row.get("pick"),
"raw_confidence": row.get("raw_confidence", row.get("confidence")),
"calibrated_confidence": row.get("calibrated_confidence", row.get("confidence")),
"bet_grade": row.get("bet_grade", "PASS"),
"value_tier": row.get("value_tier") or brain.get("value_tier"), # V31c
"playable": bool(row.get("playable")),
"stake_units": float(row.get("stake_units", 0.0) or 0.0),
"play_score": row.get("play_score", 0.0),
@@ -615,9 +874,22 @@ class BettingBrain:
"odds": row.get("odds", 0.0),
"reasons": reasons[:6],
"is_underdog_reference": bool(row.get("is_underdog_reference")),
"betting_brain": row.get("betting_brain"),
"betting_brain": brain,
}
@staticmethod
def _grade_from_tier(value_tier: Optional[str], score: float) -> str:
"""V31c: Map value_tier → bet grade so the UI surfaces the
quality band. Falls back to score-based grade for untiered picks.
premium → A (deep underdog, highest ROI, high variance)
strong → B (strong underdog)
standard → C (volume zone, thin edge)
"""
mapping = {"premium": "A", "strong": "B", "standard": "C"}
if value_tier in mapping:
return mapping[value_tier]
return "A" if score >= 82.0 else "B"
@staticmethod
def _candidate_sort_key(row: Dict[str, Any]) -> Tuple[float, float, float]:
brain = row.get("betting_brain") or {}
@@ -657,6 +929,12 @@ class BettingBrain:
odds = self._safe_float(row.get("odds"), 0.0) or 0.0
if odds <= 1.0:
return 0.0
# V31d: value-tier underdogs use a small FLAT stake (high variance),
# not the score-scaled favorite stake. score is high (70+) by design
# but that reflects validated tier EV, not win-probability.
brain = row.get("betting_brain") or {}
if brain.get("is_value_tier") or brain.get("value_tier") in self.VALUE_TIER_NAMES:
return self.VALUE_TIER_STAKE_UNITS
cap = 2.0 if score >= 82.0 else 1.2
if score < 78.0:
cap = 0.8