1"""Paper figures. Runs on the laptop from results/metrics and results/figdata; writes paper/figures/*.pdf (vector).23Standard (figure mandate 2026-11-09): Okabe–Ito colour-blind-safe palette + distinct line styles/markers so every figure survives4grayscale; uncertainty bands where available; units on axes; labelled thresholds that do not collide with data; panel letters;5self-contained small multiples. Audit: scripts/10_figures.py --audit renders every PDF in colour and grayscale to /tmp/wp47fig/.6"""7import json, sys, subprocess8from pathlib import Path9import numpy as np, pandas as pd10import matplotlib11matplotlib.use("Agg")12import matplotlib.pyplot as plt13import matplotlib.dates as mdates14from matplotlib.lines import Line2D15from matplotlib.patches import FancyBboxPatch, FancyArrowPatch, Rectangle16from matplotlib.colors import LinearSegmentedColormap, TwoSlopeNorm1718ROOT = Path(__file__).resolve().parents[1]; M = ROOT / "results/metrics"; FD = ROOT / "results/figdata"; OUT = ROOT / "paper/figures"; OUT.mkdir(exist_ok=True, parents=True)19plt.rcParams.update({"font.family": "serif", "font.serif": ["Times New Roman", "Times", "STIXGeneral", "DejaVu Serif"], "mathtext.fontset": "stix",20 "font.size": 9, "axes.labelsize": 9, "axes.titlesize": 9.5, "legend.fontsize": 7.5, "xtick.labelsize": 8, "ytick.labelsize": 8,21 "axes.grid": True, "grid.color": "#e3e3e3", "grid.linewidth": 0.5, "axes.edgecolor": "#444444", "axes.linewidth": 0.6,22 "lines.linewidth": 1.3, "legend.frameon": False, "axes.spines.top": False, "axes.spines.right": False, "pdf.fonttype": 42,23 "axes.unicode_minus": True})24# Okabe–Ito25OI = {"blue": "#0072B2", "verm": "#D55E00", "green": "#009E73", "pink": "#CC79A7", "orange": "#E69F00", "sky": "#56B4E9", "yellow": "#F0E442", "grey": "#999999", "black": "#000000"}26STY = {"dL": dict(color=OI["blue"], ls="-", marker="o"), "dS": dict(color=OI["verm"], ls="--", marker="s"), "dC": dict(color=OI["green"], ls=":", marker="^"),27 "dSR": dict(color=OI["pink"], ls="-.", marker="D"), "dy10": dict(color=OI["orange"], ls=(0, (5, 1, 1, 1)), marker="v"), "dy2": dict(color=OI["black"], ls=(0, (1, 1)), marker="x")}28for k in ("level", "slope", "curvature"): STY[k] = STY[{"level": "dL", "slope": "dS", "curvature": "dC"}[k]]29LAB = {"dL": "Level", "dS": "Slope", "dC": "Curvature", "dSR": "Short rate ($L+S$)", "dy10": "Implied 10-year yield", "dy2": "Implied 2-year yield",30 "level": "Level", "slope": "Slope", "curvature": "Curvature"}31def line(ax, x, y, key, label=None, markevery=None, lw=None, **kw):32 s = dict(STY[key]); s.pop("marker"); s.update(kw)33 if lw: s["lw"] = lw34 return ax.plot(x, y, label=label if label is not None else LAB[key], **s)35def mline(ax, x, y, key, label=None, ms=3.5, **kw):36 s = dict(STY[key]); s.update(kw); s["ms"] = ms; s["markerfacecolor"] = "white"; s["markeredgewidth"] = 1.037 return ax.plot(x, y, label=label if label is not None else LAB[key], **s)38def panel(ax, letter, title):39 ax.set_title(f"{letter}. {title}", loc="left", fontweight="bold")40def ma(x, w): return pd.Series(x).rolling(w, center=True, min_periods=1).mean().values41def save(fig, name):42 fig.savefig(OUT / name, bbox_inches="tight", pad_inches=0.02); plt.close(fig)4344REGIME_EDGES = [("2015-12-16", "ZLB | normalisation"), ("2020-01-01", "normalisation | pandemic"), ("2022-01-01", "pandemic | 2022–2026 cycle")]4546# ================================================================================================47# 1. fidelity by year (with bootstrap 90% bands)48fy = pd.read_csv(FD / "fidelity_by_year.csv")49fig, axes = plt.subplots(1, 3, figsize=(6.5, 2.5), sharey=True)50for ax, f, let, thr in zip(axes, ["level", "slope", "curvature"], "ABC", [0.9, 0.9, 0.6]):51 for tgt, lab, key, mk in (("cmt", "CMT target (11 maturities, primary)", "dL", "o"), ("gsw", "GSW zero-curve target (1–30 years)", "dC", "s")):52 s = fy[(fy.target == tgt) & (fy.factor == f)].sort_values("year")53 col = OI["blue"] if tgt == "cmt" else OI["green"]54 if "lo" in s: ax.fill_between(s.year, s.lo, s.hi, color=col, alpha=0.15, lw=0)55 ax.plot(s.year, s.r2_oos, color=col, ls="-" if tgt == "cmt" else "--", marker=mk, ms=3.2, markerfacecolor="white", markeredgewidth=1.0, label=lab)56 ax.axhline(thr, color="#333333", lw=0.7, ls=(0, (4, 2)))57 ax.text(2027.0, thr, f"H1\n{thr:.2f}", fontsize=6.0, color="#333333", ha="left", va="center", clip_on=False)58 panel(ax, let, LAB[f]); ax.set_ylim(0, 1.0); ax.set_xlim(2010.5, 2026.5); ax.set_xlabel("Year"); ax.set_xticks([2011, 2015, 2019, 2023, 2026])59 ax.margins(x=0)60axes[0].set_ylabel("Out-of-sample $R^2$ (daily)")61h, l = axes[0].get_legend_handles_labels()62h.append(Rectangle((0, 0), 1, 1, color=OI["grey"], alpha=0.3, lw=0)); l.append("90% bootstrap band (days resampled within year)")63fig.legend(h, l, loc="lower center", ncol=3, bbox_to_anchor=(0.5, -0.04), fontsize=7)64fig.tight_layout(rect=(0, 0.07, 0.985, 1), w_pad=1.6); save(fig, "fig_fidelity.pdf")6566# ================================================================================================67# 2. ETF cross-validation scatter68pp = pd.read_csv(FD / "etf_fomc_pairs.csv")69fig, axes = plt.subplots(1, 3, figsize=(6.5, 2.6))70for ax, c, let in zip(axes, ["dL", "dS", "dC"], "ABC"):71 xe, yf = pp[f"{c}_etf"].values, pp[f"{c}_fut"].values; r = np.corrcoef(xe, yf)[0, 1]; b = np.polyfit(xe, yf, 1)72 lim = np.nanmax(np.abs(np.r_[xe, yf])) * 1.173 ax.plot([-lim, lim], [-lim, lim], color=OI["grey"], lw=0.7, ls="--", label="45° line")74 xx = np.linspace(-lim, lim, 10); ax.plot(xx, b[0] * xx + b[1], color=OI["verm"], lw=1.0, label=f"OLS fit, slope {b[0]:.2f}")75 ax.scatter(xe, yf, s=11, color=OI["blue"], alpha=0.8, edgecolor="white", linewidth=0.3, zorder=3, label="FOMC windows")76 ax.set_xlim(-lim, lim); ax.set_ylim(-lim, lim); ax.set_aspect("equal"); panel(ax, let, f"{LAB[c]}: $\\rho$ = {r:.2f}, $n$ = {len(pp)}")77 ax.set_xlabel("ETF-based change (bp)"); ax.legend(loc="upper left", fontsize=6.2, handlelength=1.6)78axes[0].set_ylabel("Futures-based change (bp)")79fig.tight_layout(); save(fig, "fig_etf_crossval.pdf")8081# ================================================================================================82# 3. sample days83sd = pd.read_csv(FD / "sample_days.csv"); sd["ts"] = pd.to_datetime(sd["ts"], utc=True).dt.tz_convert("America/New_York")84rel = {"2013-06-19": ("14:00", "FOMC statement, 19 June 2013"), "2022-06-15": ("14:00", "FOMC statement, 15 June 2022"),85 "2022-11-10": ("08:30", "CPI release, 10 November 2022"), "2024-08-02": ("08:30", "Employment report, 2 August 2024")}86fig, axes = plt.subplots(2, 2, figsize=(6.5, 5.4))87for ax, (d, (t, title)), let in zip(axes.ravel(), rel.items(), "ABCD"):88 s = sd[sd.day == d].set_index("ts")89 line(ax, s.index, s.cum_L_bp, "dL"); line(ax, s.index, s.cum_S_bp, "dS"); line(ax, s.index, s.cum_C_bp, "dC", lw=1.5)90 line(ax, s.index, s.cum_y2_bp, "dy2", lw=1.0); line(ax, s.index, s.cum_y10_bp, "dy10", lw=1.1)91 rt = pd.Timestamp(f"{d} {t}", tz="America/New_York"); ax.axvline(rt, color="#333333", lw=0.7)92 ax.annotate(t, (rt, ax.get_ylim()[1]), xytext=(2, -2), textcoords="offset points", fontsize=6.5, va="top", color="#333333")93 day0 = pd.Timestamp(d, tz="America/New_York")94 ax.set_xlim(day0 - pd.Timedelta(hours=6), day0 + pd.Timedelta(hours=15, minutes=30))95 ax.xaxis.set_major_locator(mdates.HourLocator(byhour=[18, 0, 6, 12], tz="America/New_York")); ax.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M", tz="America/New_York"))96 ax.axhline(0, color="#bbbbbb", lw=0.5, zorder=0)97 panel(ax, let, title); ax.set_ylabel("Cumulative change since 15:30 ET (bp)")98for ax in axes[1]: ax.set_xlabel("Eastern time (18:00 previous day → 15:30)")99h, l = axes[0, 0].get_legend_handles_labels(); fig.legend(h, l, loc="lower center", ncol=5, bbox_to_anchor=(0.5, -0.02), fontsize=7.5)100fig.tight_layout(rect=(0, 0.04, 1, 1)); save(fig, "fig_sample_days.pdf")101102# ================================================================================================103# 4. event paths with bootstrap bands104eb = pd.read_csv(FD / "event_paths_boot.csv")105fig, axes = plt.subplots(1, 3, figsize=(6.5, 2.7))106for ax, typ, let, title in zip(axes, ["FOMC", "CPI", "EMP"], "ABC", ["FOMC statement", "CPI release", "Employment report"]):107 for c in ["dL", "dS", "dC", "dSR", "dy10"]:108 s = eb[(eb.type == typ) & (eb.series == c)].sort_values("minute")109 ax.fill_between(s.minute, s.lo, s.hi, color=STY[c]["color"], alpha=0.13, lw=0)110 line(ax, s.minute, s.rms, c)111 n = int(eb[(eb.type == typ)].n.iloc[0])112 ax.axvline(0, color="#333333", lw=0.7); ax.set_xlim(-60, 120); ax.set_xlabel("Minutes from release"); panel(ax, let, f"{title} ($n$ = {n})")113 ax.set_xticks([-60, -30, 0, 30, 60, 90, 120]); ax.set_ylim(bottom=0)114axes[0].set_ylabel("RMS cumulative change (bp)")115h, l = axes[0].get_legend_handles_labels(); h.append(Rectangle((0, 0), 1, 1, color=OI["grey"], alpha=0.3, lw=0)); l.append("90% bootstrap band (events resampled)")116fig.legend(h, l, loc="lower center", ncol=3, bbox_to_anchor=(0.5, -0.06), fontsize=7)117fig.tight_layout(rect=(0, 0.10, 1, 1)); save(fig, "fig_event_paths.pdf")118119# ================================================================================================120# 5. periodicity121pr = pd.read_csv(M / "intraday_periodicity.csv", header=[0, 1], index_col=0).sort_index()122tod = pr.index.values.astype(int)123order = np.argsort(np.where(tod >= 18 * 60, tod - 18 * 60, tod + 6 * 60))124x = np.arange(len(order))125ticks = [0, 4 * 60, 8 * 60, 12 * 60, 16 * 60, 20 * 60]; tlab = ["18:00", "22:00", "02:00", "06:00", "10:00", "14:00"]126fig, axes = plt.subplots(1, 3, figsize=(6.5, 2.6))127for ax, c, let in zip(axes, ["dL", "dS", "dC"], "ABC"):128 for grp, lab, key, ls in (("non_event_days", "Non-announcement days", "dy2", "--"), ("event_days", "Announcement days (FOMC, CPI, Employment)", c, "-")):129 v = np.sqrt(pr[(grp, c)].values[order]); ax.plot(x, ma(v, 15), label=lab, color=OI["black"] if grp == "non_event_days" else STY[c]["color"], ls=ls, lw=1.2 if grp == "event_days" else 1.0)130 for m, lab in ((14 * 60 + 30, "08:30"), (20 * 60, "14:00")):131 ax.axvline(m, color="#bbbbbb", lw=0.6, zorder=0)132 ax.set_xticks(ticks); ax.set_xticklabels(tlab); ax.set_xlim(0, len(order)); ax.set_ylim(bottom=0)133 ax.text(14 * 60 + 30, ax.get_ylim()[1] * 0.97, "08:30", fontsize=6.5, ha="center", va="top", color="#333333")134 panel(ax, let, LAB[c]); ax.set_xlabel("Eastern time (18:00 → 17:00)")135axes[0].set_ylabel("RMS change (bp/min, 15-min MA)")136h, l = axes[0].get_legend_handles_labels(); fig.legend(h, l, loc="lower center", ncol=2, bbox_to_anchor=(0.5, -0.04), fontsize=7.5)137fig.tight_layout(rect=(0, 0.08, 1, 1)); save(fig, "fig_periodicity.pdf")138139# ================================================================================================140# 6. realized volatility141dr = pd.read_csv(M / "factors_daily_realized.csv", index_col=0, parse_dates=True).sort_index(); dr = dr[dr.n_minutes >= 600]142fig, axes = plt.subplots(3, 1, figsize=(6.5, 5.6), sharex=True)143for ax, (c, k), let in zip(axes, [("dL", "L"), ("dS", "S"), ("dC", "C")], "ABC"):144 v = np.sqrt(dr[f"RV_{k}_5m"]) * 100145 for a, b in (("2011-01-01", "2015-12-16"), ("2020-03-15", "2021-12-31")): ax.axvspan(pd.Timestamp(a), pd.Timestamp(b), color="#ececec", zorder=-1, lw=0)146 ax.plot(v.index, v, color=OI["grey"], lw=0.35, label="Daily", zorder=1)147 ax.plot(v.index, v.rolling(22, min_periods=10).mean(), color=STY[c]["color"], lw=1.1, label="22-day moving average", zorder=2)148 top = float(np.nanpercentile(v, 99.7)) * 1.05; ax.set_ylim(0, top)149 nclip = int((v > top).sum())150 if nclip: ax.annotate(f"{nclip} daily values above {top:.0f} bp (max {v.max():.0f})", (0.99, 0.95), xycoords="axes fraction", ha="right", va="top", fontsize=6.5, color="#333333")151 panel(ax, let, LAB[c]); ax.set_ylabel("bp per day")152axes[-1].set_xlabel("Shaded: zero-lower-bound periods (1 Jan 2011 – 16 Dec 2015; 15 Mar 2020 – 31 Dec 2021)")153h, l = axes[0].get_legend_handles_labels(); axes[0].legend(h, l, loc="upper left", ncol=2)154fig.tight_layout(); save(fig, "fig_realized_vol.pdf")155156# ================================================================================================157# 7. signature158rc = json.load(open(M / "realized_correlations.json")); nb = json.load(open(M / "noise_benchmark.json"))159ks = ["1m", "5m", "30m"]; xk = [1, 5, 30]160fig, (a1, a2) = plt.subplots(1, 2, figsize=(6.5, 2.8))161for c, key in (("L", "dL"), ("S", "dS"), ("C", "dC")):162 mline(a1, xk, [rc["rv_mean_bp2"][k][c] for k in ks], key)163a1.set_xscale("log"); a1.set_xticks(xk); a1.set_xticklabels(["1", "5", "30"]); a1.set_xlabel("Sampling interval (minutes, log scale)"); a1.set_ylabel("Average daily realized variance (bp$^2$)")164a1.set_ylim(0, max(rc["rv_mean_bp2"]["1m"].values()) * 1.25); a1.legend(loc="upper right", ncol=3, fontsize=7, columnspacing=0.8); panel(a1, "A", "Variance signature")165pairs = (("LS", "Level–slope", OI["blue"], "o", "-"), ("LC", "Level–curvature", OI["verm"], "s", "--"), ("SC", "Slope–curvature", OI["green"], "^", ":"))166for pair, lab, col, mk, ls in pairs:167 a2.plot(xk, [rc["full"][k][pair] for k in ks], label=lab, color=col, marker=mk, ms=3.8, markerfacecolor="white", markeredgewidth=1.0, ls=ls)168 a2.plot([0.8, 40], [rc["daily_fred"]["full"][pair]] * 2, color=col, lw=0.9, ls=(0, (1, 1.5)), alpha=0.9)169 a2.annotate("daily", (40, rc["daily_fred"]["full"][pair]), xytext=(2, 0), textcoords="offset points", fontsize=5.5, color=col, va="center")170a2.axhline(nb["mean_noise_corr_LS"], color=OI["black"], lw=0.9, ls="-.")171a2.annotate(f"level–slope implied by tick noise alone ({nb['mean_noise_corr_LS']:.2f})", (0.8, nb["mean_noise_corr_LS"]), xytext=(0, 3), textcoords="offset points", fontsize=5.8, color="#333333", va="bottom")172extra = [Line2D([0], [0], color="#555555", lw=0.9, ls=(0, (1, 1.5)), label="Daily CMT correlation 2011–2026 (dotted, same colour)")]173a2.set_xscale("log"); a2.set_xticks(xk); a2.set_xticklabels(["1", "5", "30"]); a2.set_xlim(0.8, 40); a2.set_xlabel("Sampling interval (minutes, log scale)"); a2.set_ylabel("Realized correlation, 2011–2026"); a2.set_ylim(-1.05, 0.75)174hh, ll = a2.get_legend_handles_labels(); a2.legend(hh + extra, ll + [extra[0].get_label()], loc="upper right", ncol=1, fontsize=6.3); panel(a2, "B", "Correlation signature")175fig.tight_layout(); save(fig, "fig_signature.pdf")176177# ================================================================================================178# 8. event heatmaps179cmap = LinearSegmentedColormap.from_list("oi_div", [OI["blue"], "#ffffff", OI["verm"]])180CLIP = 15.0181fig, axes = plt.subplots(3, 1, figsize=(6.5, 8.4), gridspec_kw=dict(height_ratios=[1, 1.15, 1.2]))182for ax, typ, let, title, conf in zip(axes, ["FOMC", "CPI", "EMP"], "ABC", ["FOMC statements", "CPI releases", "Employment reports"], [True, False, False]):183 H = pd.read_csv(FD / f"event_heatmap_{typ}.csv"); H["date"] = pd.to_datetime(H["date"]); H = H.sort_values("date").reset_index(drop=True)184 mins = np.array([int(c) for c in H.columns[1:]]); Z = np.clip(H.iloc[:, 1:].values.astype(float), -CLIP, CLIP)185 im = ax.pcolormesh(np.r_[mins - 0.5, mins[-1] + 0.5], np.arange(len(H) + 1), Z, cmap=cmap, norm=TwoSlopeNorm(0, -CLIP, CLIP), rasterized=True)186 ax.set_ylim(0, len(H)); ax.set_xlim(-30, 120); ax.grid(False)187 ax.axvline(0, color="#000000", lw=0.8); ax.axvline(-5, color="#000000", lw=0.4, ls=":")188 if conf: ax.axvline(30, color="#000000", lw=0.8, ls="--"); ax.text(30.8, len(H) * 0.985, "press conference 14:30", fontsize=6.3, va="top", ha="left")189 for d, lab in REGIME_EDGES:190 k = int((H["date"] < pd.Timestamp(d)).sum())191 if 0 < k < len(H): ax.axhline(k, color="#000000", lw=0.6); ax.text(121, k, lab, fontsize=5.8, va="center", ha="left", color="#333333")192 yrs = H["date"].dt.year.values; yt = [np.where(yrs == y)[0][0] + 0.5 for y in sorted(set(yrs)) if y % 3 == 2 or y == yrs.min()]193 ax.set_yticks(yt); ax.set_yticklabels([str(y) for y in sorted(set(yrs)) if y % 3 == 2 or y == yrs.min()], fontsize=7)194 ax.set_xticks([-30, 0, 15, 30, 60, 90, 120]); ax.tick_params(length=2)195 panel(ax, let, f"{title} ($n$ = {len(H)}, one row per event, oldest at the bottom)")196 for sp in ax.spines.values(): sp.set_visible(True)197axes[-1].set_xlabel("Minutes from scheduled release (dotted line: −5 min, start of cumulation)")198fig.subplots_adjust(left=0.07, right=0.84, top=0.975, bottom=0.11, hspace=0.3)199cax = fig.add_axes([0.25, 0.035, 0.45, 0.012]); cb = fig.colorbar(im, cax=cax, orientation="horizontal")200cb.set_label(f"Implied 2-year yield, cumulative change (bp); scale clipped at ±{CLIP:.0f} bp — blue = fall, vermillion = rise", fontsize=7)201cb.ax.tick_params(labelsize=7)202fig.savefig(OUT / "fig_event_heatmap.pdf", bbox_inches="tight", pad_inches=0.02); plt.close(fig)203204# ================================================================================================205# 9. cartography of the pipeline206rd = json.load(open(M / "returns_diagnostics.json")); fid = json.load(open(M / "mimicking_fidelity.json"))207nbars = sum(rd[r]["bars"] for r in ["ZT", "ZF", "ZN", "TN", "US", "UB"]) / 1e6208fig = plt.figure(figsize=(6.5, 7.6)); ax = fig.add_axes([0, 0, 1, 1]); ax.set_xlim(0, 100); ax.set_ylim(0, 117); ax.axis("off")209C = {"src": "#e8f1fa", "node": "#fff4e6", "pub": "#eef7f1", "out": "#f3eef7", "val": "#fdf6e3"}210def box(x, y, w, h, title, body, fc, ec="#444444", fs=6.2, tfs=7.2, lw=0.8, ls="-"):211 ax.add_patch(FancyBboxPatch((x, y), w, h, boxstyle="round,pad=0.3,rounding_size=1.2", fc=fc, ec=ec, lw=lw, ls=ls))212 ax.text(x + 1.2, y + h - 1.3, title, fontsize=tfs, fontweight="bold", va="top", ha="left")213 ax.text(x + 1.2, y + h - 4.6, body, fontsize=fs, va="top", ha="left", linespacing=1.25)214def arrow(x0, y0, x1, y1, label="", fs=5.8, col="#333333", rad=0.0, lx=0, ly=0):215 ax.add_patch(FancyArrowPatch((x0, y0), (x1, y1), arrowstyle="-|>", mutation_scale=8, lw=0.8, color=col, connectionstyle=f"arc3,rad={rad}"))216 if label: ax.text((x0 + x1) / 2 + lx, (y0 + y1) / 2 + ly, label, fontsize=fs, color=col, ha="center", va="center", bbox=dict(fc="white", ec="none", pad=0.6))217# isolation boundary: the compute node218ax.add_patch(Rectangle((2, 23), 96, 73, fc="none", ec="#8a8a8a", lw=1.2, ls=(0, (6, 3))))219ax.text(50, 26.6, "Compute node: Mac Studio M3U96a (Apple M3 Ultra, 32 cores, 96 GB), Python 3.12 (uv): polars · pandas · numpy · scipy · arch.\nDashed boundary = licence perimeter: raw one-minute bars never leave this node; only aggregates cross it.",220 fontsize=5.6, va="top", ha="center", color="#555555", linespacing=1.15)221# sources (top)222box(2, 100, 46, 15, "FinData OS lakehouse (private, FirstRate Data licence)", "Parquet lake 565 GB · REST API :8600 · exports pinned to dataset versions\n• futures.1m.continuous_raw v132: ZT ZF ZN TN US UB, 2008-01 → 2026-11\n• futures.1d.continuous_{raw, ratio} v133 (roll detection) · SR3 1m (2018 →)\n• etf.1m.split_div: SHY IEI IEF TLH TLT (2010 →)", C["src"])223box(52, 100, 46, 15, "Public inputs (downloaded 2026-11-09, data/external/)", "• FRED H.15 constant-maturity yields, 11 series 1m–30y (15:30 ET)\n• Fed Board GSW zero curve feds200628.csv (SVENY01–30)\n• Fed FOMC calendars 2008–2026 (statement dates, time rule)\n• ALFRED release dates: CPI (rid 10), Employment (rid 50), 08:30 ET\n• FRED DFF (regimes)", C["pub"])224# stage boxes inside the node225box(4, 80, 28, 11, "01 · export", f"{nbars:.0f} M one-minute bars, 6 roots\n≈ 0.30 GB Parquet (18 files)\n+ daily raw/ratio, SR3, 5 ETFs (0.12 GB)", C["node"])226box(36, 80, 28, 11, "03 · return panel", f"{rd['panel']['minutes']/1e6:.2f} M minutes × 6 roots, log returns (%)\nsegments: break 17–18 ET, rolls, gaps > 5 min\nyield day 15:30 → 15:30 ET; {rd['panel']['yield_days']:,} days", C["node"])227box(68, 80, 28, 11, "02 · daily NS factors", "OLS fit of 11 CMT yields, λ = 0.7308\nlevel, slope, curvature (pp), 2007 → 2026\nalso GSW-based and CMT ≥ 1y / ≥ 2y targets", C["node"])228box(20, 63, 30, 11, "04 · mimicking portfolios", "Δβ_k,t = a + w′r_t, OLS, trailing 3 years\nre-estimated each quarter → OOS weights\n4 → 5 (2013) → 6 (2019) contracts", C["node"])229box(54, 63, 42, 11, "06 · minute factors", f"Δβ̂_t,m = ŵ_q′ r_t,m — {fid['full_2011_2026']['level']['n']:,} yield days, 5.0 M minutes\nrealized variances / covariances at 1, 5, 30 min\nintraday periodicity by minute of day", C["node"])230box(4, 44, 22, 13, "07 · events", "150 FOMC · 225 CPI · 226 EMP\nwindows [−5,+25], [−5,+15], [−5,+90]; n = 126 / 187 / 190\nplacebo same clock time, 3 days/event\nevent-resampling bootstrap", C["node"])231box(29, 44, 22, 13, "08 · HAR forecasts", "log RV (5-min), horizons 1 / 5 / 22 d\nvs GARCH(1,1) on daily Δβ, rolling 22 d\nmonthly re-estimation, OOS 2016 → 2026\nQLIKE · MSE · Diebold–Mariano (Holm)", C["node"])232box(54, 44, 22, 13, "13 · IRF / VAR", "local-projection IRFs to announcement\nshocks; 5-min VAR(ΔL, ΔS, ΔC) IRFs, FEVD;\nrealized-volatility spillovers\n(Diebold–Yilmaz)", C["node"])233box(79, 44, 17, 13, "09a · figdata", "small aggregates\nonly: event matrices,\npaths, sample days,\nbootstrap bands", C["node"])234# validation loops235box(4, 27, 92, 13, "Validation loops (all out of sample unless stated)", "04b GSW target & SR3 seventh instrument (2022 →) · 04c cash-yield ceiling (2/5/10/30y CMT changes as regressors) & leave-one-contract-out spanning (1/5/30 min)\n06b independent ETF construction (duration inversion + loading OLS, no daily step) · 06c tick-size noise benchmark for the level–slope correlation\n04d expanding window · 15:00 / 16:00 yield-day boundaries · 5-min RV denominators · FOMC release-minute check", C["val"], ls=(0, (3, 2)))236# outputs (bottom, outside the node)237box(2, 2, 46, 17, "Outputs that leave the node (aggregates only)", "results/metrics/*.json|csv — fidelity, weights, event summary, correlations,\nHAR losses, IRFs · results/figdata/*.csv · docs/ (design, review, response)\nrepo: gitsrv:srv/git/wp47_uqo.git (no bars, no cache)", C["out"])238box(52, 2, 46, 17, "Laptop — manuscript", "scripts/11_tables.py → paper/tables/*.tex + numbers.tex (1 047 macros)\nscripts/12_text_macros.py → numbers_text.tex · scripts/10_figures.py → paper/figures/*.pdf\nlatexmk → main.pdf; 50 Crossref-verified references", C["out"])239# arrows240arrow(18, 100, 18, 91.5, "REST export → Parquet", lx=-14, ly=2.5)241arrow(60, 100, 82, 91.5, "CSV / HTML parsed", lx=12, ly=3.2)242arrow(32, 85.5, 36, 85.5)243arrow(50, 80, 42, 74.5, "daily r_t (6)", lx=-9)244arrow(78, 80, 42, 74.5, "Δβ_t (3)", lx=8, ly=-1.5)245arrow(50, 68.5, 54, 68.5, "ŵ_q", ly=2)246arrow(50, 80, 70, 74.5, "r_t,m (1-min)", lx=10, ly=1.5)247arrow(66, 63, 15, 57.5, "Δβ̂ minutes", lx=-6, ly=1.5)248arrow(70, 63, 40, 57.5, "RV_t", lx=6, ly=1.5)249arrow(75, 63, 65, 57.5)250arrow(85, 63, 87, 57.5)251arrow(50, 44, 50, 40.5)252arrow(25, 23, 25, 19.5, "JSON · CSV aggregates", lx=-14, ly=0.0)253arrow(48, 10.5, 52, 10.5)254# legend255for i, (k, lab) in enumerate((("src", "private data source"), ("pub", "public inputs"), ("node", "pipeline stage (script no.)"), ("val", "validation loop"), ("out", "deliverables"))):256 ax.add_patch(Rectangle((28 + i * 14.5, 20.6), 2.0, 1.5, fc=C[k], ec="#444444", lw=0.6)); ax.text(30.4 + i * 14.5, 21.35, lab, fontsize=5.3, va="center")257save(fig, "fig_cartography.pdf")258259print("figures:", sorted(p.name for p in OUT.glob("*.pdf")))260261# ================================================================================================262if "--audit" in sys.argv:263 aud = Path("/tmp/wp47fig"); aud.mkdir(exist_ok=True)264 for p in sorted(OUT.glob("*.pdf")):265 subprocess.run(["pdftoppm", "-png", "-r", "110", str(p), str(aud / p.stem)], check=True)266 subprocess.run(["pdftoppm", "-png", "-gray", "-r", "110", str(p), str(aud / (p.stem + "_gray"))], check=True)267 print("audit renders in", aud)268