"""Paper figures. Runs on the laptop from results/metrics and results/figdata; writes paper/figures/*.pdf (vector). Standard (figure mandate 2026-11-09): Okabe–Ito colour-blind-safe palette + distinct line styles/markers so every figure survives grayscale; uncertainty bands where available; units on axes; labelled thresholds that do not collide with data; panel letters; self-contained small multiples. Audit: scripts/10_figures.py --audit renders every PDF in colour and grayscale to /tmp/wp47fig/. """ import json, sys, subprocess from pathlib import Path import numpy as np, pandas as pd import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt import matplotlib.dates as mdates from matplotlib.lines import Line2D from matplotlib.patches import FancyBboxPatch, FancyArrowPatch, Rectangle from matplotlib.colors import LinearSegmentedColormap, TwoSlopeNorm ROOT = Path(__file__).resolve().parents[1]; M = ROOT / "results/metrics"; FD = ROOT / "results/figdata"; OUT = ROOT / "paper/figures"; OUT.mkdir(exist_ok=True, parents=True) plt.rcParams.update({"font.family": "serif", "font.serif": ["Times New Roman", "Times", "STIXGeneral", "DejaVu Serif"], "mathtext.fontset": "stix", "font.size": 9, "axes.labelsize": 9, "axes.titlesize": 9.5, "legend.fontsize": 7.5, "xtick.labelsize": 8, "ytick.labelsize": 8, "axes.grid": True, "grid.color": "#e3e3e3", "grid.linewidth": 0.5, "axes.edgecolor": "#444444", "axes.linewidth": 0.6, "lines.linewidth": 1.3, "legend.frameon": False, "axes.spines.top": False, "axes.spines.right": False, "pdf.fonttype": 42, "axes.unicode_minus": True}) # Okabe–Ito OI = {"blue": "#0072B2", "verm": "#D55E00", "green": "#009E73", "pink": "#CC79A7", "orange": "#E69F00", "sky": "#56B4E9", "yellow": "#F0E442", "grey": "#999999", "black": "#000000"} STY = {"dL": dict(color=OI["blue"], ls="-", marker="o"), "dS": dict(color=OI["verm"], ls="--", marker="s"), "dC": dict(color=OI["green"], ls=":", marker="^"), "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")} for k in ("level", "slope", "curvature"): STY[k] = STY[{"level": "dL", "slope": "dS", "curvature": "dC"}[k]] LAB = {"dL": "Level", "dS": "Slope", "dC": "Curvature", "dSR": "Short rate ($L+S$)", "dy10": "Implied 10-year yield", "dy2": "Implied 2-year yield", "level": "Level", "slope": "Slope", "curvature": "Curvature"} def line(ax, x, y, key, label=None, markevery=None, lw=None, **kw): s = dict(STY[key]); s.pop("marker"); s.update(kw) if lw: s["lw"] = lw return ax.plot(x, y, label=label if label is not None else LAB[key], **s) def mline(ax, x, y, key, label=None, ms=3.5, **kw): s = dict(STY[key]); s.update(kw); s["ms"] = ms; s["markerfacecolor"] = "white"; s["markeredgewidth"] = 1.0 return ax.plot(x, y, label=label if label is not None else LAB[key], **s) def panel(ax, letter, title): ax.set_title(f"{letter}. {title}", loc="left", fontweight="bold") def ma(x, w): return pd.Series(x).rolling(w, center=True, min_periods=1).mean().values def save(fig, name): fig.savefig(OUT / name, bbox_inches="tight", pad_inches=0.02); plt.close(fig) REGIME_EDGES = [("2015-12-16", "ZLB | normalisation"), ("2020-01-01", "normalisation | pandemic"), ("2022-01-01", "pandemic | 2022–2026 cycle")] # ================================================================================================ # 1. fidelity by year (with bootstrap 90% bands) fy = pd.read_csv(FD / "fidelity_by_year.csv") fig, axes = plt.subplots(1, 3, figsize=(6.5, 2.5), sharey=True) for ax, f, let, thr in zip(axes, ["level", "slope", "curvature"], "ABC", [0.9, 0.9, 0.6]): for tgt, lab, key, mk in (("cmt", "CMT target (11 maturities, primary)", "dL", "o"), ("gsw", "GSW zero-curve target (1–30 years)", "dC", "s")): s = fy[(fy.target == tgt) & (fy.factor == f)].sort_values("year") col = OI["blue"] if tgt == "cmt" else OI["green"] if "lo" in s: ax.fill_between(s.year, s.lo, s.hi, color=col, alpha=0.15, lw=0) 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) ax.axhline(thr, color="#333333", lw=0.7, ls=(0, (4, 2))) ax.text(2027.0, thr, f"H1\n{thr:.2f}", fontsize=6.0, color="#333333", ha="left", va="center", clip_on=False) 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]) ax.margins(x=0) axes[0].set_ylabel("Out-of-sample $R^2$ (daily)") h, 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 (days resampled within year)") fig.legend(h, l, loc="lower center", ncol=3, bbox_to_anchor=(0.5, -0.04), fontsize=7) fig.tight_layout(rect=(0, 0.07, 0.985, 1), w_pad=1.6); save(fig, "fig_fidelity.pdf") # ================================================================================================ # 2. ETF cross-validation scatter pp = pd.read_csv(FD / "etf_fomc_pairs.csv") fig, axes = plt.subplots(1, 3, figsize=(6.5, 2.6)) for ax, c, let in zip(axes, ["dL", "dS", "dC"], "ABC"): 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) lim = np.nanmax(np.abs(np.r_[xe, yf])) * 1.1 ax.plot([-lim, lim], [-lim, lim], color=OI["grey"], lw=0.7, ls="--", label="45° line") 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}") ax.scatter(xe, yf, s=11, color=OI["blue"], alpha=0.8, edgecolor="white", linewidth=0.3, zorder=3, label="FOMC windows") 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)}") ax.set_xlabel("ETF-based change (bp)"); ax.legend(loc="upper left", fontsize=6.2, handlelength=1.6) axes[0].set_ylabel("Futures-based change (bp)") fig.tight_layout(); save(fig, "fig_etf_crossval.pdf") # ================================================================================================ # 3. sample days sd = pd.read_csv(FD / "sample_days.csv"); sd["ts"] = pd.to_datetime(sd["ts"], utc=True).dt.tz_convert("America/New_York") rel = {"2013-06-19": ("14:00", "FOMC statement, 19 June 2013"), "2022-06-15": ("14:00", "FOMC statement, 15 June 2022"), "2022-11-10": ("08:30", "CPI release, 10 November 2022"), "2024-08-02": ("08:30", "Employment report, 2 August 2024")} fig, axes = plt.subplots(2, 2, figsize=(6.5, 5.4)) for ax, (d, (t, title)), let in zip(axes.ravel(), rel.items(), "ABCD"): s = sd[sd.day == d].set_index("ts") 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) line(ax, s.index, s.cum_y2_bp, "dy2", lw=1.0); line(ax, s.index, s.cum_y10_bp, "dy10", lw=1.1) rt = pd.Timestamp(f"{d} {t}", tz="America/New_York"); ax.axvline(rt, color="#333333", lw=0.7) ax.annotate(t, (rt, ax.get_ylim()[1]), xytext=(2, -2), textcoords="offset points", fontsize=6.5, va="top", color="#333333") day0 = pd.Timestamp(d, tz="America/New_York") ax.set_xlim(day0 - pd.Timedelta(hours=6), day0 + pd.Timedelta(hours=15, minutes=30)) 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")) ax.axhline(0, color="#bbbbbb", lw=0.5, zorder=0) panel(ax, let, title); ax.set_ylabel("Cumulative change since 15:30 ET (bp)") for ax in axes[1]: ax.set_xlabel("Eastern time (18:00 previous day → 15:30)") h, 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) fig.tight_layout(rect=(0, 0.04, 1, 1)); save(fig, "fig_sample_days.pdf") # ================================================================================================ # 4. event paths with bootstrap bands eb = pd.read_csv(FD / "event_paths_boot.csv") fig, axes = plt.subplots(1, 3, figsize=(6.5, 2.7)) for ax, typ, let, title in zip(axes, ["FOMC", "CPI", "EMP"], "ABC", ["FOMC statement", "CPI release", "Employment report"]): for c in ["dL", "dS", "dC", "dSR", "dy10"]: s = eb[(eb.type == typ) & (eb.series == c)].sort_values("minute") ax.fill_between(s.minute, s.lo, s.hi, color=STY[c]["color"], alpha=0.13, lw=0) line(ax, s.minute, s.rms, c) n = int(eb[(eb.type == typ)].n.iloc[0]) 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})") ax.set_xticks([-60, -30, 0, 30, 60, 90, 120]); ax.set_ylim(bottom=0) axes[0].set_ylabel("RMS cumulative change (bp)") h, 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)") fig.legend(h, l, loc="lower center", ncol=3, bbox_to_anchor=(0.5, -0.06), fontsize=7) fig.tight_layout(rect=(0, 0.10, 1, 1)); save(fig, "fig_event_paths.pdf") # ================================================================================================ # 5. periodicity pr = pd.read_csv(M / "intraday_periodicity.csv", header=[0, 1], index_col=0).sort_index() tod = pr.index.values.astype(int) order = np.argsort(np.where(tod >= 18 * 60, tod - 18 * 60, tod + 6 * 60)) x = np.arange(len(order)) ticks = [0, 4 * 60, 8 * 60, 12 * 60, 16 * 60, 20 * 60]; tlab = ["18:00", "22:00", "02:00", "06:00", "10:00", "14:00"] fig, axes = plt.subplots(1, 3, figsize=(6.5, 2.6)) for ax, c, let in zip(axes, ["dL", "dS", "dC"], "ABC"): for grp, lab, key, ls in (("non_event_days", "Non-announcement days", "dy2", "--"), ("event_days", "Announcement days (FOMC, CPI, Employment)", c, "-")): 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) for m, lab in ((14 * 60 + 30, "08:30"), (20 * 60, "14:00")): ax.axvline(m, color="#bbbbbb", lw=0.6, zorder=0) ax.set_xticks(ticks); ax.set_xticklabels(tlab); ax.set_xlim(0, len(order)); ax.set_ylim(bottom=0) ax.text(14 * 60 + 30, ax.get_ylim()[1] * 0.97, "08:30", fontsize=6.5, ha="center", va="top", color="#333333") panel(ax, let, LAB[c]); ax.set_xlabel("Eastern time (18:00 → 17:00)") axes[0].set_ylabel("RMS change (bp/min, 15-min MA)") h, 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) fig.tight_layout(rect=(0, 0.08, 1, 1)); save(fig, "fig_periodicity.pdf") # ================================================================================================ # 6. realized volatility dr = pd.read_csv(M / "factors_daily_realized.csv", index_col=0, parse_dates=True).sort_index(); dr = dr[dr.n_minutes >= 600] fig, axes = plt.subplots(3, 1, figsize=(6.5, 5.6), sharex=True) for ax, (c, k), let in zip(axes, [("dL", "L"), ("dS", "S"), ("dC", "C")], "ABC"): v = np.sqrt(dr[f"RV_{k}_5m"]) * 100 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) ax.plot(v.index, v, color=OI["grey"], lw=0.35, label="Daily", zorder=1) 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) top = float(np.nanpercentile(v, 99.7)) * 1.05; ax.set_ylim(0, top) nclip = int((v > top).sum()) 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") panel(ax, let, LAB[c]); ax.set_ylabel("bp per day") axes[-1].set_xlabel("Shaded: zero-lower-bound periods (1 Jan 2011 – 16 Dec 2015; 15 Mar 2020 – 31 Dec 2021)") h, l = axes[0].get_legend_handles_labels(); axes[0].legend(h, l, loc="upper left", ncol=2) fig.tight_layout(); save(fig, "fig_realized_vol.pdf") # ================================================================================================ # 7. signature rc = json.load(open(M / "realized_correlations.json")); nb = json.load(open(M / "noise_benchmark.json")) ks = ["1m", "5m", "30m"]; xk = [1, 5, 30] fig, (a1, a2) = plt.subplots(1, 2, figsize=(6.5, 2.8)) for c, key in (("L", "dL"), ("S", "dS"), ("C", "dC")): mline(a1, xk, [rc["rv_mean_bp2"][k][c] for k in ks], key) a1.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$)") a1.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") pairs = (("LS", "Level–slope", OI["blue"], "o", "-"), ("LC", "Level–curvature", OI["verm"], "s", "--"), ("SC", "Slope–curvature", OI["green"], "^", ":")) for pair, lab, col, mk, ls in pairs: 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) a2.plot([0.8, 40], [rc["daily_fred"]["full"][pair]] * 2, color=col, lw=0.9, ls=(0, (1, 1.5)), alpha=0.9) a2.annotate("daily", (40, rc["daily_fred"]["full"][pair]), xytext=(2, 0), textcoords="offset points", fontsize=5.5, color=col, va="center") a2.axhline(nb["mean_noise_corr_LS"], color=OI["black"], lw=0.9, ls="-.") a2.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") extra = [Line2D([0], [0], color="#555555", lw=0.9, ls=(0, (1, 1.5)), label="Daily CMT correlation 2011–2026 (dotted, same colour)")] a2.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) hh, 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") fig.tight_layout(); save(fig, "fig_signature.pdf") # ================================================================================================ # 8. event heatmaps cmap = LinearSegmentedColormap.from_list("oi_div", [OI["blue"], "#ffffff", OI["verm"]]) CLIP = 15.0 fig, axes = plt.subplots(3, 1, figsize=(6.5, 8.4), gridspec_kw=dict(height_ratios=[1, 1.15, 1.2])) for ax, typ, let, title, conf in zip(axes, ["FOMC", "CPI", "EMP"], "ABC", ["FOMC statements", "CPI releases", "Employment reports"], [True, False, False]): 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) mins = np.array([int(c) for c in H.columns[1:]]); Z = np.clip(H.iloc[:, 1:].values.astype(float), -CLIP, CLIP) 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) ax.set_ylim(0, len(H)); ax.set_xlim(-30, 120); ax.grid(False) ax.axvline(0, color="#000000", lw=0.8); ax.axvline(-5, color="#000000", lw=0.4, ls=":") 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") for d, lab in REGIME_EDGES: k = int((H["date"] < pd.Timestamp(d)).sum()) 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") 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()] 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) ax.set_xticks([-30, 0, 15, 30, 60, 90, 120]); ax.tick_params(length=2) panel(ax, let, f"{title} ($n$ = {len(H)}, one row per event, oldest at the bottom)") for sp in ax.spines.values(): sp.set_visible(True) axes[-1].set_xlabel("Minutes from scheduled release (dotted line: −5 min, start of cumulation)") fig.subplots_adjust(left=0.07, right=0.84, top=0.975, bottom=0.11, hspace=0.3) cax = fig.add_axes([0.25, 0.035, 0.45, 0.012]); cb = fig.colorbar(im, cax=cax, orientation="horizontal") cb.set_label(f"Implied 2-year yield, cumulative change (bp); scale clipped at ±{CLIP:.0f} bp — blue = fall, vermillion = rise", fontsize=7) cb.ax.tick_params(labelsize=7) fig.savefig(OUT / "fig_event_heatmap.pdf", bbox_inches="tight", pad_inches=0.02); plt.close(fig) # ================================================================================================ # 9. cartography of the pipeline rd = json.load(open(M / "returns_diagnostics.json")); fid = json.load(open(M / "mimicking_fidelity.json")) nbars = sum(rd[r]["bars"] for r in ["ZT", "ZF", "ZN", "TN", "US", "UB"]) / 1e6 fig = 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") C = {"src": "#e8f1fa", "node": "#fff4e6", "pub": "#eef7f1", "out": "#f3eef7", "val": "#fdf6e3"} def box(x, y, w, h, title, body, fc, ec="#444444", fs=6.2, tfs=7.2, lw=0.8, ls="-"): 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)) ax.text(x + 1.2, y + h - 1.3, title, fontsize=tfs, fontweight="bold", va="top", ha="left") ax.text(x + 1.2, y + h - 4.6, body, fontsize=fs, va="top", ha="left", linespacing=1.25) def arrow(x0, y0, x1, y1, label="", fs=5.8, col="#333333", rad=0.0, lx=0, ly=0): ax.add_patch(FancyArrowPatch((x0, y0), (x1, y1), arrowstyle="-|>", mutation_scale=8, lw=0.8, color=col, connectionstyle=f"arc3,rad={rad}")) 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)) # isolation boundary: the compute node ax.add_patch(Rectangle((2, 23), 96, 73, fc="none", ec="#8a8a8a", lw=1.2, ls=(0, (6, 3)))) ax.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.", fontsize=5.6, va="top", ha="center", color="#555555", linespacing=1.15) # sources (top) box(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"]) box(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"]) # stage boxes inside the node box(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"]) box(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"]) box(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"]) box(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"]) box(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"]) box(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"]) box(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"]) box(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"]) box(79, 44, 17, 13, "09a · figdata", "small aggregates\nonly: event matrices,\npaths, sample days,\nbootstrap bands", C["node"]) # validation loops box(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))) # outputs (bottom, outside the node) box(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"]) box(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"]) # arrows arrow(18, 100, 18, 91.5, "REST export → Parquet", lx=-14, ly=2.5) arrow(60, 100, 82, 91.5, "CSV / HTML parsed", lx=12, ly=3.2) arrow(32, 85.5, 36, 85.5) arrow(50, 80, 42, 74.5, "daily r_t (6)", lx=-9) arrow(78, 80, 42, 74.5, "Δβ_t (3)", lx=8, ly=-1.5) arrow(50, 68.5, 54, 68.5, "ŵ_q", ly=2) arrow(50, 80, 70, 74.5, "r_t,m (1-min)", lx=10, ly=1.5) arrow(66, 63, 15, 57.5, "Δβ̂ minutes", lx=-6, ly=1.5) arrow(70, 63, 40, 57.5, "RV_t", lx=6, ly=1.5) arrow(75, 63, 65, 57.5) arrow(85, 63, 87, 57.5) arrow(50, 44, 50, 40.5) arrow(25, 23, 25, 19.5, "JSON · CSV aggregates", lx=-14, ly=0.0) arrow(48, 10.5, 52, 10.5) # legend for i, (k, lab) in enumerate((("src", "private data source"), ("pub", "public inputs"), ("node", "pipeline stage (script no.)"), ("val", "validation loop"), ("out", "deliverables"))): 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") save(fig, "fig_cartography.pdf") print("figures:", sorted(p.name for p in OUT.glob("*.pdf"))) # ================================================================================================ if "--audit" in sys.argv: aud = Path("/tmp/wp47fig"); aud.mkdir(exist_ok=True) for p in sorted(OUT.glob("*.pdf")): subprocess.run(["pdftoppm", "-png", "-r", "110", str(p), str(aud / p.stem)], check=True) subprocess.run(["pdftoppm", "-png", "-gray", "-r", "110", str(p), str(aud / (p.stem + "_gray"))], check=True) print("audit renders in", aud)