gg2
Deploy Iddaai Backend / build-and-deploy (push) Successful in 35s

This commit is contained in:
2026-05-29 13:35:17 +03:00
parent b5cb412236
commit 671979b07d
3 changed files with 648 additions and 1 deletions
+102
View File
@@ -465,3 +465,105 @@ def get_calibrator() -> Calibrator:
if _calibrator_instance is None:
_calibrator_instance = Calibrator()
return _calibrator_instance
# ── FINAL-OUTPUT RECALIBRATION LAYER (V31e) ─────────────────────────────────
# A thin, LAST-STEP per-market map: production calibrated_confidence -> reality.
# Built from a 60-day backtest (scripts/fit_recalibrators.py); inference is a
# pure np.interp over a 99-point monotone grid — NO sklearn needed at runtime.
#
# WHY THIS EXISTS:
# The upstream chain (temperature scaling T=1.5 -> per-outcome isotonic ->
# POST_CAL_TRUST blend) crushes high-base-rate binary markets toward 0.5,
# so "system says 51%" can really hit 70%. MS survives (near-uniform picks),
# which is why MS is already well-calibrated and OU/HT-OU markets are not.
#
# SAFETY / "DO NO HARM":
# * Only markets whose fit-time ECE >= 5.0 carry a map (currently OU15, OU35,
# HT_OU05, HT_OU15). MS and every already-good market have NO map ->
# recalibrate_conf() returns the input UNCHANGED -> guaranteed no regression.
# * Out-of-sample validated (fit=older 65%, test=unseen 35%):
# MS ECE 1.1 -> 1.3 (flat, safe)
# HT_OU15 29.2 -> 0.8
# OU15 19.0 -> 3.3
# OU35 13.9 -> 4.3
# HT_OU05 11.5 -> 2.4
# * Adjusts ONLY the displayed confidence number. All rich analysis payload
# (probabilities, edges, vetoes, tiers, bands) is preserved untouched, and
# the pre-recalibration value is kept for audit by the caller.
FINAL_RECALIBRATOR_PATH = os.path.join(CALIBRATION_DIR, "final_recalibrators.json")
class FinalRecalibrator:
"""Per-market final-output recalibration via piecewise-linear interpolation.
Loads a compact JSON of 99-point lookup grids (x=calibrated_confidence/100,
y=reality). Markets absent from the file pass through as identity.
"""
def __init__(self, path: str = FINAL_RECALIBRATOR_PATH):
self.grid: Optional[np.ndarray] = None
self.maps: Dict[str, np.ndarray] = {}
self.source_path = path
self._load(path)
def _load(self, path: str) -> None:
if not os.path.exists(path):
print(f"[FinalRecalibrator] No map file at {path} — pass-through mode (all markets unchanged)")
return
try:
with open(path, "r") as f:
data = json.load(f)
meta = data.get("_meta", {})
grid = meta.get("grid")
if not grid:
print("[FinalRecalibrator] Map file missing _meta.grid — pass-through mode")
return
self.grid = np.asarray(grid, dtype=float)
for market, m in data.items():
if market == "_meta" or not isinstance(m, dict):
continue
y = m.get("y")
if y and len(y) == len(self.grid):
self.maps[str(market).upper()] = np.asarray(y, dtype=float)
else:
print(f"[FinalRecalibrator] Skipped {market}: grid/y length mismatch")
print(f"[FinalRecalibrator] Loaded reality maps for {sorted(self.maps.keys())} "
f"(everything else, incl. MS, passes through unchanged)")
except Exception as e:
print(f"[FinalRecalibrator] Warning: failed to load {path}: {e} — pass-through mode")
self.grid = None
self.maps = {}
def has_map(self, market: str) -> bool:
return bool(self.maps) and (market or "").upper() in self.maps
def recalibrate_conf(self, market: str, calibrated_conf: float) -> float:
"""Map a 0100 confidence to its reality-aligned value.
Markets without a trained map (including MS and all already-good
markets) return the input UNCHANGED. Any failure also returns the
input unchanged so this layer can never regress production.
"""
try:
key = (market or "").upper()
if self.grid is None or key not in self.maps:
return calibrated_conf
x = float(calibrated_conf) / 100.0
x = min(max(x, 0.0), 1.0)
y = float(np.interp(x, self.grid, self.maps[key]))
return max(1.0, min(99.0, y * 100.0))
except Exception:
return calibrated_conf
# Singleton instance
_final_recalibrator_instance: Optional[FinalRecalibrator] = None
def get_final_recalibrator() -> FinalRecalibrator:
"""Get or create the global FinalRecalibrator instance."""
global _final_recalibrator_instance
if _final_recalibrator_instance is None:
_final_recalibrator_instance = FinalRecalibrator()
return _final_recalibrator_instance