SPB Git

spb/wp46_uqo

Public
12.4 KB · 184 lines python
1#!/usr/bin/env python32"""Build every machine-generated exhibit of the paper from results/: numbers.tex (macros), tables/*.tex, figures/*.pdf.3usage: build_paper.py --run main --period test   (then: cd paper && make)4Never edits prose. Every macro name is digit-free (LaTeX)."""5from __future__ import annotations67import argparse8import glob9import json10import re11import sys12from pathlib import Path1314import numpy as np15import pandas as pd16import yaml1718ROOT = Path(__file__).resolve().parents[1]19sys.path.insert(0, str(ROOT / "src"))20from wp46.reporting import figures as F  # noqa: E40221from wp46.reporting import tables as TB  # noqa: E4022223REG = yaml.safe_load(open(ROOT / "configs" / "model_registry.yaml"))["models"]24PROT = yaml.safe_load(open(ROOT / "configs" / "forecast_protocol.yaml"))25WORDS = {0: "Zero", 1: "One", 2: "Two", 3: "Three", 4: "Four", 5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine"}262728def macro_name(s: str) -> str:29    s = re.sub(r"[^A-Za-z0-9]", " ", s).title().replace(" ", "")30    return "".join(WORDS[int(c)] if c.isdigit() else c for c in s)313233def fnum(x, nd=3):34    if x is None or (isinstance(x, float) and not np.isfinite(x)):35        return "--"36    if isinstance(x, (int, np.integer)):37        return f"{x:,}"38    return f"{x:,.{nd}f}"394041def main() -> None:42    ap = argparse.ArgumentParser()43    ap.add_argument("--run", default="main"); ap.add_argument("--period", default="test")44    a = ap.parse_args()45    adir = ROOT / "results" / "metrics" / f"analysis_{a.run}_{a.period}"46    per_symbol = {Path(f).stem: json.load(open(f)) for f in sorted(glob.glob(str(adir / "per_symbol" / "*.json")))}47    P = pd.read_csv(adir / "pooled.csv")48    specs_all = sorted({s for r in per_symbol.values() for s in r["horizons"]["1"]["specs"]})49    labels = {s: F.spec_label(s) for s in specs_all}50    macros = {}51    # --- headline numbers52    macros["NInstruments"] = len(per_symbol)53    import subprocess54    try:55        col = subprocess.run([str(ROOT / ".venv" / "bin" / "python"), "-m", "pytest", "--collect-only", "-q", "tests"], capture_output=True, text=True, cwd=ROOT, timeout=300).stdout56        macros["NTests"] = sum(1 for l in col.splitlines() if "::" in l)57    except Exception:  # noqa: BLE00158        macros["NTests"] = "--"59    macros["NSpecs"] = len(specs_all)60    macros["NModels"] = len({s.split("__")[0] for s in specs_all})61    macros["NOriginsTotal"] = sum(r["horizons"]["1"]["n_origins"] for r in per_symbol.values())62    macros["NValidTargetsHOne"] = sum(r["horizons"]["1"]["n_valid_targets"] for r in per_symbol.values())63    Pa = P[P.asset_class == "all"]64    for r in Pa.itertuples():65        base = macro_name(f"{r.spec_id.split('__')[0]} {r.spec_id.split('__')[1]} h{r.h}")66        macros[f"Ratio{base}"] = fnum(r.mean_ratio)67        macros[f"CiLo{base}"] = fnum(1 + r.boot_ci_low, 3); macros[f"CiHi{base}"] = fnum(1 + r.boot_ci_high, 3)68        macros[f"BootP{base}"] = fnum(r.boot_p, 3); macros[f"Wins{base}"] = int(r.dm_wins_holm10); macros[f"Losses{base}"] = int(r.dm_losses_holm10)69        macros[f"McsIn{base}"] = int(r.mcs_inclusions)70    # best spec per horizon (pooled, Tier A / Tier B)71    for h in (1, 5, 22):72        for tier in ("A", "B"):73            sub = Pa[(Pa.h == h) & (Pa.tier == tier)].sort_values("mean_ratio")74            if len(sub):75                macros[f"BestSpecTier{tier}H{WORDS[h] if h < 10 else 'TwentyTwo'}"] = labels[sub.iloc[0].spec_id]76                macros[f"BestRatioTier{tier}H{WORDS[h] if h < 10 else 'TwentyTwo'}"] = fnum(sub.iloc[0].mean_ratio)77    # --- accounting table78    acc_rows = []79    for s in specs_all:80        vals = [r["accounting"].get(s, {}) for r in per_symbol.values()]81        acc_rows.append(dict(spec_id=s, family=F.fam(s), failed=np.nanmean([v.get("failed", np.nan) for v in vals]), inadmissible=np.nanmean([v.get("inadmissible", np.nan) for v in vals]),82                             coverage=np.nanmean([r["horizons"]["1"]["specs"].get(s, {}).get("coverage", np.nan) for r in per_symbol.values()]),83                             fit_time_median=np.nanmedian([v.get("fit_time_median", np.nan) for v in vals]), fit_time_mean=np.nanmean([v.get("fit_time_mean", np.nan) for v in vals]),84                             iter_mean=np.nanmean([v.get("iter_mean", np.nan) for v in vals])))85    A = pd.DataFrame(acc_rows); A["failed_or_inadmissible"] = A["failed"].fillna(0) + A["inadmissible"].fillna(0)86    A.to_csv(ROOT / "results" / "tables" / f"accounting_{a.run}_{a.period}.csv", index=False)87    # --- tail risk table88    tr = []89    for sym, r in per_symbol.items():90        for s, v in r.get("tailrisk", {}).items():91            for lvl, t in v.items():92                if isinstance(t, dict) and "hit_rate" in t:93                    tr.append(dict(symbol=sym, spec_id=s, family=F.fam(s), level=float(lvl), **t))94    TR = pd.DataFrame(tr); TR.to_csv(ROOT / "results" / "tables" / f"tailrisk_{a.run}_{a.period}.csv", index=False)95    # representative specifications (Student-t where available) and family order, used by tables and figures96    rep0 = {}97    for sp in specs_all:98        mid, dist, _ = sp.split("__"); pref = {"t": 0, "normal": 1, "empirical": 2, "ged": 3, "skewt": 4}[dist]99        if mid not in rep0 or pref < rep0[mid][0]:100            rep0[mid] = (pref, sp)101    fam_order = ["baseline", "classical", "asymmetric", "power", "fractional", "component", "regime-switching", "score-driven", "realized"]102    # --- LaTeX tables103    tdir = ROOT / "paper" / "tables"; tdir.mkdir(exist_ok=True)104    tierA = [s for s in specs_all if F.fam(s) != "realized" and (s.split("__")[1] in ("normal", "t", "empirical"))]105    tierB = [s for s in specs_all if F.fam(s) == "realized"]106    TB.pooled_table(P, tierA, labels, tdir / "tab_pooled_tierA.tex")107    repA_main = sorted([v[1] for v in rep0.values() if F.fam(v[1]) != "realized" and v[1].split("__")[0] not in F.EXT_PARENT], key=lambda s: (fam_order.index(F.fam(s)), s))108    TB.pooled_table(P, repA_main, labels, tdir / "tab_pooled_tierA_rep.tex")109    ext_specs = sorted([sp for sp in specs_all if sp.split("__")[0] in F.EXT_PARENT])110    if ext_specs:111        TB.pooled_table(P, ext_specs, labels, tdir / "tab_pooled_extensions.tex")112    TB.pooled_table(P, tierB, labels, tdir / "tab_pooled_tierB.tex")113    TB.accounting_table(A, labels, tdir / "tab_accounting.tex")114    tc = ROOT / "results" / "metrics" / "analysis_tier_c_test"115    if (tc / "tier_c_summary.csv").exists():116        TC = pd.read_csv(tc / "tier_c_summary.csv"); TCX = json.load(open(tc / "tier_c_extra.json"))117        TB.tier_c_table(TC, TCX, tdir / "tab_tier_c.tex")118        for b in TC.basket.unique():119            for m in ("dcc11", "adcc11", "scalar_bekk11", "ewma_cov094", "rolling_cov252"):120                q = TC[(TC.basket == b) & (TC.model == m) & (TC.loss == "mv_qlike")]121                if len(q):122                    macros[f"TierC{macro_name(b)}{macro_name(m)}Ratio"] = fnum(float(q.iloc[0].ratio)); macros[f"TierC{macro_name(b)}{macro_name(m)}DmP"] = fnum(float(q.iloc[0].dm_p))123    cc = ROOT / "results" / "metrics" / "rugarch_crosscheck.json"124    if cc.exists():125        TB.crosscheck_table(json.load(open(cc)), tdir / "tab_crosscheck.tex")126    # --- figures127    # representative specifications (Student-t where available) ordered by family: readable heatmaps (mandate §5-6)128    rep0 = {}129    for sp in specs_all:130        mid, dist, _ = sp.split("__"); pref = {"t": 0, "normal": 1, "empirical": 2, "ged": 3, "skewt": 4}[dist]131        if mid not in rep0 or pref < rep0[mid][0]:132            rep0[mid] = (pref, sp)133    fam_order = ["baseline", "classical", "asymmetric", "power", "fractional", "component", "regime-switching", "score-driven", "realized"]134    repA = sorted([v[1] for v in rep0.values() if F.fam(v[1]) != "realized" and v[1].split("__")[0] not in F.EXT_PARENT], key=lambda s: (fam_order.index(F.fam(s)), s))135    repB = sorted([v[1] for v in rep0.values() if F.fam(v[1]) == "realized" and v[1].split("__")[0] not in F.EXT_PARENT], key=lambda s: s)136    F.ratio_heatmap(per_symbol, 1, repA, F.FIG / "fig_heatmap_h1_tierA.pdf", "Tier A (representative specifications), h = 1")137    F.ratio_heatmap(per_symbol, 22, repA, F.FIG / "fig_heatmap_h22_tierA.pdf", "Tier A (representative specifications), h = 22")138    F.ratio_heatmap(per_symbol, 1, repB + [s for s in specs_all if s.startswith("garch11__normal")], F.FIG / "fig_heatmap_h1_tierB.pdf", "Tier B (+ GARCH(1,1)-N reference), h = 1")139    F.ratio_heatmap(per_symbol, 1, tierA, F.FIG / "fig_heatmap_h1_tierA_full.pdf", "Tier A, all innovation laws, h = 1", vmin=0.7, vmax=1.3)140    F.mcs_matrix(per_symbol, 1, "A", F.FIG / "fig_mcs_h1_tierA.pdf", "90% MCS, Tier A, all specifications (pre-registered), h = 1")141    F.mcs_matrix(per_symbol, 22, "A", F.FIG / "fig_mcs_h22_tierA.pdf", "90% MCS, Tier A, all specifications (pre-registered), h = 22")142    F.mcs_matrix(per_symbol, 1, "B", F.FIG / "fig_mcs_h1_tierB.pdf", "90% MCS, Tier B (+ GARCH(1,1)-N), h = 1")143    F.mcs_matrix(per_symbol, 1, "A", F.FIG / "fig_mcsrep_h1_tierA.pdf", "90% MCS, Tier A, one specification per architecture (exploratory), h = 1", key="mcs_rep")144    F.mcs_matrix(per_symbol, 22, "A", F.FIG / "fig_mcsrep_h22_tierA.pdf", "90% MCS, Tier A, one specification per architecture (exploratory), h = 22", key="mcs_rep")145    F.mcs_matrix(per_symbol, 1, "B", F.FIG / "fig_mcsrep_h1_tierB.pdf", "90% MCS, Tier B, one specification per architecture (exploratory), h = 1", key="mcs_rep")146    topA = Pa[(Pa.h == 1) & (Pa.tier == "A") & (~Pa.spec_id.str.split("__").str[0].isin(list(F.EXT_PARENT)))].sort_values("mean_ratio").spec_id.head(3).tolist()147    topB = Pa[(Pa.h == 1) & (Pa.tier == "B") & (~Pa.spec_id.str.split("__").str[0].isin(list(F.EXT_PARENT)))].sort_values("mean_ratio").spec_id.head(3).tolist()148    F.horizon_profile(P, topA + topB + [s for s in specs_all if s.startswith("garch11__t__")], F.FIG / "fig_horizon.pdf")149    F.pareto(Pa[Pa.h == 1], A, F.FIG / "fig_pareto_time.pdf")150    F.pareto(Pa[Pa.h == 1], A, F.FIG / "fig_pareto_fail.pdf", xcol="failed_or_inadmissible", xlabel="Share of refits failed or inadmissible")151    F.failure_bars(A, F.FIG / "fig_failures.pdf")152    if len(TR):153        F.var_calibration(TR, F.FIG / "fig_var_1pct.pdf", "0.01"); F.var_calibration(TR, F.FIG / "fig_var_5pct.pdf", "0.05")154    F.timeline(); F.coverage_map(); F.volatility_series(["SPY", "CL", "EURUSD", "BTC"], Path.home() / "wp46-cache" / "panels", F.FIG / "fig_volseries.pdf")155    F.lineage()156    # representative specs (Student-t where available) for heterogeneity exhibits157    rep = {}158    for sp in specs_all:159        mid, dist, _ = sp.split("__"); pref = {"t": 0, "normal": 1, "empirical": 2, "ged": 3, "skewt": 4}[dist]160        if mid not in rep or pref < rep[mid][0]:161            rep[mid] = (pref, sp)162    rep_specs = sorted([v[1] for v in rep.values()], key=lambda s: (F.fam(s), s))163    F.class_heatmap(P, rep_specs, 1, F.FIG / "fig_class_h1.pdf", "h = 1"); F.class_heatmap(P, rep_specs, 22, F.FIG / "fig_class_h22.pdf", "h = 22")164    reg_specs = [s for s in rep_specs if s.split("__")[0] in ("gjr11", "egarch11", "figarch11", "cgarch11", "msgarch2", "har_rv", "realgarch11", "heavy11")]165    F.regime_profile(per_symbol, reg_specs, F.FIG / "fig_regimes_h1.pdf", 1)166    TB.regime_table(per_symbol, rep_specs, labels, ["vol_tercile=low", "vol_tercile=mid", "vol_tercile=high", "prev_sign=neg", "prev_sign=pos", "episode=covid", "episode=rate_shock_2022", "episode=tariff_shock_2025"], tdir / "tab_regimes_h1.tex", 1)167    TB.regime_table(per_symbol, rep_specs, labels, ["vol_tercile=low", "vol_tercile=mid", "vol_tercile=high", "prev_sign=neg", "prev_sign=pos", "episode=covid", "episode=rate_shock_2022", "episode=tariff_shock_2025"], tdir / "tab_regimes_h22.tex", 22)168    if len(TR):169        TB.tailrisk_table(TR, [s for s in rep_specs] + [s for s in specs_all if s.startswith("garch11__") and s not in rep_specs], labels, tdir / "tab_tailrisk.tex")170    if any(sp.split("__")[0] in F.EXT_PARENT for sp in specs_all):171        F.extension_heatmap(per_symbol, 1, F.FIG / "fig_extensions_h1.pdf"); F.extension_heatmap(per_symbol, 22, F.FIG / "fig_extensions_h22.pdf")172    # pooled by asset class for the representative specs173    P[P.spec_id.isin(rep_specs)].to_csv(ROOT / "results" / "tables" / f"pooled_by_class_{a.run}_{a.period}.csv", index=False)174    # --- numbers.tex175    lines = ["% generated by scripts/build_paper.py — do not edit"]176    for k, v in macros.items():177        lines.append(f"\\newcommand{{\\{k}}}{{{v}}}")178    (ROOT / "paper" / "numbers.tex").write_text("\n".join(lines) + "\n")179    print("macros", len(macros), "tables", len(list(tdir.glob("*.tex"))), "figures", len(list(F.FIG.glob("*.pdf"))))180181182if __name__ == "__main__":183    main()184
Files · main
Menu

Theme