Repository Public Research preview
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