SPB Git

Repository Public Research preview

Commit

WP46 paper: wide tables scaled to text width

Simon-Pierre Boucher committed · Nov 10, 2026 parent 87da86e
11 changed files +157 −31 Browse files
modified paper/appendices/extra_tables.tex +4 −4
@@ -7,12 +7,12 @@ Table~\ref{tab:pooledAfullrobust} the 1\%-trimmed and median ratios together wit
7 7
8 8 \begin{table}[htp]\centering\tiny
9 9 \caption{Tier A, all specifications: pooled QLIKE ratios to GARCH(1,1)-normal (test period). Columns as in Table~\ref{tab:pooledA}.}\label{tab:pooledAfull}
10 −\input{tables/tab_pooled_tierA}
10 +\adjustbox{max width=\textwidth}{\input{tables/tab_pooled_tierA}}
11 11 \end{table}
12 12
13 13 \begin{table}[htp]\centering\tiny
14 14 \caption{Robust summaries, $h=1$ and $h=22$: 1\%-trimmed and median pooled ratios and explosive-forecast shares.}\label{tab:pooledAfullrobust}
15 −\input{tables/tab_pooled_robust}
15 +\adjustbox{max width=\textwidth}{\input{tables/tab_pooled_robust}}
16 16 \end{table}
17 17
18 18 \subsection{Model confidence sets over all specifications}
@@ -28,14 +28,14 @@ Table~\ref{tab:pooledAfullrobust} the 1\%-trimmed and median ratios together wit
28 28 \subsection{Regimes at the monthly horizon}
29 29 \begin{table}[htp]\centering\scriptsize
30 30 \caption{Pooled QLIKE ratios by regime, $h=22$ (conventions as in Table~\ref{tab:regimes}).}\label{tab:regimes22}
31 −\input{tables/tab_regimes_h22}
31 +\adjustbox{max width=\textwidth}{\input{tables/tab_regimes_h22}}
32 32 \end{table}
33 33
34 34 \subsection{Secondary proxies and losses}
35 35 \begin{table}[htp]\centering\tiny
36 36 \caption{Pooled ratios to GARCH(1,1)-normal under the secondary proxies (QLIKE) and the secondary losses (primary proxy), $h=1$,
37 37 representative specifications.}\label{tab:secondary}
38 −\input{tables/tab_secondary}
38 +\adjustbox{max width=\textwidth}{\input{tables/tab_secondary}}
39 39 \end{table}
40 40
41 41 \subsection{Cross-package verification}
modified paper/appendices/verification.tex +1 −1
@@ -28,7 +28,7 @@ corresponding numbers are reported in the table when the R fits converged.
28 28
29 29 \begin{table}[tp]\centering\footnotesize
30 30 \caption{Cross-package verification on the shared fixture (maximized log-likelihoods; differences = ours $-$ R).}\label{tab:crosscheck}
31 −\input{tables/tab_crosscheck}
31 +\adjustbox{max width=\textwidth}{\input{tables/tab_crosscheck}}
32 32 \end{table}
33 33
34 34 \paragraph{Known implementation caveats.} (i) arch uses $\kappa=1/2$ in the GJR multi-step recursion for every innovation law
modified paper/main.pdf +0 −0

Binary file not shown.

modified paper/sections/frontier.tex +1 −1
@@ -52,7 +52,7 @@ the architecture, and the skewed Student-$t$ is the only law that calibrates bot
52 52 or were rejected by the likelihood floor; inadmissible = converged outside the admissibility region (kept and flagged);
53 53 coverage = share of origins with a valid forecast; median refit = wall time of one re-estimation (Apple M-series core, one
54 54 thread); iterations = optimizer iterations summed over the multi-start sequence.}\label{tab:accounting}
55 −\input{tables/tab_accounting}
55 +\adjustbox{max width=\textwidth}{\input{tables/tab_accounting}}
56 56 \end{table}
57 57
58 58 \begin{figure}[tp]\centering
modified paper/sections/results_daily.tex +1 −1
@@ -80,7 +80,7 @@ instrument-level ratios; 90\% CI = joint stationary block bootstrap over dates (
80 80 instruments on which the specification significantly beats / loses to the benchmark at 10\% after a Holm adjustment across the
81 81 N instruments. Stars: bootstrap $p$ for H$_0$: pooled relative difference $=0$ (*** 1\%, ** 5\%, * 10\%). Full table with all
82 82 innovation laws, trimmed and median ratios and explosive-forecast shares in Appendix~\ref{app:extra}.}\label{tab:pooledA}
83 −\input{tables/tab_pooled_tierA_rep}
83 +\adjustbox{max width=\textwidth}{\input{tables/tab_pooled_tierA_rep}}
84 84 \end{table}
85 85
86 86 \begin{figure}[tp]\centering
modified paper/sections/results_extensions.tex +1 −1
@@ -17,7 +17,7 @@ confirmatory tables. Table~\ref{tab:extensions} and Figure~\ref{fig:extensions}
17 17 \begin{table}[tp]\centering\scriptsize
18 18 \caption{Post-registration extensions (exploratory): pooled QLIKE ratios to GARCH(1,1)-normal, test period, columns as in
19 19 Table~\ref{tab:pooledA}. Each extension should be read against its parent in Tables~\ref{tab:pooledA}--\ref{tab:pooledB}.}\label{tab:extensions}
20 −\input{tables/tab_pooled_extensions}
20 +\adjustbox{max width=\textwidth}{\input{tables/tab_pooled_extensions}}
21 21 \end{table}
22 22
23 23 \begin{figure}[tp]\centering
modified paper/sections/results_heterogeneity.tex +1 −1
@@ -53,7 +53,7 @@ headline statements: nothing material within the daily tier, a stable 12--15\% f
53 53 instrument-level pairwise ratios; number of instruments in parentheses). Terciles of the trailing 22-day realized variance
54 54 at the origin (thresholds from the validation period); sign of the previous day's return; calendar episodes fixed in the
55 55 protocol (the 2025 tariff episode has too few origins per instrument and is reported in Appendix~\ref{app:extra}).}\label{tab:regimes}
56 −\input{tables/tab_regimes_h1}
56 +\adjustbox{max width=\textwidth}{\input{tables/tab_regimes_h1}}
57 57 \end{table}
58 58
59 59 \begin{figure}[tp]\centering
modified paper/sections/results_realized.tex +1 −1
@@ -44,7 +44,7 @@ cryptocurrencies, whose one-minute data are noisy even after 2022 (Section~\ref{
44 44 \begin{table}[tp]\centering\scriptsize
45 45 \caption{Tier B, pooled QLIKE ratios to GARCH(1,1)-normal (test period, pairwise samples, primary proxy). Models use the lagged
46 46 full-day realized variance in addition to daily returns (larger information set than Tier A). Columns as in Table~\ref{tab:pooledA}.}\label{tab:pooledB}
47 −\input{tables/tab_pooled_tierB}
47 +\adjustbox{max width=\textwidth}{\input{tables/tab_pooled_tierB}}
48 48 \end{table}
49 49
50 50 \begin{figure}[tp]\centering
modified paper/sections/results_tail_body.tex +1 −1
@@ -22,7 +22,7 @@ calibrated unconditionally but fail the dynamic test on two thirds of the instru
22 22 hit \% = median across instruments of the realized exceedance rate; UC / CC / DQ rej.\ \% = share of instruments on which
23 23 the Kupiec, Christoffersen and Engle--Manganelli tests reject at 5\%; FZ0 ratio = mean across instruments of the FZ0 joint
24 24 VaR--ES loss relative to GARCH(1,1)-$t$ (origins with a degenerate ES are excluded and counted in Appendix~\ref{app:extra}).}\label{tab:tailrisk}
25 −\input{tables/tab_tailrisk}
25 +\adjustbox{max width=\textwidth}{\input{tables/tab_tailrisk}}
26 26 \end{table}
27 27
28 28 \begin{figure}[tp]\centering
modified paper/sections/results_tail_multivariate.tex +1 −1
@@ -25,5 +25,5 @@ two seconds---is modest.
25 25 \caption{Tier C: one-step covariance forecasts, test period 2016--2026. Ratios are relative to CCC-GARCH(1,1); stars mark
26 26 Diebold--Mariano significance versus CCC (*** 1\%, ** 5\%, * 10\%); MCS $p$ from the 90\% model confidence set over the six
27 27 models (multivariate QLIKE); refit time is the median wall time of one re-estimation.}\label{tab:tierc}
28 −\input{tables/tab_tier_c}
28 +\adjustbox{max width=\textwidth}{\input{tables/tab_tier_c}}
29 29 \end{table}
modified src/wp46/reporting/plotstyle.py +145 −19
@@ -1,40 +1,166 @@
1 −"""Journal figure style for WP46 (matplotlib, vector PDF). Categorical colours = the validated reference palette of the
2 −dataviz method (fixed slot order, never cycled), assigned by model FAMILY (identity), with distinct markers/linestyles so
3 −that identity never rests on colour alone (print, CVD). Sequential = single hue (blue) light->dark; diverging = blue/red
4 −with a neutral grey midpoint. Text always in ink colours; thin marks; recessive grid; no truncated axes."""
1 +"""Journal figure style for WP46 (matplotlib, vector PDF, Times/newtx-compatible serif).
2 +
3 +Rules (docs/EXHIBITS_MANDATE.md, dataviz skill):
4 +* categorical colours = the validated reference palette (fixed slot order, never cycled), assigned by model FAMILY;
5 + identity never rests on colour alone: every family also has a marker, a line style and a hatch;
6 +* sequential = one hue (blue, light -> dark); diverging = blue / neutral grey / red with the ratio 1.0 at the midpoint
7 + and a flat grey band for the +-2 % materiality zone so that the band is visible in the colour bar itself;
8 +* all text in ink colours (never the series colour); thin marks; recessive solid hairline grid; fonts >= 7 pt at print
9 + size (figures are built at their final width: W1 = 3.25 in column, W2 = 6.5 in text width);
10 +* deterministic PDF metadata (no timestamps).
11 +"""
5 12 from __future__ import annotations
6 13
7 14 import matplotlib as mpl
8 15 import matplotlib.pyplot as plt
9 −from matplotlib.colors import LinearSegmentedColormap
16 +import numpy as np
17 +from matplotlib.colors import LinearSegmentedColormap, to_rgb
18 +from matplotlib.lines import Line2D
19 +from matplotlib.patches import Patch
10 20
21 +# --- validated reference palette (scripts/validate_palette.js: adjacent pairs PASS on #ffffff; contrast WARN on slots 3/4/5
22 +# --- is relieved by direct text labels / tables, as every family mark sits beside an ink label)
11 23 SLOTS = ["#2a78d6", "#eb6834", "#1baf7a", "#eda100", "#e87ba4", "#008300", "#6250d6", "#e34948"]
24 +FAMILY_ORDER = ["baseline", "classical", "asymmetric", "power", "fractional", "component", "regime-switching", "score-driven", "realized"]
12 25 FAMILY_COLOR = {"classical": SLOTS[0], "asymmetric": SLOTS[1], "power": SLOTS[2], "fractional": SLOTS[3], "component": SLOTS[4],
13 26 "regime-switching": SLOTS[5], "score-driven": SLOTS[6], "realized": SLOTS[7], "baseline": "#7a7975", "multivariate": SLOTS[0]}
14 27 FAMILY_MARKER = {"classical": "o", "asymmetric": "s", "power": "^", "fractional": "D", "component": "v", "regime-switching": "P",
15 − "score-driven": "X", "realized": "*", "baseline": "x"}
16 −FAMILY_LABEL = {"classical": "Classical GARCH", "asymmetric": "Asymmetric", "power": "Power/absolute", "fractional": "Fractional",
28 + "score-driven": "X", "realized": "*", "baseline": "x", "multivariate": "<"}
29 +FAMILY_LS = {"classical": "-", "asymmetric": "--", "power": "-.", "fractional": ":", "component": (0, (5, 1, 1, 1)),
30 + "regime-switching": (0, (3, 1)), "score-driven": (0, (1, 1)), "realized": "-", "baseline": (0, (6, 2))}
31 +FAMILY_HATCH = {"classical": "", "asymmetric": "////", "power": "....", "fractional": "\\\\\\\\", "component": "xx",
32 + "regime-switching": "++", "score-driven": "oo", "realized": "**", "baseline": "--"}
33 +FAMILY_LABEL = {"classical": "Classical GARCH", "asymmetric": "Asymmetric", "power": "Power / absolute", "fractional": "Fractional",
17 34 "component": "Component", "regime-switching": "Regime switching", "score-driven": "Score-driven", "realized": "Realized-measure",
18 35 "baseline": "Baselines"}
19 −INK, INK2, INK3 = "#0b0b0b", "#52514e", "#8a8984"
20 −SURFACE = "#ffffff"
21 −SEQ = LinearSegmentedColormap.from_list("wp46_seq", ["#e8f0fb", "#2a78d6", "#0d2f5c"])
22 −DIV = LinearSegmentedColormap.from_list("wp46_div", ["#0d3f8f", "#2a78d6", "#f2f2f0", "#e34948", "#7a1716"])
23 −W1, W2 = 3.25, 6.5 # column widths in inches
36 +FAMILY_SHORT = {"classical": "Classical", "asymmetric": "Asymmetric", "power": "Power", "fractional": "Fractional", "component": "Component",
37 + "regime-switching": "Regime", "score-driven": "Score", "realized": "Realized", "baseline": "Baselines"}
38 +# asset classes: five adjacent blocks (adjacent-pair validation); each block also carries a text label, never colour alone
39 +CLASS_COLOR = {"etf": SLOTS[0], "stock": SLOTS[1], "futures": SLOTS[2], "fx": SLOTS[3], "crypto": SLOTS[4]}
40 +CLASS_LS = {"etf": "-", "stock": "--", "futures": "-.", "fx": ":", "crypto": (0, (5, 1, 1, 1))}
41 +
42 +INK, INK2, INK3 = "#0b0b0b", "#52514e", "#898781"
43 +GRID, BAND, SURFACE = "#e1e0d9", "#f0efec", "#ffffff"
44 +NEUTRAL = "#e9e8e3" # diverging midpoint / materiality band
45 +SEQ = LinearSegmentedColormap.from_list("wp46_seq", ["#e6eef9", "#9ec5f4", "#3987e5", "#1c5cab", "#0d366b"])
46 +W1, W2 = 3.25, 6.5 # column width / text width in inches
47 +MIN_FONT = 7.0
24 48
25 49
26 50 def apply() -> None:
27 51 mpl.rcParams.update({
28 − "font.family": "serif", "font.serif": ["STIX Two Text", "Times New Roman", "Times", "DejaVu Serif"], "mathtext.fontset": "stix",
29 − "font.size": 8.5, "axes.labelsize": 8.5, "axes.titlesize": 9, "legend.fontsize": 7.5, "xtick.labelsize": 7.5, "ytick.labelsize": 7.5,
52 + "font.family": "serif", "font.serif": ["Times New Roman", "Times", "STIX Two Text", "DejaVu Serif"], "mathtext.fontset": "stix",
53 + "font.size": 8, "axes.labelsize": 8, "axes.titlesize": 8.5, "legend.fontsize": 7, "xtick.labelsize": 7, "ytick.labelsize": 7,
30 54 "axes.edgecolor": INK3, "axes.linewidth": 0.6, "axes.spines.top": False, "axes.spines.right": False,
31 − "axes.grid": True, "grid.color": "#e3e2de", "grid.linewidth": 0.5, "axes.axisbelow": True,
32 − "xtick.color": INK2, "ytick.color": INK2, "text.color": INK, "axes.labelcolor": INK,
33 − "lines.linewidth": 1.2, "lines.markersize": 4, "legend.frameon": False, "figure.dpi": 150, "savefig.dpi": 300,
34 − "pdf.fonttype": 42, "ps.fonttype": 42, "figure.facecolor": SURFACE, "axes.facecolor": SURFACE,
55 + "axes.grid": True, "grid.color": GRID, "grid.linewidth": 0.5, "grid.linestyle": "-", "axes.axisbelow": True,
56 + "xtick.color": INK2, "ytick.color": INK2, "xtick.labelcolor": INK, "ytick.labelcolor": INK, "text.color": INK, "axes.labelcolor": INK,
57 + "xtick.major.size": 2.5, "ytick.major.size": 2.5, "xtick.major.width": 0.5, "ytick.major.width": 0.5,
58 + "lines.linewidth": 1.1, "lines.markersize": 3.8, "lines.markeredgewidth": 0.6, "legend.frameon": False, "legend.handlelength": 1.8,
59 + "figure.dpi": 150, "savefig.dpi": 300, "pdf.fonttype": 42, "ps.fonttype": 42, "figure.facecolor": SURFACE, "axes.facecolor": SURFACE,
60 + "hatch.linewidth": 0.4, "hatch.color": SURFACE, "axes.titlelocation": "left", "axes.titleweight": "bold", "axes.titlepad": 4,
35 61 })
36 62
37 63
38 64 def save(fig, path, **kw) -> None:
39 − fig.savefig(path, bbox_inches="tight", metadata={"CreationDate": None, "Creator": "wp46"}, **kw)
65 + """Vector PDF with deterministic metadata (no timestamps); closes the figure."""
66 + fig.savefig(path, bbox_inches="tight", pad_inches=0.02,
67 + metadata={"CreationDate": None, "ModDate": None, "Creator": "wp46 build_paper.py", "Producer": "wp46 (matplotlib)"}, **kw)
40 68 plt.close(fig)
69 +
70 +
71 +# ----------------------------------------------------------------------------- diverging map with a flat materiality band
72 +def div_cmap(vmin: float = 0.8, vmax: float = 1.25, band: float = 0.02, n: int = 512) -> LinearSegmentedColormap:
73 + """Blue (ratio < 1, better than GARCH) / neutral / red (ratio > 1) on the log scale, with a FLAT neutral segment covering
74 + [1-band, 1+band] so that the pre-registered +-2 % materiality zone reads as 'no difference' both in the cells and in the bar."""
75 + lo, hi = np.log(vmin), np.log(vmax)
76 + blo, bhi = np.log(1 - band), np.log(1 + band)
77 + blue = LinearSegmentedColormap.from_list("b", ["#0d366b", "#1c5cab", "#3987e5", "#9ec5f4", NEUTRAL])
78 + red = LinearSegmentedColormap.from_list("r", [NEUTRAL, "#f2a9a8", "#e34948", "#b02a2a", "#6e1413"])
79 + xs = np.linspace(lo, hi, n)
80 + cols = []
81 + for x in xs:
82 + if x <= blo:
83 + cols.append(blue((x - lo) / (blo - lo)))
84 + elif x >= bhi:
85 + cols.append(red((x - bhi) / (hi - bhi)))
86 + else:
87 + cols.append(to_rgb(NEUTRAL) + (1.0,))
88 + cm = LinearSegmentedColormap.from_list("wp46_div", cols, N=n)
89 + cm.set_bad("#ffffff")
90 + return cm
91 +
92 +
93 +DIV = div_cmap()
94 +
95 +
96 +def ratio_colorbar(fig, im, ax, vmin=0.8, vmax=1.25, band=0.02, label="QLIKE ratio to GARCH(1,1)-N (log scale)", orientation="horizontal", **kw):
97 + """Colour bar for a log-ratio image: ticks at round ratios, the +-band zone outlined, text in ink."""
98 + cb = fig.colorbar(im, ax=ax, orientation=orientation, **kw)
99 + ticks = [t for t in (0.7, 0.8, 0.9, 1 - band, 1.0, 1 + band, 1.1, 1.25, 1.4) if vmin <= t <= vmax]
100 + cb.set_ticks(np.log(ticks)); cb.set_ticklabels([f"{t:.2f}" for t in ticks])
101 + cb.ax.tick_params(labelsize=MIN_FONT, length=2, width=0.5, color=INK2)
102 + for t in (np.log(1 - band), np.log(1 + band)):
103 + (cb.ax.axvline if orientation == "horizontal" else cb.ax.axhline)(t, color=INK, lw=0.6)
104 + cb.set_label(label + f"; grey band = ±{100 * band:.0f} % materiality zone", fontsize=MIN_FONT, color=INK)
105 + cb.outline.set_edgecolor(INK3); cb.outline.set_linewidth(0.5)
106 + return cb
107 +
108 +
109 +# ----------------------------------------------------------------------------- shared annotations
110 +def tag(ax, text: str = "exploratory", loc: str = "upper right") -> None:
111 + """Grey tag separating post-registration / exploratory exhibits from the confirmatory ones (mandate §5)."""
112 + x, ha = (0.995, "right") if "right" in loc else (0.005, "left")
113 + y, va = (1.0, "bottom") if "upper" in loc else (0.0, "top")
114 + ax.text(x, y + (0.01 if va == "bottom" else -0.01), text, transform=ax.transAxes, ha=ha, va=va, fontsize=MIN_FONT, color=INK2,
115 + bbox=dict(boxstyle="round,pad=0.25", fc=BAND, ec="none"))
116 +
117 +
118 +def ref_band(ax, axis: str = "y", centre: float = 1.0, band: float = 0.02, log: bool = False) -> None:
119 + """Reference line at `centre` and the +-band materiality zone (shaded) on the given axis."""
120 + lo, hi = (centre * (1 - band), centre * (1 + band)) if not log else (np.log(1 - band), np.log(1 + band))
121 + c = centre if not log else 0.0
122 + if axis == "y":
123 + ax.axhspan(lo, hi, color=BAND, zorder=0, lw=0); ax.axhline(c, color=INK2, lw=0.6, zorder=1)
124 + else:
125 + ax.axvspan(lo, hi, color=BAND, zorder=0, lw=0); ax.axvline(c, color=INK2, lw=0.6, zorder=1)
126 +
127 +
128 +def family_handles(families, with_hatch: bool = False, kind: str = "marker"):
129 + """Legend handles with the family's colour + marker (or hatch) so that the legend survives grayscale."""
130 + hs = []
131 + for f in families:
132 + if kind == "patch":
133 + hs.append(Patch(fc=FAMILY_COLOR.get(f, INK3), ec=SURFACE, hatch=FAMILY_HATCH.get(f, "") if with_hatch else "", label=FAMILY_LABEL.get(f, f)))
134 + else:
135 + hs.append(Line2D([], [], marker=FAMILY_MARKER.get(f, "o"), color=FAMILY_COLOR.get(f, INK3), ls=FAMILY_LS.get(f, "-") if kind == "line" else "none",
136 + ms=4.2, mew=0.6, mec=FAMILY_COLOR.get(f, INK3), label=FAMILY_LABEL.get(f, f)))
137 + return hs
138 +
139 +
140 +def luminance(rgba) -> float:
141 + r, g, b = to_rgb(rgba)
142 + return 0.2126 * r + 0.7152 * g + 0.0722 * b
143 +
144 +
145 +def spread_labels(ys: list[float], min_gap: float, lo: float | None = None, hi: float | None = None) -> list[float]:
146 + """Push label positions apart (1-D) so that consecutive labels are at least `min_gap` apart; order preserved."""
147 + order = np.argsort(ys); out = np.array(ys, dtype=float)
148 + for k in range(1, len(order)):
149 + i, j = order[k - 1], order[k]
150 + if out[j] - out[i] < min_gap:
151 + out[j] = out[i] + min_gap
152 + if hi is not None and len(out) and out[order[-1]] > hi:
153 + shift = out[order[-1]] - hi
154 + for k in range(len(order) - 1, -1, -1):
155 + i = order[k]
156 + out[i] -= shift
157 + if k and out[i] - out[order[k - 1]] >= min_gap:
158 + break
159 + if lo is not None:
160 + for k in range(len(order)):
161 + i = order[k]
162 + if out[i] < lo:
163 + out[i] = lo
164 + if k and out[i] - out[order[k - 1]] < min_gap:
165 + out[i] = out[order[k - 1]] + min_gap
166 + return out.tolist()
41 167
Menu

Theme