""" Compara formas de construir el rango del 80% usando los errores que guardó backtest_proyeccion.py. La proyección central es la misma en todas: solo cambia cómo se convierte el error histórico en un rango. asim-K: cuantiles 10/90 del error con signo, en las K ventanas más recientes. sim-K: cuantil 80 del tamaño del error (sin signo), aplicado hacia arriba y hacia abajo por igual. No arrastra el sesgo de una época de subida o de bajada. Un rango del 80% bien calibrado contiene el valor real en ~80% de los meses. """ import json from pathlib import Path import numpy as np import pandas as pd from scipy.stats import binomtest A = Path(__file__).resolve().parent d = pd.read_csv(A / "backtest_proyeccion.csv") e = {r["mes"]: np.array(r["errores_bandas"]) for r in json.load(open(A / "backtest_errores.json"))} def rango(c, err, tipo): if tipo == "asim": return c / (1 + np.quantile(err, .9)), c / (1 + np.quantile(err, .1)) a = np.quantile(np.abs(np.log1p(err)), .8) return c * np.exp(-a), c * np.exp(a) filas = [] for tipo in ("asim", "sim"): for k in (6, 12, 24): dentro, ancho = [], [] for r in d.itertuples(): lo, hi = rango(r.proyectado, e[r.mes][: 3 * k], tipo) dentro.append(lo <= r.real <= hi); ancho.append(hi - lo) n, x = len(dentro), int(sum(dentro)) filas.append({"variante": f"{tipo}-{k}", "dentro": f"{x}/{n}", "cobertura_%": round(100 * x / n), "ancho_medio": round(float(np.mean(ancho))), "p_vs_80%": round(binomtest(x, n, .8).pvalue, 3)}) print(pd.DataFrame(filas).to_string(index=False))