SPB Git

Repository Public Research preview

tables.py 10.7 KB 169 lines python

1"""LaTeX table fragments from the analysis outputs (booktabs; numbers formatted here, never by hand)."""2from __future__ import annotations34from pathlib import Path56import numpy as np7import pandas as pd8910def fmt(x, nd=3):11    return "--" if x is None or (isinstance(x, float) and not np.isfinite(x)) else f"{x:.{nd}f}"121314def stars(p):15    return "" if not np.isfinite(p) else ("***" if p < 0.01 else "**" if p < 0.05 else "*" if p < 0.10 else "")161718def pooled_table(P: pd.DataFrame, specs: list[str], labels: dict, path: Path, caption_note: str = "") -> None:19    rows = []20    for sp in specs:21        cells = [labels.get(sp, sp)]22        for h in (1, 5, 22):23            r = P[(P.spec_id == sp) & (P.h == h) & (P.asset_class == "all")]24            if len(r):25                r = r.iloc[0]26                cells.append(f"{fmt(r.mean_ratio)}{stars(r.boot_p)}")27                cells.append(f"[{fmt(1+r.boot_ci_low, 2)}, {fmt(1+r.boot_ci_high, 2)}]")28                cells.append(f"{int(r.dm_wins_holm10)}/{int(r.dm_losses_holm10)}/{int(r.n_instruments)}")29            else:30                cells += ["--", "--", "--"]31        rows.append(" & ".join(cells) + r" \\")32    head = (r"\begin{tabular}{l" + "ccc" * 3 + "}\n\\toprule\n & \\multicolumn{3}{c}{$h=1$} & \\multicolumn{3}{c}{$h=5$} & \\multicolumn{3}{c}{$h=22$} \\\\\n"33            r"\cmidrule(lr){2-4}\cmidrule(lr){5-7}\cmidrule(lr){8-10}" + "\nModel & ratio & 90\\% CI & W/L/N & ratio & 90\\% CI & W/L/N & ratio & 90\\% CI & W/L/N \\\\\n\\midrule\n")34    path.write_text(head + "\n".join(rows) + "\n\\bottomrule\n\\end{tabular}\n")353637def accounting_table(A: pd.DataFrame, labels: dict, path: Path) -> None:38    rows = []39    for r in A.sort_values(["family", "spec_id"]).itertuples():40        rows.append(f"{labels.get(r.spec_id, r.spec_id)} & {fmt(100*r.failed,1)} & {fmt(100*r.inadmissible,1)} & {fmt(100*r.coverage,1)} & {fmt(r.fit_time_median,3)} & {fmt(r.iter_mean,0)} \\\\")41    head = "\\begin{tabular}{lrrrrr}\n\\toprule\nModel & Failed (\\%) & Inadmissible (\\%) & Coverage (\\%) & Median refit (s) & Iterations \\\\\n\\midrule\n"42    path.write_text(head + "\n".join(rows) + "\n\\bottomrule\n\\end{tabular}\n")434445def crosscheck_table(cc: dict, path: Path) -> None:46    """results/metrics/rugarch_crosscheck.json -> LaTeX rows (model, R package/model, R loglik, ours, difference, note)."""47    R, O = cc.get("rugarch", {}), cc.get("ours", {})48    rows = []49    spec = [("garch11_t", "GARCH(1,1)-$t$", "rugarch sGARCH"), ("garch11_skewt", "GARCH(1,1)-skew-$t$", "rugarch sGARCH/sstd"),50            ("gjr11_t", "GJR(1,1)-$t$", "rugarch gjrGARCH"), ("tgarch11_t", "TGARCH(1,1)-$t$", "rugarch fGARCH/TGARCH"),51            ("egarch11_t", "EGARCH(1,1)-$t$", "rugarch eGARCH"), ("aparch11_t", "APARCH(1,1)-$t$", "rugarch apARCH"),52            ("figarch11_t", "FIGARCH(1,$d$,1)-$t$", "rugarch fiGARCH"), ("igarch11_t", "IGARCH(1,1)-$t$", "rugarch iGARCH"),53            ("cgarch11_t", "Component GARCH-$t$", "rugarch csGARCH"), ("nagarch11_t", "NAGARCH(1,1)-$t$", "rugarch fGARCH/NAGARCH"),54            ("realgarch11_n", "Realized GARCH(1,1)-N", "rugarch realGARCH"), ("garchx_return_eq_n", "GARCH-X(1,1)-N (return eq.)", "rugarch sGARCH + vxreg"),55            ("msgarch2_n", "MS-GARCH(2)-N", "MSGARCH FitML"), ("gas_t", "GAS-$t$(1,1)", "GAS (scale parameterization; reference)")]56    for key, lab, pkg in spec:57        r = R.get(key, {}); o = O.get(key, {})58        rl = r.get("loglik") if r.get("converged") else None59        rl = float(np.ravel(rl)[0]) if rl is not None and np.size(rl) else None60        ol = o.get("loglik_return_eq", o.get("loglik"))61        note = ""62        if key == "realgarch11_n" and rl is None:63            note = "R fit did not converge"64        if key == "garchx_return_eq_n":65            note = "per-observation comparison (R drops one obs.)"66        if key == "msgarch2_n" and r.get("converged") and o.get("var_forecast"):67            vf = np.asarray(r["vol_forecast"]) ** 2; rel = np.max(np.abs(np.asarray(o["var_forecast"]) - vf) / vf)68            note = f"22-step variance forecasts within {100*rel:.1f}\\%"69        if key == "gas_t":70            note = "different parameterization, not comparable"71        diff = fmt(ol - rl) if (ol is not None and rl is not None and key not in ("garchx_return_eq_n", "gas_t")) else "--"72        rows.append(f"{lab} & {pkg} & {fmt(rl, 2) if rl is not None else '--'} & {fmt(ol, 2) if ol is not None else '--'} & {diff} & {note} \\\\")73    head = "\\begin{tabular}{llrrrl}\n\\toprule\nModel & R reference & R log-lik. & Ours & Diff. & Note \\\\\n\\midrule\n"74    path.write_text(head + "\n".join(rows) + "\n\\bottomrule\n\\end{tabular}\n")757677def tier_c_table(T: pd.DataFrame, extra: dict, path: Path) -> None:78    """Tier C summary: per basket and model, mean multivariate QLIKE, ratio to CCC, DM p, Frobenius ratio, portfolio QLIKE ratio, MCS p."""79    labels = {"rolling_cov252": "Rolling covariance (252)", "ewma_cov094": "EWMA covariance (0.94)", "ccc_garch11": "CCC-GARCH(1,1)",80              "dcc11": "DCC(1,1)", "adcc11": "Asymmetric DCC(1,1)", "scalar_bekk11": "Scalar BEKK(1,1)"}81    rows = []82    for b in T.basket.unique():83        rows.append(f"\\multicolumn{{7}}{{l}}{{\\emph{{Basket: {', '.join(extra[b]['basket'])}}}}} \\\\")84        for m in labels:85            q = T[(T.basket == b) & (T.model == m) & (T.loss == "mv_qlike")]86            fr = T[(T.basket == b) & (T.model == m) & (T.loss == "frobenius")]87            pf = T[(T.basket == b) & (T.model == m) & (T.loss == "pf_qlike")]88            if not len(q):89                continue90            q = q.iloc[0]91            mp = extra.get(f"{b}|mv_qlike|mcs", {}).get(m, {})92            rows.append(f"{labels[m]} & {fmt(q['mean'])} & {fmt(q['ratio'])}{stars(q['dm_p'])} & {fmt(fr.iloc[0]['ratio']) if len(fr) else '--'} & "93                        f"{fmt(pf.iloc[0]['ratio']) if len(pf) else '--'} & {fmt(mp.get('p', np.nan), 2)} & {fmt(extra[b]['fit_time_median'].get(m, np.nan), 2)} \\\\")94    head = ("\\begin{tabular}{lrrrrrr}\n\\toprule\nModel & mv-QLIKE & ratio to CCC & Frobenius ratio & portfolio-QLIKE ratio & MCS $p$ & refit (s) \\\\\n\\midrule\n")95    path.write_text(head + "\n".join(rows) + "\n\\bottomrule\n\\end{tabular}\n")969798def tailrisk_table(TR: pd.DataFrame, specs: list[str], labels: dict, path: Path) -> None:99    """Per specification (across instruments): median 1 % and 5 % hit rates, share of instruments rejected by Kupiec / CC / DQ at 5 %,100    mean FZ0 relative to GARCH(1,1)-t (per instrument, then averaged)."""101    rows = []102    ref = "garch11__t__w1000_r5"103    for sp in specs:104        cells = [labels.get(sp, sp)]105        for lvl in (0.01, 0.05):106            d = TR[(TR.spec_id == sp) & (TR.level == lvl)]107            if not len(d):108                cells += ["--"] * 5109                continue110            b = TR[(TR.spec_id == ref) & (TR.level == lvl)].set_index("symbol")["fz0_mean"]111            rel = (d.set_index("symbol")["fz0_mean"] / b.reindex(d.symbol).values)112            cells += [fmt(100 * d.hit_rate.median(), 2), fmt(100 * (d.kupiec_p < 0.05).mean(), 0), fmt(100 * (d.cc_p < 0.05).mean(), 0),113                      fmt(100 * (d.dq_p < 0.05).mean(), 0), fmt(float(np.nanmean(rel)), 3)]114        rows.append(" & ".join(cells) + r" \\")115    head = ("\\begin{tabular}{l" + "rrrrr" * 2 + "}\n\\toprule\n & \\multicolumn{5}{c}{1\\% VaR / ES} & \\multicolumn{5}{c}{5\\% VaR / ES} \\\\\n"116            "\\cmidrule(lr){2-6}\\cmidrule(lr){7-11}\nModel & hit \\% & UC rej. \\% & CC rej. \\% & DQ rej. \\% & FZ0 ratio & hit \\% & UC rej. \\% & CC rej. \\% & DQ rej. \\% & FZ0 ratio \\\\\n\\midrule\n")117    path.write_text(head + "\n".join(rows) + "\n\\bottomrule\n\\end{tabular}\n")118119120def regime_table(per_symbol: dict, specs: list[str], labels: dict, regime_keys: list[str], path: Path, h: int = 1) -> None:121    """Pooled (equal-weight) QLIKE ratios to GARCH(1,1)-N by regime for a list of specifications (test period, pairwise samples)."""122    rows = []123    for sp in specs:124        cells = [labels.get(sp, sp)]125        for rk in regime_keys:126            vals = []127            for sym, r in per_symbol.items():128                d = r["regimes"].get(str(h), r["regimes"].get(h, {})).get(rk, {}).get(sp)129                if d and np.isfinite(d.get("ratio", np.nan)):130                    vals.append(d["ratio"])131            cells.append(fmt(float(np.mean(vals))) + f" ({len(vals)})" if vals else "--")132        rows.append(" & ".join(cells) + r" \\")133    head = "\\begin{tabular}{l" + "c" * len(regime_keys) + "}\n\\toprule\nModel & " + " & ".join(k.replace("_", "\\_").replace("=", ": ") for k in regime_keys) + " \\\\\n\\midrule\n"134    path.write_text(head + "\n".join(rows) + "\n\\bottomrule\n\\end{tabular}\n")135136137def robust_table(P: pd.DataFrame, specs: list[str], labels: dict, path: Path) -> None:138    """1 %-trimmed and median pooled ratios and explosive-forecast shares at h = 1 and h = 22."""139    rows = []140    for sp in specs:141        cells = [labels.get(sp, sp)]142        for h in (1, 22):143            r = P[(P.spec_id == sp) & (P.h == h) & (P.asset_class == "all")]144            if len(r):145                r = r.iloc[0]146                cells += [fmt(r.mean_ratio), fmt(r.get("mean_ratio_trim1", np.nan)), fmt(r.median_ratio), fmt(100 * r.get("explosive_share", np.nan), 2)]147            else:148                cells += ["--"] * 4149        rows.append(" & ".join(cells) + r" \\")150    head = ("\\begin{tabular}{l" + "rrrr" * 2 + "}\n\\toprule\n & \\multicolumn{4}{c}{$h=1$} & \\multicolumn{4}{c}{$h=22$} \\\\\n\\cmidrule(lr){2-5}\\cmidrule(lr){6-9}\n"151            "Model & mean & trimmed 1\\% & median & explosive \\% & mean & trimmed 1\\% & median & explosive \\% \\\\\n\\midrule\n")152    path.write_text(head + "\n".join(rows) + "\n\\bottomrule\n\\end{tabular}\n")153154155def secondary_table(per_symbol: dict, specs: list[str], labels: dict, keys: list[str], path: Path) -> None:156    """Pooled (equal-weight) pairwise ratios under secondary proxies/losses; keys like 'rv5_on|qlike|1' or 'rv5ss_on|mse|1'."""157    rows = []158    for sp in specs:159        cells = [labels.get(sp, sp)]160        for k in keys:161            vals = [r["secondary"].get(k, {}).get(sp) for r in per_symbol.values()]162            vals = [v for v in vals if v is not None and np.isfinite(v)]163            cells.append(fmt(float(np.mean(vals))) if vals else "--")164        rows.append(" & ".join(cells) + r" \\")165    heads = [k.replace("|1", "").replace("|", " / ").replace("_on", "+on").replace("_scaled", " scaled").replace("rv5ss", "RV5ss").replace("rv5", "RV5").replace("rv1", "RV1")166             .replace("bv5", "BV5").replace("medrv", "medRV").replace("r2", "$r^2$").replace("mse_sd", "MSE-sd").replace("_", "\\_") for k in keys]167    head = "\\begin{tabular}{l" + "c" * len(keys) + "}\n\\toprule\nModel & " + " & ".join(heads) + " \\\\\n\\midrule\n"168    path.write_text(head + "\n".join(rows) + "\n\\bottomrule\n\\end{tabular}\n")169
Files main
Menu

Theme