@@ -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 0–100 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
|
||||
|
||||
Reference in New Issue
Block a user