This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"_meta": {
|
||||
"purpose": "A-milli erkek futbol ligleri. betting_brain milli-maç gate'i bu listeyle tetiklenir.",
|
||||
"strategy": "Milli maçta SADECE MS, oran 4.0-7.0, Hazırlık+Eleme oynanabilir. Turnuva/diğer market analiz-only. Backtest: +17% ROI, kararlılık-test geçti (eski/yeni yarı +22/+24%).",
|
||||
"source": "2300-maç milli backtest (multi_backtest_20260602) segment+grid+stability analizi",
|
||||
"competition_type_rule": "lig adı: 'hazırlık'->HAZIRLIK | 'eleme'/'play-off'->ELEME | diğer->TURNUVA"
|
||||
},
|
||||
"league_ids": [
|
||||
"cesdwwnxbc5fmajgroc0hqzy2",
|
||||
"3aa4mumjl6zyetg6o9hwd5hhx",
|
||||
"40yjcbx2sq6oq736iqqqczwt1",
|
||||
"39q1hq42hxjfylxb7xpe9bvf9",
|
||||
"cu0rmpyff5692eo06ltddjo8a",
|
||||
"ax1yf4nlzqpcji4j8epdgx3zl",
|
||||
"1gxlzw2ezkyeykhcaa5x8ozkk",
|
||||
"gfskxsdituog2kqp9yiu7bzi",
|
||||
"595nsvo7ykvoe690b1e4u5n56",
|
||||
"68zplepppndhl8bfdvgy9vgu1",
|
||||
"3a0j0giz3c3ajw9h59evv7lqt",
|
||||
"emy1ibc8fu2l0fukh4vlu5xl5",
|
||||
"2db0aw1duj2my9l5iey5gm6nq",
|
||||
"cc5tzz23tryrfqbm2pbv0jill",
|
||||
"8tddm56zbasf57jkkay4kbf11",
|
||||
"2r1hqz453bn9ljzt53kdr2lwb",
|
||||
"93i7thp7zi0ympyt6l8aa1r2i",
|
||||
"45db8orh1qttbsqq9hqapmbit",
|
||||
"ude9t6yj60lebbn356qzg4k4",
|
||||
"9qzn8cs96sgtqmesa9gpfti23",
|
||||
"ad8y7vdjhinfqv4wo8rod6dck"
|
||||
]
|
||||
}
|
||||
@@ -41,6 +41,23 @@ class BettingBrain:
|
||||
VALUE_TIER_STAKE_UNITS = 0.5
|
||||
TRAP_MARKET_GAP = 0.10
|
||||
|
||||
# ── V31f: NATIONAL-TEAM REGIME ───────────────────────────────────────
|
||||
# National matches behave nothing like clubs (2300-match backtest):
|
||||
# * Only MS carries edge — OU/BTTS/HT/DC/OE all -12%..-21% → hard mute.
|
||||
# * MS edge lives in the 4.0–7.0 odds band for HAZIRLIK/ELEME fixtures
|
||||
# (+17% ROI, stable across older/newer halves: +22%/+24%).
|
||||
# * Favorites (odds<3) lose (-10..-18%); TURNUVA inverts the pattern
|
||||
# (4-7 band is -9% there) → tournaments get NO bet (analysis only).
|
||||
# Calibration is fine; this is a *bet-selection* gate, applied only when
|
||||
# match_info.is_national is True. Clubs are completely unaffected.
|
||||
# See mds/national-team-strategy.md.
|
||||
NATIONAL_BET_MARKET = "MS"
|
||||
NATIONAL_MIN_ODDS = 4.0
|
||||
NATIONAL_MAX_ODDS = 7.0
|
||||
NATIONAL_ALLOWED_COMPETITIONS = {"HAZIRLIK", "ELEME"}
|
||||
NATIONAL_BASE_SCORE = 66.0 # clears MIN_BET_SCORE(62) when gate passes
|
||||
NATIONAL_STAKE_UNITS = 0.5 # flat, high-variance band (~24% hit)
|
||||
|
||||
MARKET_MIN_CONFIDENCE = {
|
||||
"MS": 45.0,
|
||||
"DC": 55.0,
|
||||
@@ -366,7 +383,7 @@ class BettingBrain:
|
||||
|
||||
rejected = [d for d in decisions if d.get("action") == "REJECT"]
|
||||
guarded["betting_brain"] = {
|
||||
"version": "judge-v31d-evidence-tiers",
|
||||
"version": "judge-v31f-national-regime",
|
||||
"decision": decision,
|
||||
"reason": decision_reason,
|
||||
"main_pick_key": main_key or None,
|
||||
@@ -396,6 +413,10 @@ class BettingBrain:
|
||||
pick = str(row.get("pick") or "")
|
||||
model_prob = self._market_probability(row, package)
|
||||
odds = self._safe_float(row.get("odds"), 0.0) or 0.0
|
||||
# V31f: national-team match flags (set by orchestrator in match_info).
|
||||
_mi = package.get("match_info") or {}
|
||||
is_national = bool(_mi.get("is_national"))
|
||||
competition_type = str(_mi.get("competition_type") or "")
|
||||
implied = (1.0 / odds) if odds > 1.0 else 0.0
|
||||
model_gap = (model_prob - implied) if model_prob is not None and implied > 0 else None
|
||||
calibrated_conf = self._safe_float(row.get("calibrated_confidence", row.get("confidence")), 0.0) or 0.0
|
||||
@@ -713,6 +734,85 @@ class BettingBrain:
|
||||
score = value_score
|
||||
positives.append(f"value_tier_{tier_value}")
|
||||
|
||||
# ── V31f: NATIONAL-TEAM REGIME (overrides club logic) ─────────────
|
||||
# For national matches the validated strategy is a narrow, mechanical
|
||||
# value gate (MS / odds 4-7 / Hazırlık+Eleme). We REPLACE the club
|
||||
# verdict so club-tuned vetoes/scores don't distort it. All the rich
|
||||
# analysis (probs, model_gap, divergence, triple) above is preserved
|
||||
# in the payload below — only action/score/stake are decided here.
|
||||
national_gate_passed = False
|
||||
if is_national:
|
||||
in_band = self.NATIONAL_MIN_ODDS <= odds <= self.NATIONAL_MAX_ODDS
|
||||
is_bet_market = market == self.NATIONAL_BET_MARKET
|
||||
comp_ok = competition_type in self.NATIONAL_ALLOWED_COMPETITIONS
|
||||
# Genuine safety vetoes still kill the bet even for national matches.
|
||||
hard_unsafe = {
|
||||
"low_reliability_league_hard_block",
|
||||
"v25_v27_hard_disagreement",
|
||||
"extreme_negative_ev",
|
||||
"htft_reversal_risk_high",
|
||||
}
|
||||
has_hard_unsafe = any(v in hard_unsafe for v in vetoes)
|
||||
|
||||
national_vetoes: List[str] = []
|
||||
if not is_bet_market:
|
||||
national_vetoes.append("national_non_ms_market_muted")
|
||||
if not comp_ok:
|
||||
national_vetoes.append("national_tournament_no_bet")
|
||||
if not in_band:
|
||||
national_vetoes.append("national_odds_outside_value_band")
|
||||
if has_hard_unsafe:
|
||||
national_vetoes.append("national_hard_safety_veto")
|
||||
|
||||
if national_vetoes:
|
||||
vetoes = national_vetoes
|
||||
action = "REJECT"
|
||||
score = min(score, 40.0)
|
||||
issues.append(f"national_gate:{competition_type or 'unknown'}")
|
||||
else:
|
||||
vetoes = []
|
||||
national_gate_passed = True
|
||||
score = self.NATIONAL_BASE_SCORE + max(-4.0, min(8.0, ev_edge * 30.0))
|
||||
score = max(0.0, min(100.0, score))
|
||||
action = "BET"
|
||||
positives.append("national_value_gate_passed")
|
||||
issues.append(f"national_gate:{competition_type}")
|
||||
# skip the club action logic below
|
||||
row["betting_brain"] = {
|
||||
"action": action,
|
||||
"score": round(score, 1),
|
||||
"summary": self._summary(action, market, pick, positives, issues, vetoes),
|
||||
"positives": positives[:5],
|
||||
"issues": issues[:6],
|
||||
"vetoes": vetoes[:6],
|
||||
"sniper_bypassed": sniper_bypassed,
|
||||
"value_tier_bypassed": [],
|
||||
"is_value_tier": False,
|
||||
"is_national": True, # V31f
|
||||
"competition_type": competition_type,
|
||||
"trap_market_flag": trap_market_flag,
|
||||
"trap_market_gap": trap_market_gap,
|
||||
"tier_label": "national_value" if national_gate_passed else None,
|
||||
"value_tier": None,
|
||||
"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,
|
||||
"v27_prob": round(v27_prob, 4) if v27_prob is not None else None,
|
||||
"divergence": round(divergence, 4) if divergence is not None else None,
|
||||
"triple_key": triple_key,
|
||||
"triple_value": triple,
|
||||
}
|
||||
if national_gate_passed:
|
||||
row["is_guaranteed"] = False # high-variance band, never "guaranteed"
|
||||
row["pick_reason"] = "national_value_gate"
|
||||
row["stake_units"] = self.NATIONAL_STAKE_UNITS
|
||||
row["bet_grade"] = "B"
|
||||
row["playable"] = True
|
||||
else:
|
||||
self._force_no_bet(row, f"betting_brain_{action.lower()}")
|
||||
self._append_reason(row, f"betting_brain_national_{action.lower()}_{round(score)}")
|
||||
return row
|
||||
|
||||
score = max(0.0, min(100.0, score))
|
||||
action = "BET"
|
||||
if vetoes:
|
||||
|
||||
@@ -108,6 +108,26 @@ class MarketBoardMixin:
|
||||
"hit": info.get("hit"),
|
||||
}
|
||||
|
||||
def _is_national_match(self, league_id: Optional[str]) -> bool:
|
||||
"""True if this league is an A-milli (senior men's) national competition."""
|
||||
if not league_id:
|
||||
return False
|
||||
natl = getattr(self, "national_leagues", None) or set()
|
||||
return str(league_id) in natl
|
||||
|
||||
def _competition_type_for(
|
||||
self, league_id: Optional[str], league_name: Optional[str]
|
||||
) -> Optional[str]:
|
||||
"""For national matches, classify HAZIRLIK/ELEME/TURNUVA from the league
|
||||
name. None for non-national leagues (clubs don't use this)."""
|
||||
if not self._is_national_match(league_id):
|
||||
return None
|
||||
try:
|
||||
from utils.national_leagues import classify_competition
|
||||
return classify_competition(league_name or "")
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _build_prediction_package(
|
||||
self,
|
||||
data: MatchData,
|
||||
@@ -346,6 +366,9 @@ class MarketBoardMixin:
|
||||
# Backtest-derived per-league confidence (ROI + sample size).
|
||||
# None when the league has too little data to judge → FE shows no badge.
|
||||
"league_confidence": self._league_confidence_for(data.league_id),
|
||||
# National-team match flags (drive betting_brain's national gate).
|
||||
"is_national": self._is_national_match(data.league_id),
|
||||
"competition_type": self._competition_type_for(data.league_id, data.league_name),
|
||||
"match_date_ms": data.match_date_ms,
|
||||
"sport": data.sport,
|
||||
# Live snapshot — match_commentary uses this to detect upset-in-progress
|
||||
|
||||
@@ -58,6 +58,7 @@ 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 utils.league_confidence import load_league_confidence
|
||||
from utils.national_leagues import load_national_leagues
|
||||
from config.config_loader import build_threshold_dict, get_threshold_default, get_config
|
||||
from models.calibration import get_calibrator
|
||||
|
||||
@@ -173,6 +174,7 @@ class SingleMatchOrchestrator(
|
||||
self.top_league_ids = load_top_league_ids()
|
||||
self.league_reliability = load_league_reliability()
|
||||
self.league_confidence = load_league_confidence()
|
||||
self.national_leagues = load_national_leagues()
|
||||
self.enrichment = FeatureEnrichmentService()
|
||||
self.odds_band_analyzer = OddsBandAnalyzer()
|
||||
# ── Market Thresholds (loaded from config/market_thresholds.json) ──
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
"""
|
||||
National-Team League Loader + Competition-Type Classifier
|
||||
=========================================================
|
||||
Loads the A-milli (senior men's) football league IDs from
|
||||
data/national_leagues.json and classifies a league name into a
|
||||
competition type. Powers the betting_brain national-match gate.
|
||||
|
||||
Why this exists:
|
||||
Backtest (2300 national matches) showed national matches behave very
|
||||
differently from clubs — only the MS market carries edge, and only in
|
||||
the 4.0–7.0 odds band for Hazırlık/Eleme fixtures (tournaments behave
|
||||
inversely). Calibration is fine; the issue is *which* bets to allow.
|
||||
See mds/national-team-strategy.md.
|
||||
|
||||
Usage:
|
||||
from utils.national_leagues import load_national_leagues, classify_competition
|
||||
natl = load_national_leagues() # set[str] of league_ids
|
||||
ctype = classify_competition(name) # "HAZIRLIK" | "ELEME" | "TURNUVA"
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import Set
|
||||
|
||||
|
||||
_DATA_FILE = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
"..",
|
||||
"data",
|
||||
"national_leagues.json",
|
||||
)
|
||||
|
||||
|
||||
def load_national_leagues() -> Set[str]:
|
||||
"""Return the set of A-milli football league IDs (empty on any failure)."""
|
||||
if not os.path.isfile(_DATA_FILE):
|
||||
print(
|
||||
f"⚠️ national_leagues.json not found at {_DATA_FILE}. "
|
||||
"National-match gate disabled (no league treated as national)."
|
||||
)
|
||||
return set()
|
||||
try:
|
||||
with open(_DATA_FILE, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
ids = set(str(x) for x in (data.get("league_ids") or []))
|
||||
print(f"✅ Loaded {len(ids)} national-team league IDs")
|
||||
return ids
|
||||
except (json.JSONDecodeError, KeyError, TypeError) as exc:
|
||||
print(f"⚠️ Failed to parse national_leagues.json: {exc}")
|
||||
return set()
|
||||
|
||||
|
||||
def classify_competition(league_name: str) -> str:
|
||||
"""Map a league name to a competition type.
|
||||
|
||||
HAZIRLIK = friendlies, ELEME = qualifiers/play-offs, TURNUVA = finals/cups.
|
||||
The backtest edge lives in HAZIRLIK+ELEME (MS, odds 4-7); TURNUVA is
|
||||
handled conservatively (no bet) by the gate.
|
||||
"""
|
||||
n = (league_name or "").lower()
|
||||
if "hazırlık" in n or "hazirlik" in n or "friendl" in n:
|
||||
return "HAZIRLIK"
|
||||
if "eleme" in n or "play-off" in n or "playoff" in n or "qualif" in n:
|
||||
return "ELEME"
|
||||
return "TURNUVA"
|
||||
@@ -0,0 +1,70 @@
|
||||
# Milli Takım / Dünya Kupası — Bahis Stratejisi (Veri-Temelli)
|
||||
|
||||
> Kaynak: 2.300 maçlık milli backtest (multi_backtest_20260602, /tmp/bt_natl.csv).
|
||||
> Tüm rakamlar offline simülasyon (production'a dokunulmadan, aynı veride kural testi).
|
||||
> Tarih: 2026-06.
|
||||
|
||||
## Temel Bulgular (kanıtlanmış)
|
||||
1. **Kalibrasyon İYİ** (MS ECE 1.6, OU15 2.2) — model olasılıkları milli maçta da doğru.
|
||||
Sorun kalibrasyon değil EDGE. Yani piyasa oranları da keskin; avantaj sadece
|
||||
belirli segmentlerde var.
|
||||
2. **Sadece MS market'inde edge var.** OU/BTTS/HT/DC/OE hepsi "bet-all" ROI
|
||||
−12%..−21% — milli maçta gol/skor marketleri güvenilmez, KAPATILMALI.
|
||||
3. **MS'te edge oran bandına + rekabet türüne bağlı:**
|
||||
- Favori (oran<3): zararlı (−10..−18%). Milli favoriler takılır (rotasyon/motivasyon).
|
||||
- Denk-üstü (oran 4-7): ELEME/HAZIRLIK'ta kârlı, TURNUVA'da zararlı.
|
||||
4. **Rekabet türü kritik faktör** (DB'de feature YOK, lig adından türetilir):
|
||||
HAZIRLIK / ELEME / TURNUVA çok farklı davranır.
|
||||
|
||||
## Grid + Kararlılık Testi (overfit'e karşı)
|
||||
En iyi kombolar (N>=150, MS market):
|
||||
| kural | N | hit% | ROI |
|
||||
|---|---|---|---|
|
||||
| 4.0-7.0 sadece ELEME | 585 | 25% | +23.1% |
|
||||
| 3.5-6.0 HAZ+ELE | 1021 | 25% | +14.5% |
|
||||
| **4.0-7.0 HAZ+ELE (SEÇİLEN)** | **865** | **24%** | **+17.1%** |
|
||||
| 3.0-6.0 HAZ+ELE | 1381 | 25% | +10.1% |
|
||||
|
||||
**Kararlılık (en güçlü kanıt):** "4-7 sadece ELEME" eski yarı +22.1% / yeni yarı +24.0%
|
||||
→ iki bağımsız zaman diliminde de pozitif = overfit DEĞİL, sahada tutar.
|
||||
|
||||
## TURNUVA/FİNAL farkı (Dünya Kupası finalleri için kritik)
|
||||
Turnuva (Avrupa Şamp, Copa America, Uluslar Ligi, Gold Cup, Asya/Afrika Kupası):
|
||||
- 4-7 bandı turnuvada ZARARLI (−8.9%) — elemenin tersi.
|
||||
- Sadece underdog 5+ kârlı (+51% ama n=274, oynak, şans payı yüksek).
|
||||
- Sebep: büyük turnuva finallerinde favoriler tutarlı, sürpriz az.
|
||||
|
||||
## SEÇİLEN STRATEJİ (kullanıcı kararı)
|
||||
**Milli-maç gate kuralı:**
|
||||
- Market: SADECE MS (diğer tüm marketler milli maçta kapalı)
|
||||
- Oran bandı: 4.0 ≤ odds < 7.0
|
||||
- Rekabet türü: SADECE Hazırlık + Eleme
|
||||
- TURNUVA/FİNAL: bahis ÖNERME (sadece analiz/olasılık göster). Underdog +51%
|
||||
cazip ama oynak/az-örneklem → gerçek paraya bağlanmadı (kullanıcı kararı).
|
||||
Beklenen: +17% ROI, ~865 bahis/2300 maç. Mevcut gate +0.9% idi → ~19x iyileşme.
|
||||
|
||||
## Mimari Notu (uygulama için)
|
||||
- Sorun model değil → ayrı ML modeli GEREKSİZ (1898 maç zaten overfit riski; karar verildi: kurma).
|
||||
- Çözüm = betting brain'de milli-maça özel GATE (eğitim-sonrası kural katmanı).
|
||||
- Rekabet türü lig adından türetilir: 'hazırlık'→HAZIRLIK, 'eleme/play-off'→ELEME,
|
||||
diğer→TURNUVA. Milli lig tespiti: qualified_leagues.json'a eklenen 21 milli lig.
|
||||
- Kalıcı feature olarak rekabet türü eklenebilir (daha temiz) ama gate hardcode de yeter.
|
||||
|
||||
## Durum: UYGULANDI + DOĞRULANDI (betting_brain v31f-national-regime).
|
||||
Kod:
|
||||
- utils/national_leagues.py — loader (data/national_leagues.json, 21 lig) + classify_competition
|
||||
- single_match_orchestrator.py — self.national_leagues yüklenir
|
||||
- orchestrator/market_board.py — match_info.is_national + competition_type; _is_national_match/_competition_type_for helpers
|
||||
- betting_brain.py _judge_row — national regime bloğu: is_national ise club mantığını override eder,
|
||||
SADECE MS + 4.0-7.0 + (HAZIRLIK|ELEME) → BET (NATIONAL_BASE_SCORE 66, stake 0.5u, grade B),
|
||||
diğer her şey REJECT. Hard-safety vetoları (low_reliability_hard, v25_v27_hard, htft_reversal)
|
||||
national'da da geçerli. Rich analiz payload korunur.
|
||||
|
||||
DOĞRULAMA (V2 backtest, yeni gate aktif, 1829 maç, /tmp/bt_natl_v2.csv):
|
||||
BET=784 → TAMAMI MS, oran 4.00-6.99 (bant dışı 0 bahis), hit %23.7, ROI +16.0%, +125.7u.
|
||||
Simülasyondaki +17% ile birebir. OU/BTTS/HT/turnuva artık 0 BET.
|
||||
|
||||
NOT: ai-engine ~10:10'da restart oldu (compose) → national-gate + V31e recal + league_confidence
|
||||
kodu CANLI API'de aktif. Ama bunlar docker cp ile deploy edildi; kalıcılık için repo commit +
|
||||
image rebuild gerekir (yeni container build'inde kaybolur).
|
||||
## İlgili: 422 lig-gate düzeltmesi CANLIDA (qualified_leagues 48→69, milli ligler açıldı).
|
||||
Reference in New Issue
Block a user