1#!/usr/bin/env python32"""Tier C rolling covariance forecasts (h = 1) on the ETF baskets, refit every 5 days with daily re-filtering for the3GARCH-based models (parameters fixed between refits; one-step forecast recomputed with the window up to the origin),4window 1000, OOS 2012-01-03 -> 2026-10-30. Writes results/metrics/tier_c/<basket>_<model>.parquet with per-origin losses5(Frobenius, multivariate QLIKE, equally weighted portfolio-variance QLIKE), min eigenvalue, fit status and timings."""6from __future__ import annotations78import argparse9import sys10from pathlib import Path1112import numpy as np13import pandas as pd14import yaml1516ROOT = Path(__file__).resolve().parents[1]17sys.path.insert(0, str(ROOT / "src"))18from wp46.findata_client.ingest import cache_root # noqa: E40219from wp46.models.multivariate import CovModel, frobenius_loss, mv_qlike, portfolio_variance_loss # noqa: E4022021PROT = yaml.safe_load(open(ROOT / "configs" / "forecast_protocol.yaml"))22STUDY = yaml.safe_load(open(ROOT / "configs" / "study.yaml"))232425def run(basket: str, model_id: str, window: int, refit: int, start: str, end: str) -> pd.DataFrame:26 z = np.load(cache_root() / "rcov" / f"{basket}.npz", allow_pickle=True)27 dates = pd.to_datetime(z["dates"]); R = z["R"]; RC = z["RC"]; valid = z["valid"]28 N = R.shape[1]; w = np.ones(N) / N29 origins = np.where((dates >= pd.Timestamp(start)) & (dates <= pd.Timestamp(end)))[0]30 origins = origins[origins >= window]31 m = CovModel(model_id)32 rows = []; last = None; n_since = refit33 for t in origins:34 Rw = R[t + 1 - window:t + 1]35 if not np.all(np.isfinite(Rw)):36 rows.append(dict(origin=dates[t], status="missing_input")); continue37 if n_since >= refit or last is None:38 try:39 last = m.fit_forecast(Rw); status = "ok" if last["converged"] else "failed"40 except Exception as e: # noqa: BLE00141 last = None; status = f"failed:{type(e).__name__}"42 n_since = 0; fit_time = last["time_s"] if last else np.nan43 else:44 # re-filter with the fixed parameters: for the GARCH-based models this is approximated by a refit on the45 # extended window only every `refit` days (documented); between refits the previous forecast is advanced46 # by recomputing with the current window using the same code path (cheap for rolling/EWMA)47 if model_id in ("rolling_cov252", "ewma_cov094"):48 last = m.fit_forecast(Rw)49 status = "ok" if last and last["converged"] else "stale_fit"50 fit_time = 0.051 n_since += 152 if last is None or t + 1 >= len(dates) or not valid[t + 1]:53 rows.append(dict(origin=dates[t], status=status if last else "no_fit", target_valid=bool(t + 1 < len(dates) and valid[t + 1]))); continue54 H = last["H1"]; Y = RC[t + 1]55 rows.append(dict(origin=dates[t], status=status, target_valid=True, frobenius=frobenius_loss(Y, H), mv_qlike=mv_qlike(Y, H),56 pf_qlike=portfolio_variance_loss(Y, H, w), min_eig=last["min_eig"], fit_time_s=fit_time,57 **{f"p_{k}": v for k, v in last["params"].items() if isinstance(v, float)}))58 df = pd.DataFrame(rows); df["basket"] = basket; df["model_id"] = model_id59 return df606162def main() -> None:63 ap = argparse.ArgumentParser()64 ap.add_argument("--baskets", default=",".join(STUDY["tier_c_baskets"]))65 ap.add_argument("--models", default=",".join(STUDY["tier_c_models"]))66 a = ap.parse_args()67 out = ROOT / "results" / "metrics" / "tier_c"; out.mkdir(parents=True, exist_ok=True)68 for b in a.baskets.split(","):69 for mid in a.models.split(","):70 df = run(b, mid, PROT["estimation"]["window"], PROT["estimation"]["refit_every"], PROT["periods"]["validation"]["start"], PROT["periods"]["test"]["end"])71 df.to_parquet(out / f"{b}_{mid}.parquet", index=False)72 ok = df[df.status.isin(["ok", "stale_fit"]) & df.get("target_valid", False)]73 print(b, mid, "origins", len(df), df.status.value_counts().to_dict(), "mean mv_qlike", round(ok["mv_qlike"].mean(), 4) if len(ok) else None, flush=True)747576if __name__ == "__main__":77 main()78