Repository Public Research preview
1"""Alignment of forecasts with realized targets and computation of losses (one long table per instrument).23Target for horizon h at origin t (day index i): Y_{t,h} = sum_{k=1..h} y_{i+k}, where y is a daily variance proxy4(percent^2) from wp46.scoring.proxies. The target is valid only if all h proxy days are valid. For the5'rv5ss_scaled' proxy, the scaling constant c_t = mean(r^2)/mean(rv5ss) is computed over the estimation window ending6at the origin (valid days only; no look-ahead). Forecast rows with status != ok/stale_fit get loss = NaN and keep their7status so that failure accounting is possible downstream."""8from __future__ import annotations910import numpy as np11import pandas as pd1213from .losses import LOSSES14from .proxies import proxy_series1516HORIZONS = (1, 5, 22)171819def cumulative_targets(panel: pd.DataFrame, proxy: str, horizons=HORIZONS, window: int = 1000) -> pd.DataFrame:20 y = proxy_series(panel, proxy)21 n = len(y)22 if proxy == "rv5ss_scaled":23 r2 = panel["r"].values ** 224 valid = np.isfinite(y)25 num = pd.Series(np.where(valid, r2, np.nan)).rolling(window, min_periods=200).mean().values26 den = pd.Series(y).rolling(window, min_periods=200).mean().values27 c = num / den # c_t known at the origin t28 out = {"origin": pd.to_datetime(panel["session"]).values}29 for h in horizons:30 tgt = np.full(n, np.nan)31 for i in range(n - h):32 seg = y[i + 1:i + 1 + h]33 if np.all(np.isfinite(seg)):34 tgt[i] = float(np.sum(seg)) * (c[i] if proxy == "rv5ss_scaled" else 1.0)35 out[f"y{h}"] = tgt36 return pd.DataFrame(out)373839def score_forecasts(F: pd.DataFrame, panel: pd.DataFrame, proxies=("rv5ss_on",), losses=("qlike",), horizons=HORIZONS,40 window: int = 1000) -> pd.DataFrame:41 """Long table: origin, spec_id, model_id, dist, tier, h, proxy, loss, value, status, forecast, target."""42 rows = []43 F = F.copy()44 F["origin"] = pd.to_datetime(F["origin"])45 for proxy in proxies:46 T = cumulative_targets(panel, proxy, horizons, window)47 M = F.merge(T, on="origin", how="left")48 okf = M["status"].isin(["ok", "stale_fit"])49 for h in horizons:50 fc = M[f"h{h}"].values if f"h{h}" in M else np.full(len(M), np.nan)51 y = M[f"y{h}"].values52 for ln in losses:53 v = LOSSES[ln](np.where(np.isfinite(y), y, np.nan), np.where(okf & np.isfinite(fc), fc, np.nan))54 v = np.where(okf & np.isfinite(y) & np.isfinite(fc), v, np.nan)55 rows.append(pd.DataFrame({"origin": M["origin"].values, "spec_id": M["spec_id"].values, "model_id": M["model_id"].values,56 "dist": M["dist"].values, "tier": M["tier"].values, "h": h, "proxy": proxy, "loss": ln,57 "value": v, "status": M["status"].values, "forecast": fc, "target": y}))58 return pd.concat(rows, ignore_index=True)596061def common_sample_mask(L: pd.DataFrame, spec_ids: list[str]) -> pd.Series:62 """Origins (per h/proxy/loss) at which every spec in spec_ids has a finite loss."""63 sub = L[L["spec_id"].isin(spec_ids)]64 ok = sub.assign(f=np.isfinite(sub["value"])).groupby(["origin", "h", "proxy", "loss"])["f"].agg(["sum", "size"])65 full = ok[(ok["sum"] == len(spec_ids)) & (ok["size"] == len(spec_ids))].index66 key = pd.MultiIndex.from_frame(L[["origin", "h", "proxy", "loss"]])67 return pd.Series(key.isin(full), index=L.index)686970def score_forecasts_wide(F: pd.DataFrame, panel: pd.DataFrame, proxies=("rv5ss_on",), losses=("qlike",), horizons=HORIZONS,71 window: int = 1000, primary_proxy: str = "rv5ss_on", primary_loss: str = "qlike") -> dict:72 """Memory-efficient scoring. Returns dict with73 'forecast': {h: DataFrame origin x spec_id (cumulative variance forecasts, NaN when status not ok/stale_fit)},74 'status': DataFrame origin x spec_id (status strings),75 'target': {(proxy, h): Series origin -> target},76 'loss': {(proxy, loss, h): DataFrame origin x spec_id};77 secondary losses are computed for the primary proxy only and secondary proxies for the primary loss only."""78 F = F.copy()79 F["origin"] = pd.to_datetime(F["origin"])80 okf = F["status"].isin(["ok", "stale_fit"])81 status = F.pivot_table(index="origin", columns="spec_id", values="status", aggfunc="first")82 fc = {}83 for h in horizons:84 col = f"h{h}"85 v = F[col].where(okf) if col in F else pd.Series(np.nan, index=F.index)86 fc[h] = F.assign(_v=v).pivot_table(index="origin", columns="spec_id", values="_v", aggfunc="first", dropna=False)87 for extra in ("mu", "dp1", "dp2", "sigma2_t"):88 if extra in F:89 fc[extra] = F.assign(_v=F[extra].where(okf)).pivot_table(index="origin", columns="spec_id", values="_v", aggfunc="first", dropna=False)90 targets, loss = {}, {}91 for proxy in proxies:92 T = cumulative_targets(panel, proxy, horizons, window).set_index("origin")93 for h in horizons:94 y = T[f"y{h}"].reindex(fc[h].index)95 targets[(proxy, h)] = y96 for ln in losses:97 if proxy != primary_proxy and ln != primary_loss:98 continue99 Hm = fc[h].values100 Y = np.repeat(y.values[:, None], Hm.shape[1], axis=1)101 with np.errstate(all="ignore"):102 Lm = LOSSES[ln](np.where(np.isfinite(Y), Y, np.nan), np.where(np.isfinite(Hm), Hm, np.nan))103 Lm = np.where(np.isfinite(Y) & np.isfinite(Hm), Lm, np.nan)104 loss[(proxy, ln, h)] = pd.DataFrame(Lm, index=fc[h].index, columns=fc[h].columns)105 return {"forecast": fc, "status": status, "target": targets, "loss": loss}106107108def pack_wide(res: dict, symbol: str) -> pd.DataFrame:109 """Long-but-compact table for storage: one row per (kind, proxy, loss, h, origin) with spec columns."""110 frames = []111 for h, df in res["forecast"].items():112 if isinstance(h, int):113 frames.append(df.assign(kind="forecast", proxy="", loss="", h=h))114 else:115 frames.append(df.assign(kind=h, proxy="", loss="", h=1))116 for (proxy, ln, h), df in res["loss"].items():117 frames.append(df.assign(kind="loss", proxy=proxy, loss=ln, h=h))118 tgt = pd.DataFrame({f"__target__{p}_{h}": s for (p, h), s in res["target"].items()})119 out = pd.concat(frames).reset_index().rename(columns={"index": "origin"})120 out["symbol"] = symbol121 return out, tgt.reset_index().rename(columns={"index": "origin"}), res["status"].reset_index()122