SPB Git

spb/wp39_uqo

Public

WP39 v1.0: journal-style figures and Times math typography (newtxmath, notation macros, displayed definitions)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Nov 2, 2026) parent 42edd96 Browse files

36 changed files +358 −164

modified PROGRESS.md +10 −0
@@ -66,3 +66,13 @@
66 66 - `09_make_manuscript`: 15 LaTeX tables, 9 figures (validated palette), 206 macros in `paper/results_macros.tex`
67 67 (fixes: NaN subgroup in figure, LaTeX escaping of math labels, Unicode in bib). `paper/main.pdf` compiled with
68 68 `latexmk` on the laptop: 33 pages, 0 undefined references/citations.
69 +
70 +## Typography and figure pass (2026-11-02 16:30–17:00)
71 +- Paper: `newtxtext` + `newtxmath` (Times math with proper metrics), `mathtools`, `bm`, `dsfont`; notation macros
72 + (`\E`, `\ind{}`, `\Info`, `\Acc`, `\Loss`, `\LFDS`, `\RV`, `\BPV`, `\RS`, `\TA`, `\dlog`); displayed definitions for the
73 + surprise and its seasonal expectation (eq. 1), the log loss (eq. 2), the information set (eq. 3), the realized-measure
74 + families (eq. 4), the panel regression and the Giacomini–White statistic; author names normalised in the bibliography.
75 +- Figures regenerated in journal style (`src/wp39/figures.py`): Times/STIX typography, monochrome marks distinguished by
76 + shape and line style with a single restrained accent for the 1-minute series, Panel A/B labels, inward ticks, no grid,
77 + journal widths (3.25 / 6.5 in), redesigned timeline schematic, ordinal horizon axis. Tables: grouped rules by family.
78 +- `paper/main.pdf`: 34 pages, 0 undefined references.
modified literature/references.bib +13 −13
@@ -198,7 +198,7 @@
198 198 }
199 199
200 200 @article{chen2022,
201 − author = {CHEN, XI and CHO, YANG HA (TONY) and DOU, YIWEI and LEV, BARUCH},
201 + author = {Chen, XI and Cho, Yang Ha (tony) and Dou, Yiwei and Lev, Baruch},
202 202 title = {{Predicting Future Earnings Changes Using Machine Learning and Detailed Financial Data}},
203 203 journal = {Journal of Accounting Research},
204 204 year = {2022},
@@ -219,7 +219,7 @@
219 219 }
220 220
221 221 @article{hirshleifer2009,
222 − author = {HIRSHLEIFER, DAVID and LIM, SONYA SEONGYEON and TEOH, SIEW HONG},
222 + author = {Hirshleifer, David and Lim, Sonya Seongyeon and Teoh, Siew Hong},
223 223 title = {{Driven to Distraction: Extraneous Events and Underreaction to Earnings News}},
224 224 journal = {The Journal of Finance},
225 225 year = {2009},
@@ -230,7 +230,7 @@
230 230 }
231 231
232 232 @article{dellavigna2009,
233 − author = {DELLAVIGNA, STEFANO and POLLET, JOSHUA M.},
233 + author = {DellaVigna, Stefano and Pollet, JOSHUA M.},
234 234 title = {{Investor Inattention and Friday Earnings Announcements}},
235 235 journal = {The Journal of Finance},
236 236 year = {2009},
@@ -241,7 +241,7 @@
241 241 }
242 242
243 243 @article{loughran2011,
244 − author = {LOUGHRAN, TIM and MCDONALD, BILL},
244 + author = {Loughran, Tim and McDonald, Bill},
245 245 title = {{When Is a Liability Not a Liability? Textual Analysis, Dictionaries, and 10-Ks}},
246 246 journal = {The Journal of Finance},
247 247 year = {2011},
@@ -285,7 +285,7 @@
285 285 }
286 286
287 287 @article{hendershott2011,
288 − author = {HENDERSHOTT, TERRENCE and JONES, CHARLES M. and MENKVELD, ALBERT J.},
288 + author = {Hendershott, Terrence and Jones, CHARLES M. and Menkveld, ALBERT J.},
289 289 title = {{Does Algorithmic Trading Improve Liquidity?}},
290 290 journal = {The Journal of Finance},
291 291 year = {2011},
@@ -318,7 +318,7 @@
318 318 }
319 319
320 320 @article{campbell2008,
321 − author = {CAMPBELL, JOHN Y. and HILSCHER, JENS and SZILAGYI, JAN},
321 + author = {Campbell, JOHN Y. and Hilscher, Jens and Szilagyi, Jan},
322 322 title = {{In Search of Distress Risk}},
323 323 journal = {The Journal of Finance},
324 324 year = {2008},
@@ -505,7 +505,7 @@
505 505 }
506 506
507 507 @article{bollerslev2011tails,
508 − author = {BOLLERSLEV, TIM and TODOROV, VIKTOR},
508 + author = {Bollerslev, Tim and Todorov, Viktor},
509 509 title = {{Tails, Fears, and Risk Premia}},
510 510 journal = {The Journal of Finance},
511 511 year = {2011},
@@ -527,7 +527,7 @@
527 527 }
528 528
529 529 @article{savor2016,
530 − author = {SAVOR, PAVEL and WILSON, MUNGO},
530 + author = {Savor, Pavel and Wilson, Mungo},
531 531 title = {{Earnings Announcements and Systematic Risk}},
532 532 journal = {The Journal of Finance},
533 533 year = {2016},
@@ -571,7 +571,7 @@
571 571 }
572 572
573 573 @article{brogaard2019,
574 − author = {BROGAARD, JONATHAN and HENDERSHOTT, TERRENCE and RIORDAN, RYAN},
574 + author = {Brogaard, Jonathan and Hendershott, Terrence and Riordan, Ryan},
575 575 title = {{Price Discovery without Trading: Evidence from Limit Orders}},
576 576 journal = {The Journal of Finance},
577 577 year = {2019},
@@ -582,7 +582,7 @@
582 582 }
583 583
584 584 @article{ang2006,
585 − author = {ANG, ANDREW and HODRICK, ROBERT J. and XING, YUHANG and ZHANG, XIAOYAN},
585 + author = {Ang, Andrew and Hodrick, ROBERT J. and Xing, Yuhang and Zhang, Xiaoyan},
586 586 title = {{The Cross-Section of Volatility and Expected Returns}},
587 587 journal = {The Journal of Finance},
588 588 year = {2006},
@@ -626,7 +626,7 @@
626 626 }
627 627
628 628 @article{fang2009,
629 − author = {FANG, LILY and PERESS, JOEL},
629 + author = {Fang, Lily and Peress, Joel},
630 630 title = {{Media Coverage and the Cross-section of Stock Returns}},
631 631 journal = {The Journal of Finance},
632 632 year = {2009},
@@ -714,7 +714,7 @@
714 714 }
715 715
716 716 @article{kaniel2012,
717 − author = {KANIEL, RON and LIU, SHUMING and SAAR, GIDEON and TITMAN, SHERIDAN},
717 + author = {Kaniel, Ron and Liu, Shuming and Saar, Gideon and Titman, Sheridan},
718 718 title = {{Individual Investor Trading and Return Patterns around Earnings Announcements}},
719 719 journal = {The Journal of Finance},
720 720 year = {2012},
@@ -845,7 +845,7 @@
845 845 }
846 846
847 847 @article{vankervel2019,
848 − author = {VAN KERVEL, VINCENT and MENKVELD, ALBERT J.},
848 + author = {van Kervel, Vincent and Menkveld, ALBERT J.},
849 849 title = {{High-Frequency Trading around Large Institutional Orders}},
850 850 journal = {The Journal of Finance},
851 851 year = {2019},
modified paper/appendix/appendix.tex +4 −4
@@ -11,10 +11,10 @@ Variable & Definition & Source / step \\
11 11 \midrule
12 12 $A_{i,q}$ & Earnings-release date: first \texttt{earnings\_events.event\_date} in $[E_q, F_q+10\,\mathrm{d}]$ & FinData Postgres extract (01a) \\
13 13 $F_{i,q}$, $V_{i,q}$ & SEC filing date; availability $=F+1$ day 00:00 ET & \texttt{/fundamentals} (02) \\
14 −$S_{i,q}$ & $(X_q-\hat X_q)/TA_{q-1}$, $X=$ operating income, $\hat X_q=X_{q-4}+\frac14\sum_{k=0}^3(X_{q-1-k}-X_{q-5-k})$ & 02, \texttt{labels.py} \\
15 −$D_{i,q}$ & $\mathbb 1[S_{i,q}\le-0.005]$ & 02 \\
16 −Secondary targets & net-income surprise; $\Delta$ROA$_{yoy}\le-0.01$; $(CFO_q-CFO_{q-4})/TA\le-0.01$; current ratio $-10\%$ or coverage $<1$ from $\ge1$; $|S|\ge0.005$; EPS $<$ estimate & 02 \\
17 −Intraday families (per bar length $f\in\{1,5,30\}$ min) & RV $=\sum r^2$; BPV $=\frac{\pi}{2}\sum|r_t||r_{t-1}|$; RJ $=\max(0,\mathrm{RV}-\mathrm{BPV})/\mathrm{RV}$; RS$^-$/RV; SJV $=(\mathrm{RS}^+-\mathrm{RS}^-)/\mathrm{RV}$; realized skewness/kurtosis; $\max r^2/\mathrm{RV}$; AC(1); drift $=\ln(C/O)$; opening/closing 30-min volume shares; volume Gini; down-bar volume share; present-bar share; RV share on SPY-down bars; $\beta=\sum r\,r^{SPY}/\sum (r^{SPY})^2$; vol-of-vol; RV/Var(daily) & 03, \texttt{features.py}, \texttt{samples.py} \\
14 +$S_{i,q}$ & $(X_q-\hat X_q)/\TA_{q-1}$, $X=$ operating income, $\hat X_q=X_{q-4}+\tfrac14\sum_{k=0}^{3}(X_{q-1-k}-X_{q-5-k})$ & 02, \texttt{labels.py} \\
15 +$D_{i,q}$ & $\ind{S_{i,q}\le-0.005}$ & 02 \\
16 +Secondary targets & net-income surprise; $\Delta$ROA$_{yoy}\le-0.01$; $(\mathit{CFO}_q-\mathit{CFO}_{q-4})/\TA\le-0.01$; current ratio $-10\%$ or coverage $<1$ from $\ge1$; $\abs{S}\ge0.005$; EPS $<$ estimate & 02 \\
17 +Intraday families (per bar length $f\in\set{1,5,30}$ min) & $\RV=\sum_t r_t^2$; $\BPV=\tfrac{\pi}{2}\sum_t\abs{r_t}\abs{r_{t-1}}$; $\mathrm{RJ}=\max(0,\RV-\BPV)/\RV$; $\RS^-/\RV$; $\mathrm{SJV}=(\RS^+-\RS^-)/\RV$; realized skewness/kurtosis; $\max_t r_t^2/\RV$; AC(1); drift $=\log(C/O)$; opening/closing 30-min volume shares; volume Gini; down-bar volume share; present-bar share; $\RV$ share on SPY-down bars; $\hat\beta=\sum_t r_t\,r^{\mathrm{SPY}}_t/\sum_t (r^{\mathrm{SPY}}_t)^2$; vol-of-vol; $\overline{\RV}/\operatorname{Var}(r^{\mathrm{daily}})$ & 03, \texttt{features.py}, \texttt{samples.py} \\
18 18 Daily block & 20/60-day log returns, SD, Parkinson, skewness, kurtosis, overnight/intraday sums, volume ratios (20d, 5d vs 60d median), Amihud (60d), zero-return share, log dollar volume, max drawdown (60d), bars per day & 03 \\
19 19 Accounting block & gross/operating margin, NI/TA, CFO/TA, accruals, cash/TA, debt/TA, current ratio, capex/TA, R\&D/revenue, coverage, loss dummy, log assets, ROA, revenue growth, net issuance, log market cap, last surprise, mean of last four surprises, trailing adverse rate, information age & 02--04 \\
20 20 Returns & split- and dividend-adjusted close-to-close from raw bars and \texttt{corporate\_actions} (ex-date rule) & 03 \\
modified paper/figures/fig01_timeline.pdf +0 −0

Binary file not shown.

modified paper/figures/fig01_timeline.png +0 −0

Binary file not shown.

modified paper/figures/fig02_coverage.pdf +0 −0

Binary file not shown.

modified paper/figures/fig02_coverage.png +0 −0

Binary file not shown.

modified paper/figures/fig03_prevalence.pdf +0 −0

Binary file not shown.

modified paper/figures/fig03_prevalence.png +0 −0

Binary file not shown.

modified paper/figures/fig04_frequency_gains.pdf +0 −0

Binary file not shown.

modified paper/figures/fig04_frequency_gains.png +0 −0

Binary file not shown.

modified paper/figures/fig05_reliability.pdf +0 −0

Binary file not shown.

modified paper/figures/fig05_reliability.png +0 −0

Binary file not shown.

modified paper/figures/fig06_signal_evolution.pdf +0 −0

Binary file not shown.

modified paper/figures/fig06_signal_evolution.png +0 −0

Binary file not shown.

modified paper/figures/fig07_heterogeneity.pdf +0 −0

Binary file not shown.

modified paper/figures/fig07_heterogeneity.png +0 −0

Binary file not shown.

modified paper/figures/fig08_rolling.pdf +0 −0

Binary file not shown.

modified paper/figures/fig08_rolling.png +0 −0

Binary file not shown.

modified paper/figures/fig09_economic.pdf +0 −0

Binary file not shown.

modified paper/figures/fig09_economic.png +0 −0

Binary file not shown.

modified paper/main.pdf +0 −0

Binary file not shown.

modified paper/main.tex +28 −2
@@ -17,14 +17,21 @@
17 17 right=1in
18 18 ]{geometry}
19 19
20 +% --- Math (amsthm must precede newtxmath) ---
21 +\usepackage{amsmath,amsthm}
22 +\usepackage{mathtools}
23 +\allowdisplaybreaks
24 +
20 25 % --- Typography ---
21 −\usepackage{mathptmx} % Times-like font (text + math)
26 +\usepackage{newtxtext} % Times text
27 +\usepackage[varvw,cmintegrals,cmbraces]{newtxmath} % matching Times math with proper metrics
28 +\usepackage{bm} % bold math for vectors
29 +\usepackage{dsfont} % \mathds{1} indicator
22 30 \usepackage{microtype} % Improved typographic refinements
23 31 \usepackage{setspace}
24 32 \onehalfspacing % 1.5 line spacing
25 33
26 34 % --- Math ---
27 −\usepackage{amsmath,amssymb,amsthm}
28 35
29 36 % --- Tables ---
30 37 \usepackage{booktabs}
@@ -116,6 +123,25 @@
116 123 Gatineau, Québec, Canada J9A 1L8%
117 124 }
118 125
126 +% --- Notation macros (consistent symbols throughout) ---
127 +\newcommand{\E}{\mathbb{E}}
128 +\newcommand{\Prob}{\mathbb{P}}
129 +\newcommand{\ind}[1]{\mathds{1}\!\left[#1\right]}
130 +\newcommand{\Info}{\mathcal{I}}
131 +\newcommand{\Acc}{\mathcal{A}}
132 +\newcommand{\Loss}{\mathcal{L}}
133 +\newcommand{\LFDS}{\mathrm{LFDS}}
134 +\newcommand{\RV}{\mathrm{RV}}
135 +\newcommand{\BPV}{\mathrm{BPV}}
136 +\newcommand{\RS}{\mathrm{RS}}
137 +\newcommand{\TA}{\mathit{TA}}
138 +\newcommand{\dlog}{\Delta\Loss}
139 +\newcommand{\set}[1]{\left\{#1\right\}}
140 +\newcommand{\abs}[1]{\left\lvert#1\right\rvert}
141 +\newcommand{\tr}{^{\mkern-1.5mu\mathsf{T}}}
142 +\newcommand{\bgamma}{\bm{\gamma}}
143 +\newcommand{\bX}{\bm{X}}
144 +
119 145 % ============================================================================
120 146 % DOCUMENT
121 147 % ============================================================================
modified paper/references.bib +13 −13
@@ -198,7 +198,7 @@
198 198 }
199 199
200 200 @article{chen2022,
201 − author = {CHEN, XI and CHO, YANG HA (TONY) and DOU, YIWEI and LEV, BARUCH},
201 + author = {Chen, XI and Cho, Yang Ha (tony) and Dou, Yiwei and Lev, Baruch},
202 202 title = {{Predicting Future Earnings Changes Using Machine Learning and Detailed Financial Data}},
203 203 journal = {Journal of Accounting Research},
204 204 year = {2022},
@@ -219,7 +219,7 @@
219 219 }
220 220
221 221 @article{hirshleifer2009,
222 − author = {HIRSHLEIFER, DAVID and LIM, SONYA SEONGYEON and TEOH, SIEW HONG},
222 + author = {Hirshleifer, David and Lim, Sonya Seongyeon and Teoh, Siew Hong},
223 223 title = {{Driven to Distraction: Extraneous Events and Underreaction to Earnings News}},
224 224 journal = {The Journal of Finance},
225 225 year = {2009},
@@ -230,7 +230,7 @@
230 230 }
231 231
232 232 @article{dellavigna2009,
233 − author = {DELLAVIGNA, STEFANO and POLLET, JOSHUA M.},
233 + author = {DellaVigna, Stefano and Pollet, JOSHUA M.},
234 234 title = {{Investor Inattention and Friday Earnings Announcements}},
235 235 journal = {The Journal of Finance},
236 236 year = {2009},
@@ -241,7 +241,7 @@
241 241 }
242 242
243 243 @article{loughran2011,
244 − author = {LOUGHRAN, TIM and MCDONALD, BILL},
244 + author = {Loughran, Tim and McDonald, Bill},
245 245 title = {{When Is a Liability Not a Liability? Textual Analysis, Dictionaries, and 10-Ks}},
246 246 journal = {The Journal of Finance},
247 247 year = {2011},
@@ -285,7 +285,7 @@
285 285 }
286 286
287 287 @article{hendershott2011,
288 − author = {HENDERSHOTT, TERRENCE and JONES, CHARLES M. and MENKVELD, ALBERT J.},
288 + author = {Hendershott, Terrence and Jones, CHARLES M. and Menkveld, ALBERT J.},
289 289 title = {{Does Algorithmic Trading Improve Liquidity?}},
290 290 journal = {The Journal of Finance},
291 291 year = {2011},
@@ -318,7 +318,7 @@
318 318 }
319 319
320 320 @article{campbell2008,
321 − author = {CAMPBELL, JOHN Y. and HILSCHER, JENS and SZILAGYI, JAN},
321 + author = {Campbell, JOHN Y. and Hilscher, Jens and Szilagyi, Jan},
322 322 title = {{In Search of Distress Risk}},
323 323 journal = {The Journal of Finance},
324 324 year = {2008},
@@ -505,7 +505,7 @@
505 505 }
506 506
507 507 @article{bollerslev2011tails,
508 − author = {BOLLERSLEV, TIM and TODOROV, VIKTOR},
508 + author = {Bollerslev, Tim and Todorov, Viktor},
509 509 title = {{Tails, Fears, and Risk Premia}},
510 510 journal = {The Journal of Finance},
511 511 year = {2011},
@@ -527,7 +527,7 @@
527 527 }
528 528
529 529 @article{savor2016,
530 − author = {SAVOR, PAVEL and WILSON, MUNGO},
530 + author = {Savor, Pavel and Wilson, Mungo},
531 531 title = {{Earnings Announcements and Systematic Risk}},
532 532 journal = {The Journal of Finance},
533 533 year = {2016},
@@ -571,7 +571,7 @@
571 571 }
572 572
573 573 @article{brogaard2019,
574 − author = {BROGAARD, JONATHAN and HENDERSHOTT, TERRENCE and RIORDAN, RYAN},
574 + author = {Brogaard, Jonathan and Hendershott, Terrence and Riordan, Ryan},
575 575 title = {{Price Discovery without Trading: Evidence from Limit Orders}},
576 576 journal = {The Journal of Finance},
577 577 year = {2019},
@@ -582,7 +582,7 @@
582 582 }
583 583
584 584 @article{ang2006,
585 − author = {ANG, ANDREW and HODRICK, ROBERT J. and XING, YUHANG and ZHANG, XIAOYAN},
585 + author = {Ang, Andrew and Hodrick, ROBERT J. and Xing, Yuhang and Zhang, Xiaoyan},
586 586 title = {{The Cross-Section of Volatility and Expected Returns}},
587 587 journal = {The Journal of Finance},
588 588 year = {2006},
@@ -626,7 +626,7 @@
626 626 }
627 627
628 628 @article{fang2009,
629 − author = {FANG, LILY and PERESS, JOEL},
629 + author = {Fang, Lily and Peress, Joel},
630 630 title = {{Media Coverage and the Cross-section of Stock Returns}},
631 631 journal = {The Journal of Finance},
632 632 year = {2009},
@@ -714,7 +714,7 @@
714 714 }
715 715
716 716 @article{kaniel2012,
717 − author = {KANIEL, RON and LIU, SHUMING and SAAR, GIDEON and TITMAN, SHERIDAN},
717 + author = {Kaniel, Ron and Liu, Shuming and Saar, Gideon and Titman, Sheridan},
718 718 title = {{Individual Investor Trading and Return Patterns around Earnings Announcements}},
719 719 journal = {The Journal of Finance},
720 720 year = {2012},
@@ -845,7 +845,7 @@
845 845 }
846 846
847 847 @article{vankervel2019,
848 − author = {VAN KERVEL, VINCENT and MENKVELD, ALBERT J.},
848 + author = {van Kervel, Vincent and Menkveld, ALBERT J.},
849 849 title = {{High-Frequency Trading around Large Institutional Orders}},
850 850 journal = {The Journal of Finance},
851 851 year = {2019},
modified paper/sections/mechanisms.tex +3 −3
@@ -36,11 +36,11 @@ period and therefore absent from the test subgroups.
36 36 \end{figure}
37 37
38 38 The panel regressions of Table~\ref{tab:panel} use the out-of-fold Latent Fundamental Deterioration Signal from the
39 −expanding-window refits. The daily-benchmark signal $\mathrm{LFDS}^{\mathrm{B2}}$ is a powerful predictor of the
39 +expanding-window refits. The daily-benchmark signal $\LFDS^{\mathrm{B2}}$ is a powerful predictor of the
40 40 realised surprise with firm and sector-by-quarter fixed effects ($\beta=\panSonlfdsTwoanddlfdslfdsTwo$,
41 41 $t=\pantSonlfdsTwoanddlfdslfdsTwo$, two-way clustered): a one-point increase in the predicted probability of an adverse
42 42 quarter is associated with a surprise lower by 7.6\% of assets. The \emph{incremental} 1-minute component
43 −$\mathrm{LFDS}^{\mathrm{B5}}-\mathrm{LFDS}^{\mathrm{B2}}$ has a coefficient of \panSonlfdsTwoanddlfdsdlfds{}
43 +$\LFDS^{\mathrm{B5}}-\LFDS^{\mathrm{B2}}$ has a coefficient of \panSonlfdsTwoanddlfdsdlfds{}
44 44 ($t=\pantSonlfdsTwoanddlfdsdlfds$), one sixth of the benchmark's and marginal at conventional levels. Its interactions
45 45 with ex-ante terciles point in the direction \emph{opposite} to H3: the incremental component is less informative for
46 46 the smallest firms ($t=\pantdlfdsxsizedlfdsxsizeZero$ on the bottom-tercile interaction) and for the least levered
@@ -68,7 +68,7 @@ $^{*}$, $^{**}$, $^{***}$: 10, 5, 1\%.
68 68
69 69 If the deterioration probability were news to the market, firms with a high ex-ante probability should experience
70 70 negative abnormal returns between the cutoff and the day after the release. Appendix Table~\ref{app:tables} shows that
71 −the market-adjusted return from $c+1$ to $A_q+1$ is unrelated to $\mathrm{LFDS}^{\mathrm{B5}}$
71 +the market-adjusted return from $c+1$ to $A_q+1$ is unrelated to $\LFDS^{\mathrm{B5}}$
72 72 ($\beta=\carcaronlfdsFivelfdsFive$, $t=\cartcaronlfdsFivelfdsFive$) and to the daily benchmark signal: by the cutoff,
73 73 twenty trading days before the release, the information that the models extract is already in prices. The incremental
74 74 1-minute component carries a positive coefficient ($\carcaronlfdsTwodlfdsdlfds$, $t=\cartcaronlfdsTwodlfdsdlfds$),
modified paper/sections/methodology.tex +43 −19
@@ -8,9 +8,15 @@ $E_{i,q}$, the earnings-release date $A_{i,q}$ (the reveal date, with its before
8 8 10-Q/10-K filing date $F_{i,q}$ and the vendor availability instant $V_{i,q}=F_{i,q}+1$ day. The prediction cutoff is
9 9 $c=A_{i,q}-h$ trading days for $h\in\{40,20,10,5,1\}$, with $h=20$ as the preregistered primary horizon. A cutoff is
10 10 admissible only if $c<\min(A_{i,q},F_{i,q})$ and $c>A_{i,q-1}$, so the feature window never contains the release of
11 −quarter $q$ and never straddles two reveals. The information set $\mathcal{I}(c)$ contains regular-session bars with
12 −session date $\le c$ and accounting facts with $V_{i,j}\le c$ only; the latest such quarter is $j^*(c)=q-1$ in the
13 −retained sample (we require that the forecaster predicts the quarter immediately following the latest available one).
11 +quarter $q$ and never straddles two reveals. The information set is
12 +\begin{equation}
13 + \Info(c)\;=\;\set{\text{bars}_{i,t}:\ \mathrm{session}(t)\le c}\;\cup\;\set{\text{facts}_{i,j}:\ V_{i,j}\le c},
14 + \qquad j^*(c)=\max\set{j: V_{i,j}\le c},
15 + \label{eq:infoset}
16 +\end{equation}
17 +regular-session bars up to the close of $c$ and accounting facts available by $c$ only; the latest such quarter is
18 +$j^*(c)=q-1$ in the retained sample (we require that the forecaster predicts the quarter immediately following the
19 +latest available one).
14 20 Twelve leakage guards (future facts, fiscal-end joins, post-cutoff bars, test-period transforms, daylight-saving errors,
15 21 identifier drift, split artefacts, release-day bars, overlapping windows across splits, survivorship, ingestion dates,
16 22 randomised placebos) are implemented as unit tests in the replication package; the local as-of join was verified live
@@ -30,10 +36,10 @@ tests and as a robustness check because it does not condition on knowledge of th
30 36
31 37 \subsection{Targets}
32 38
33 −The primary outcome is the scaled operating-income surprise $S_{i,q}$ of equation~\eqref{eq:surprise}, with
34 −$\mathbb{E}[X_q]=X_{q-4}+\tfrac14\sum_{k=0}^{3}(X_{q-1-k}-X_{q-5-k})$ computed from quarters available at $c$ and scaled by
35 −the latest available total assets. The binary adverse outcome is $D_{i,q}=\mathbb{1}[S_{i,q}\le-0.5\%]$, i.e., a
36 −shortfall of at least half a percent of assets (roughly a fifth of a typical quarterly return on assets). Secondary
39 +The primary outcome is the scaled operating-income surprise $S_{i,q}$ of equation~\eqref{eq:surprise}, with the
40 +seasonal expectation computed from quarters available at $c$ and scaled by the latest available total assets. The binary
41 +adverse outcome is $D_{i,q}=\ind{S_{i,q}\le-0.005}$, i.e., a shortfall of at least half a percent of assets (roughly a
42 +fifth of a typical quarterly return on assets). Secondary
37 43 outcomes are the net-income analogue, a year-on-year ROA decline of at least one point, a negative operating-cash-flow
38 44 innovation of at least one percent of assets, a liquidity/coverage deterioration (current ratio down 10\% or interest
39 45 coverage falling below one), a symmetric placebo $|S_{i,q}|\ge0.5\%$ that a pure volatility channel would predict, and an
@@ -45,12 +51,23 @@ target uses prices.
45 51
46 52 All market features are derived from the same raw 1-minute bars of the regular session (09:30 to the closing print),
47 53 aggregated to 5-minute and 30-minute buckets and to daily bars. Each intraday \emph{family} is computed per session
48 −on bars of the given length and averaged (or summed) over the 20-session window ending at $c$: realized variance,
49 −bipower variation, relative jump, negative-semivariance share, signed-jump variation, realized skewness and kurtosis,
50 −concentration of variance in the largest bar, first-order autocorrelation, intraday drift, opening and closing volume
51 −shares, volume dispersion (Gini), down-bar volume share, share of expected bars present, share of variance on
52 −market-down bars, realized beta to SPY (ratio of summed cross-products), variance ratio against the daily return variance,
53 −and volatility-of-volatility. The 30-minute, 5-minute and 1-minute sets therefore have the \emph{same} \kIntra{} columns.
54 +on bars of the given length and averaged (or summed) over the 20-session window ending at $c$. With $r_{t}$ the
55 +within-session log return of bar $t$ and $n$ the number of bars present, the variance families are
56 +\begin{equation}
57 + \RV=\sum_{t} r_t^{2},\qquad
58 + \BPV=\frac{\pi}{2}\sum_{t}\abs{r_t}\,\abs{r_{t-1}},\qquad
59 + \mathrm{RJ}=\frac{\max(\RV-\BPV,0)}{\RV},\qquad
60 + \RS^{\pm}=\sum_{t} r_t^{2}\,\ind{r_t\gtrless 0},
61 + \label{eq:rv}
62 +\end{equation}
63 +together with the negative-semivariance share $\RS^-/\RV$, the signed-jump variation $(\RS^+-\RS^-)/\RV$, realized
64 +skewness $\sqrt{n}\sum_t r_t^{3}/\RV^{3/2}$ and kurtosis $n\sum_t r_t^{4}/\RV^{2}$, the concentration $\max_t r_t^{2}/\RV$,
65 +and the first-order autocorrelation of $r_t$. The activity families are the intraday drift $\log(C/O)$, the opening and
66 +closing 30-minute volume shares, the volume Gini across bars, the down-bar volume share
67 +$\sum_t v_t\ind{r_t<0}/\sum_t v_t$, and the share of expected bars present. The co-movement families are the realized
68 +beta to SPY, $\hat\beta=\sum_t r_t r^{\mathrm{SPY}}_t/\sum_t (r^{\mathrm{SPY}}_t)^{2}$ with sums taken over the window,
69 +the share of $\RV$ accruing on market-down bars, the variance ratio $\overline{\RV}/\operatorname{Var}(r^{\mathrm{daily}})$,
70 +and the volatility-of-volatility (dispersion of daily $\RV$ within the window). The 30-minute, 5-minute and 1-minute sets therefore have the \emph{same} \kIntra{} columns.
54 71 Daily features (\kDaily{} columns) are 20- and 60-day returns, volatilities, Parkinson range, skewness, kurtosis,
55 72 overnight and intraday return components, volume ratios, Amihud illiquidity, zero-return share, dollar volume, maximum
56 73 drawdown and bar activity. Point-in-time accounting features (\kFund{} columns) are margins, ROA, accruals, cash,
@@ -80,15 +97,22 @@ The chronological split trains on cutoffs in 2012--2018, tunes on 2019--2020 and
80 97 of training outcomes revealed after the next split's start minus a one-quarter embargo \citep{lopezdeprado2018}.
81 98 Expanding-window annual refits from 2016 provide out-of-fold predictions for the panel regressions and the timing and
82 99 pricing exercises; a 20\% firm hold-out evaluates transfer to unseen firms. The primary metric is log loss; Brier score,
83 −AUC, PR-AUC and calibration are reported for every model. Paired differences (B5$-$B2, B5$-$B4, B5$-$B3) are assessed
84 −with a block bootstrap over calendar quarters of the reveal date \citep{kunsch1989, politis1994} and with the
85 −Giacomini--White test on quarterly mean loss differentials with Newey--West variance \citep{diebold1995,
86 −giacomini2006}; Holm's correction is applied within each hypothesis family \citep{holm1979}. Panel regressions
100 +AUC, PR-AUC and calibration are reported for every model. Paired differences (B5$-$B2, B5$-$B4, B5$-$B3) are assessed on the per-observation loss differential
101 +$d_{i,q}=\ell^{\mathrm{A}}_{i,q}-\ell^{\mathrm{B}}_{i,q}$ with a block bootstrap over calendar quarters of the reveal
102 +date \citep{kunsch1989, politis1994} and with the Giacomini--White statistic on quarterly means,
103 +\begin{equation}
104 + t_{\mathrm{GW}}\;=\;\frac{\bar d}{\sqrt{\hat\sigma^{2}_{\mathrm{NW}}/T}},\qquad
105 + \bar d=\frac{1}{T}\sum_{\tau=1}^{T}\bar d_{\tau},
106 + \label{eq:gw}
107 +\end{equation}
108 +where $\bar d_\tau$ is the mean differential in calendar quarter $\tau$ and $\hat\sigma^{2}_{\mathrm{NW}}$ its
109 +Newey--West long-run variance \citep{diebold1995, giacomini2006}; Holm's correction is applied within each hypothesis
110 +family \citep{holm1979}. Panel regressions
87 111 \begin{equation}
88 − S_{i,q}=\alpha_i+\delta_{s(i)\times t(q)}+\beta\,\mathrm{LFDS}_{i,c}+\gamma'X_{i,c}+\varepsilon_{i,q}
112 + S_{i,q}\;=\;\alpha_i+\delta_{s(i)\times t(q)}+\beta\,\LFDS_{i,c}+\bgamma\tr\bX_{i,c}+\varepsilon_{i,q}
89 113 \label{eq:panel}
90 114 \end{equation}
91 115 use the out-of-fold B5 probability as the Latent Fundamental Deterioration Signal (LFDS), firm and sector-by-quarter
92 116 fixed effects, and two-way clustering by firm and calendar quarter \citep{petersen2009, cameron2011}. Mechanism tests
93 −(H3) interact the \emph{incremental} signal $\mathrm{LFDS}^{\mathrm{B5}}-\mathrm{LFDS}^{\mathrm{B2}}$ with ex-ante
117 +(H3) interact the \emph{incremental} signal $\LFDS^{\mathrm{B5}}-\LFDS^{\mathrm{B2}}$ with ex-ante
94 118 terciles of size, illiquidity and leverage formed within each calendar quarter.
modified paper/sections/theory_hypotheses.tex +15 −8
@@ -5,16 +5,18 @@
5 5
6 6 Let $X_{i,q}$ denote an accounting flow of firm $i$ for fiscal quarter $q$ (operating income in our primary
7 7 specification), revealed to the public at the earnings-release date $A_{i,q}$ and structured in the SEC filing of date
8 −$F_{i,q}\ge A_{i,q}$. A forecaster standing at a trading day $c<A_{i,q}$ holds an information set $\mathcal{I}(c)$ that
8 +$F_{i,q}\ge A_{i,q}$. A forecaster standing at a trading day $c<A_{i,q}$ holds an information set $\Info(c)$ that
9 9 contains, at most, prices and volumes up to the close of $c$ and the accounting statements that were \emph{available} at
10 10 $c$, i.e., quarters $j$ with $F_{i,j}+1\le c$. The quantity of interest is the conditional distribution of the surprise
11 11 \begin{equation}
12 − S_{i,q} \;=\; \frac{X_{i,q}-\mathbb{E}\!\left[X_{i,q}\mid \mathcal{A}(c)\right]}{TA_{i,j^*(c)}},
12 + S_{i,q} \;=\; \frac{X_{i,q}-\E\!\left[X_{i,q}\mid \Acc(c)\right]}{\TA_{i,j^*(c)}},
13 + \qquad
14 + \E\!\left[X_{i,q}\mid \Acc(c)\right] \;=\; X_{i,q-4}+\frac{1}{4}\sum_{k=0}^{3}\bigl(X_{i,q-1-k}-X_{i,q-5-k}\bigr),
13 15 \label{eq:surprise}
14 16 \end{equation}
15 −where $\mathcal{A}(c)\subset\mathcal{I}(c)$ is the accounting information alone, $j^*(c)$ is the latest available quarter,
16 −and the expectation is a seasonal random walk with drift \citep{foster1977}, the classic honest benchmark when no
17 −point-in-time consensus exists. Semi-strong efficiency \citep{fama1970} implies that prices at $c$ impound all public
17 +where $\Acc(c)\subset\Info(c)$ is the accounting information alone, $j^*(c)$ is the latest available quarter, and the
18 +expectation is a seasonal random walk with drift \citep{foster1977}, the classic honest benchmark when no point-in-time
19 +consensus exists. The adverse event is $D_{i,q}=\ind{S_{i,q}\le -0.005}$. Semi-strong efficiency \citep{fama1970} implies that prices at $c$ impound all public
18 20 information, including $\mathcal{A}(c)$; the ``prices lead earnings'' literature \citep{beaver1980, kothari1992,
19 21 collins1994} documents that daily and annual returns indeed forecast accounting changes. None of this is the question
20 22 we ask. Our question is whether the \emph{resolution} at which the market is observed carries incremental information,
@@ -43,11 +45,16 @@ empirical questions, and a precise null is as informative as a positive estimate
43 45 \subsection{Hypotheses}
44 46
45 47 All hypotheses, endpoints and decision rules were frozen before the test period was opened (\texttt{docs/RESEARCH\_DESIGN.md}
46 −in the replication package). Let $\mathcal{L}(M)$ denote the out-of-sample log loss of model $M$ for the binary adverse
47 −outcome $D_{i,q}=\mathbb{1}[S_{i,q}\le -0.5\%]$, and let B1--B5 be the nested information sets of Table~\ref{tab:infosets}.
48 +in the replication package). Let
49 +\begin{equation}
50 + \Loss(M)\;=\;-\frac{1}{N}\sum_{(i,q)}\Bigl[D_{i,q}\log \hat p^{M}_{i,q}+(1-D_{i,q})\log\bigl(1-\hat p^{M}_{i,q}\bigr)\Bigr]
51 + \label{eq:logloss}
52 +\end{equation}
53 +denote the out-of-sample log loss of model $M$ for the binary adverse outcome, and let B1--B5 be the nested information
54 +sets of Table~\ref{tab:infosets}; the paired increment of interest is $\dlog(\mathrm{A},\mathrm{B})=\Loss(\mathrm{A})-\Loss(\mathrm{B})$.
48 55
49 56 \begin{description}[leftmargin=1.5em]
50 −\item[H1 (incremental information).] $\mathcal{L}(\mathrm{B5})<\mathcal{L}(\mathrm{B2})$ at the 20-trading-day horizon:
57 +\item[H1 (incremental information).] $\Loss(\mathrm{B5})<\Loss(\mathrm{B2})$ at the 20-trading-day horizon:
51 58 one-minute features improve forecasts beyond point-in-time fundamentals and daily OHLCV. Null: no improvement.
52 59 \item[H2 (temporal resolution).] The gain of B5 over B2 exceeds the gains of B4 (5-minute) and B3 (30-minute) over B2,
53 60 with the same feature families and the same number of inputs. Null: the gains are equal.
modified paper/tables/tab05_headline.tex +2 −0
@@ -4,12 +4,14 @@ Model & N & Log loss & Brier & AUC & PR-AUC & Cal. slope & Cal. int. \\
4 4 \midrule
5 5 B0 & 53,667 & 0.6282 & 0.2178 & 0.500 & 0.315 & 0.39 & -0.39 \\
6 6 B0b & 53,667 & 0.6137 & 0.2126 & 0.596 & 0.371 & 0.75 & -0.00 \\
7 +\addlinespace[2pt]\midrule
7 8 B1 & 53,667 & 0.5527 & 0.1854 & 0.730 & 0.565 & 0.95 & 0.10 \\
8 9 B2 & 53,667 & 0.5467 & 0.1828 & 0.738 & 0.572 & 0.86 & -0.13 \\
9 10 B3 & 53,667 & 0.5464 & 0.1830 & 0.739 & 0.571 & 0.87 & -0.04 \\
10 11 B4 & 53,667 & 0.5466 & 0.1831 & 0.739 & 0.571 & 0.87 & -0.02 \\
11 12 B5 & 53,667 & 0.5481 & 0.1838 & 0.738 & 0.569 & 0.86 & 0.00 \\
12 13 B5all & 53,667 & 0.5480 & 0.1837 & 0.738 & 0.569 & 0.86 & 0.00 \\
14 +\addlinespace[2pt]\midrule
13 15 LB1 & 53,667 & 0.4937 & 0.1634 & 0.797 & 0.655 & 1.07 & 0.13 \\
14 16 LB2 & 53,667 & 0.4869 & 0.1606 & 0.803 & 0.665 & 1.04 & -0.02 \\
15 17 LB3 & 53,667 & 0.4886 & 0.1613 & 0.801 & 0.662 & 1.03 & 0.04 \\
modified paper/tables/tab05b_contrasts.tex +1 −0
@@ -11,6 +11,7 @@ logistic & B5all & B2 & 53,667 & +0.0012 & [-0.0014, +0.0040] & 0.495 & & +0.00
11 11 logistic & B5all & B5 & 53,667 & -0.0001 & [-0.0006, +0.0004] & 0.899 & & -0.00008 \\
12 12 logistic & B2 & B1 & 53,667 & -0.0060 & [-0.0101, -0.0023] & 0.085* & & -0.00265 \\
13 13 logistic & B1 & B0 & 53,667 & -0.0754 & [-0.0846, -0.0675] & 0.000*** & & -0.03243 \\
14 +\addlinespace[2pt]\midrule
14 15 lightgbm & LB5 & LB2 & 53,667 & +0.0016 & [+0.0010, +0.0023] & 0.001*** & 0.002 & +0.00057 \\
15 16 lightgbm & LB4 & LB2 & 53,667 & +0.0021 & [+0.0007, +0.0035] & 0.030** & 0.049 & +0.00082 \\
16 17 lightgbm & LB3 & LB2 & 53,667 & +0.0017 & [+0.0003, +0.0031] & 0.025** & 0.049 & +0.00074 \\
modified paper/tables/tab06_panel.tex +5 −0
@@ -3,17 +3,22 @@
3 3 Specification & Regressor & Coef. & SE & N & Firms & Quarters \\
4 4 \midrule
5 5 S on lfds5 & lfds5 & -0.0763*** & (0.0024) & 84,759 & 3,598 & 44 \\
6 +\addlinespace[2pt]\midrule
6 7 S on lfds2 and dlfds & lfds2 & -0.0765*** & (0.0024) & 84,759 & 3,598 & 44 \\
7 8 S on lfds2 and dlfds & d lfds & -0.0129* & (0.0073) & 84,759 & 3,598 & 44 \\
9 +\addlinespace[2pt]\midrule
8 10 y on lfds5 & lfds5 & 0.9681*** & (0.0180) & 84,759 & 3,598 & 44 \\
11 +\addlinespace[2pt]\midrule
9 12 dlfds x size & lfds2 & -0.0765*** & (0.0024) & 84,759 & 3,598 & 44 \\
10 13 dlfds x size & d lfds & -0.0228*** & (0.0082) & 84,759 & 3,598 & 44 \\
11 14 dlfds x size & dlfds x size0 & 0.0327** & (0.0143) & 84,759 & 3,598 & 44 \\
12 15 dlfds x size & dlfds x size2 & -0.0089 & (0.0096) & 84,759 & 3,598 & 44 \\
16 +\addlinespace[2pt]\midrule
13 17 dlfds x illiq & lfds2 & -0.0765*** & (0.0024) & 84,759 & 3,598 & 44 \\
14 18 dlfds x illiq & d lfds & -0.0148 & (0.0090) & 84,759 & 3,598 & 44 \\
15 19 dlfds x illiq & dlfds x illiq0 & -0.0118 & (0.0113) & 84,759 & 3,598 & 44 \\
16 20 dlfds x illiq & dlfds x illiq2 & 0.0152 & (0.0157) & 84,759 & 3,598 & 44 \\
21 +\addlinespace[2pt]\midrule
17 22 dlfds x lev & lfds2 & -0.0765*** & (0.0024) & 84,759 & 3,598 & 44 \\
18 23 dlfds x lev & d lfds & -0.0239*** & (0.0083) & 84,759 & 3,598 & 44 \\
19 24 dlfds x lev & dlfds x lev0 & 0.0300*** & (0.0103) & 84,759 & 3,598 & 44 \\
modified paper/tables/tab07_horizons.tex +1 −0
@@ -7,6 +7,7 @@ lightgbm & 20 & 53,667 & 0.804 & 0.804 & -0.0012 & [-0.0023, -0.0001] & 0.134 \\
7 7 lightgbm & 10 & 53,751 & 0.805 & 0.803 & +0.0006 & [-0.0005, +0.0018] & 0.240 \\
8 8 lightgbm & 5 & 53,575 & 0.803 & 0.803 & -0.0004 & [-0.0013, +0.0005] & 0.786 \\
9 9 lightgbm & 1 & 53,669 & 0.804 & 0.803 & +0.0009 & [+0.0001, +0.0017] & 0.063 \\
10 +\addlinespace[2pt]\midrule
10 11 logistic & 40 & 49,340 & 0.740 & 0.740 & +0.0025 & [+0.0005, +0.0048] & 0.195 \\
11 12 logistic & 20 & 53,667 & 0.738 & 0.738 & +0.0013 & [-0.0013, +0.0041] & 0.518 \\
12 13 logistic & 10 & 53,751 & 0.737 & 0.738 & +0.0029 & [-0.0004, +0.0066] & 0.295 \\
modified paper/tables/tab10_targets.tex +5 −0
@@ -4,14 +4,19 @@ Target & Family & N & Base rate & AUC B2 & AUC B5 & $\Delta$ log loss & 95\% CI
4 4 \midrule
5 5 y adverse netinc & logistic & 53,663 & 0.331 & 0.708 & 0.709 & -0.0005 & [-0.0014, +0.0004] & 0.828 \\
6 6 y adverse netinc & lightgbm & 53,663 & 0.331 & 0.758 & 0.757 & +0.0013 & [+0.0000, +0.0023] & 0.081 \\
7 +\addlinespace[2pt]\midrule
7 8 y roa decline & logistic & 53,662 & 0.277 & 0.688 & 0.688 & -0.0014 & [-0.0030, -0.0001] & 0.334 \\
8 9 y roa decline & lightgbm & 53,662 & 0.277 & 0.722 & 0.724 & +0.0002 & [-0.0010, +0.0015] & 0.484 \\
10 +\addlinespace[2pt]\midrule
9 11 y ocf negative & logistic & 52,999 & 0.243 & 0.704 & 0.702 & +0.0078 & [+0.0035, +0.0131] & 0.016 \\
10 12 y ocf negative & lightgbm & 52,999 & 0.243 & 0.745 & 0.745 & +0.0022 & [+0.0005, +0.0039] & 0.019 \\
13 +\addlinespace[2pt]\midrule
11 14 y liquidity deterioration & logistic & 40,376 & 0.447 & 0.642 & 0.639 & +0.0006 & [-0.0007, +0.0017] & 0.335 \\
12 15 y liquidity deterioration & lightgbm & 40,376 & 0.447 & 0.694 & 0.695 & -0.0005 & [-0.0015, +0.0005] & 0.593 \\
16 +\addlinespace[2pt]\midrule
13 17 y abs surprise & logistic & 53,667 & 0.628 & 0.783 & 0.785 & +0.0015 & [-0.0003, +0.0037] & 0.067 \\
14 18 y abs surprise & lightgbm & 53,667 & 0.628 & 0.836 & 0.835 & +0.0008 & [+0.0002, +0.0013] & 0.008 \\
19 +\addlinespace[2pt]\midrule
15 20 y adverse opinc & logistic & 53,667 & 0.315 & 0.738 & 0.738 & +0.0013 & [-0.0013, +0.0041] & 0.518 \\
16 21 y adverse opinc & lightgbm & 53,667 & 0.315 & 0.803 & 0.801 & +0.0016 & [+0.0010, +0.0023] & 0.001 \\
17 22 \bottomrule
modified paper/tables/tab11_economic.tex +1 −0
@@ -4,6 +4,7 @@ Score & Variant & Mean L/S (\%/mo) & FF5+MOM $\alpha$ (\%/mo) & $t$ (HAC) & Shar
4 4 \midrule
5 5 LB2 & gross & -0.51 & -0.49 & -1.64 & -0.36 & 0.59 & 129 \\
6 6 LB2 & net20bps & -0.53 & -0.51 & -1.73 & -0.38 & 0.59 & 129 \\
7 +\addlinespace[2pt]\midrule
7 8 LB5 & gross & -0.65 & -0.59 & -1.85 & -0.44 & 0.60 & 129 \\
8 9 LB5 & net20bps & -0.68 & -0.62 & -1.93 & -0.46 & 0.60 & 129 \\
9 10 \bottomrule
modified paper/tables/tab11b_car.tex +1 −0
@@ -3,6 +3,7 @@
3 3 Specification & Regressor & Coef. & SE & N \\
4 4 \midrule
5 5 car on lfds5 & lfds5 & 0.0001 & (0.0042) & 84,755 \\
6 +\addlinespace[2pt]\midrule
6 7 car on lfds2 dlfds & lfds2 & -0.0002 & (0.0042) & 84,755 \\
7 8 car on lfds2 dlfds & d lfds & 0.0605*** & (0.0209) & 84,755 \\
8 9 \bottomrule
modified scripts/09_make_manuscript.py +16 −9
@@ -29,10 +29,16 @@ def stars(p):
29 29 return "***" if p < 0.01 else "**" if p < 0.05 else "*" if p < 0.1 else ""
30 30
31 31
32 −def tex_table(df: pd.DataFrame, path: Path, cols: list[tuple[str, str, str]], note: str = ""):
33 − """cols: (column, header, fmt) — fmt is a python format or 'int'/'str'."""
32 +def tex_table(df: pd.DataFrame, path: Path, cols: list[tuple[str, str, str]], note: str = "", group: str | None = None):
33 + """cols: (column, header, fmt) — fmt is a python format or 'int'/'str'/'tex'. `group`: column whose change inserts a rule."""
34 34 lines = ["\\begin{tabular}{l" + "r" * (len(cols) - 1) + "}", "\\toprule", " & ".join(h for _, h, _ in cols) + " \\\\", "\\midrule"]
35 + prev = None
35 36 for _, r in df.iterrows():
37 + if group is not None:
38 + g = r[group] if group in df.columns else None
39 + if prev is not None and g != prev:
40 + lines.append("\\addlinespace[2pt]\\midrule")
41 + prev = g
36 42 cells = []
37 43 for c, _, fmt in cols:
38 44 v = r[c]
@@ -101,7 +107,8 @@ def main() -> int:
101 107 m20 = met[(met.h == hp) & (met.target == "y_adverse_opinc") & (met.part == "test") & (met.tag == "chrono")].copy()
102 108 order = ["B0", "B0b", "B1", "B2", "B3", "B4", "B5", "B5all", "LB1", "LB2", "LB3", "LB4", "LB5", "LB5all"]
103 109 m20["o"] = m20.model.map({m: i for i, m in enumerate(order)}); m20 = m20.sort_values("o")
104 − tex_table(m20, TB / "tab05_headline.tex", [("model", "Model", "str"), ("n", "N", "int"), ("logloss", "Log loss", "{:.4f}"), ("brier", "Brier", "{:.4f}"),
110 + m20["fam"] = np.where(m20.model.str.startswith("L"), "lightgbm", np.where(m20.model.str.startswith("B0"), "base", "logistic"))
111 + tex_table(m20, TB / "tab05_headline.tex", group="fam", cols= [("model", "Model", "str"), ("n", "N", "int"), ("logloss", "Log loss", "{:.4f}"), ("brier", "Brier", "{:.4f}"),
105 112 ("auc", "AUC", "{:.3f}"), ("pr_auc", "PR-AUC", "{:.3f}"), ("cal_slope", "Cal. slope", "{:.2f}"), ("cal_intercept", "Cal. int.", "{:.2f}")])
106 113 for _, r in m20.iterrows():
107 114 macro(f"ll{r.model}", r.logloss, "{:.4f}"); macro(f"auc{r.model}", r.auc, "{:.3f}")
@@ -110,7 +117,7 @@ def main() -> int:
110 117 con["ci"] = con.apply(lambda r: f"[{r.ci_lo:+.4f}, {r.ci_hi:+.4f}]", axis=1)
111 118 con["gwp"] = con.apply(lambda r: f"{r.gw_p:.3f}{stars(r.gw_p)}", axis=1)
112 119 con["holm"] = con["gw_p_holm"].map(lambda v: "" if pd.isna(v) else f"{v:.3f}")
113 − tex_table(con, TB / "tab05b_contrasts.tex", [("family", "Family", "str"), ("a", "A", "str"), ("b", "B", "str"), ("n", "N", "int"), ("d_logloss", "$\\Delta$ log loss", "{:+.4f}"),
120 + tex_table(con, TB / "tab05b_contrasts.tex", group="family", cols=[("family", "Family", "str"), ("a", "A", "str"), ("b", "B", "str"), ("n", "N", "int"), ("d_logloss", "$\\Delta$ log loss", "{:+.4f}"),
114 121 ("ci", "95\\% CI", "str"), ("gwp", "GW $p$", "str"), ("holm", "Holm", "str"), ("d_brier", "$\\Delta$ Brier", "{:+.5f}")])
115 122 for _, r in con.iterrows():
116 123 k = f"{r.family[:3]}{r.a}vs{r.b}".replace("B5all", "Ball")
@@ -119,14 +126,14 @@ def main() -> int:
119 126 # ---------------- Table 7: horizon ladder, secondary targets, firm holdout
120 127 lad = pd.read_csv(E / "horizon_ladder.csv")
121 128 lad["ci"] = lad.apply(lambda r: f"[{r.ci_lo:+.4f}, {r.ci_hi:+.4f}]", axis=1)
122 − tex_table(lad.sort_values(["family", "h"], ascending=[True, False]), TB / "tab07_horizons.tex", [("family", "Family", "str"), ("h", "$h$", "int"), ("n", "N", "int"),
129 + tex_table(lad.sort_values(["family", "h"], ascending=[True, False]), TB / "tab07_horizons.tex", group="family", cols=[("family", "Family", "str"), ("h", "$h$", "int"), ("n", "N", "int"),
123 130 ("auc_b", "AUC B2", "{:.3f}"), ("auc_a", "AUC B5", "{:.3f}"), ("d_logloss", "$\\Delta$ log loss", "{:+.4f}"), ("ci", "95\\% CI", "str"), ("gw_p", "GW $p$", "{:.3f}")])
124 131 h4 = json.load(open(E / "h4_timing.json"))
125 132 for fam, v in h4.items():
126 133 macro(f"h4rho{fam}", v["spearman_gain_vs_minus_h"], "{:+.2f}"); macro(f"h4p{fam}", v["p"], "{:.2f}")
127 134 sec = pd.read_csv(E / "secondary_targets.csv")
128 135 sec["ci"] = sec.apply(lambda r: f"[{r.ci_lo:+.4f}, {r.ci_hi:+.4f}]", axis=1)
129 − tex_table(sec, TB / "tab10_targets.tex", [("target", "Target", "str"), ("family", "Family", "str"), ("n", "N", "int"), ("base_rate", "Base rate", "{:.3f}"),
136 + tex_table(sec, TB / "tab10_targets.tex", group="target", cols=[("target", "Target", "str"), ("family", "Family", "str"), ("n", "N", "int"), ("base_rate", "Base rate", "{:.3f}"),
130 137 ("auc_b", "AUC B2", "{:.3f}"), ("auc_a", "AUC B5", "{:.3f}"), ("d_logloss", "$\\Delta$ log loss", "{:+.4f}"), ("ci", "95\\% CI", "str"), ("gw_p", "GW $p$", "{:.3f}")])
131 138 fh = pd.read_csv(E / "firm_holdout.csv"); fh["ci"] = fh.apply(lambda r: f"[{r.ci_lo:+.4f}, {r.ci_hi:+.4f}]", axis=1)
132 139 for _, r in fh.iterrows():
@@ -136,7 +143,7 @@ def main() -> int:
136 143 pr = pd.read_csv(E / "panel_regressions.csv")
137 144 pr = pr[pr["var"].isin(["lfds5", "lfds2", "d_lfds", "dlfds_x_size0", "dlfds_x_size2", "dlfds_x_illiq0", "dlfds_x_illiq2", "dlfds_x_lev0", "dlfds_x_lev2"])].copy()
138 145 pr["coef_s"] = pr.apply(lambda r: f"{r.coef:.4f}{stars(r.p)}", axis=1); pr["se_s"] = pr.se.map(lambda v: f"({v:.4f})")
139 − tex_table(pr, TB / "tab06_panel.tex", [("spec", "Specification", "str"), ("var", "Regressor", "str"), ("coef_s", "Coef.", "str"), ("se_s", "SE", "str"), ("n", "N", "int"), ("n_firms", "Firms", "int"), ("n_quarters", "Quarters", "int")])
146 + tex_table(pr, TB / "tab06_panel.tex", group="spec", cols=[("spec", "Specification", "str"), ("var", "Regressor", "str"), ("coef_s", "Coef.", "str"), ("se_s", "SE", "str"), ("n", "N", "int"), ("n_firms", "Firms", "int"), ("n_quarters", "Quarters", "int")])
140 147 for _, r in pr.iterrows():
141 148 macro(f"pan{r.spec.replace('_', '')}{r['var'].replace('_', '')}", r.coef, "{:+.4f}"); macro(f"pant{r.spec.replace('_', '')}{r['var'].replace('_', '')}", r.t, "{:.2f}")
142 149 # ---------------- Table 7b: mechanisms / subgroups; Table 8: placebos
@@ -152,13 +159,13 @@ def main() -> int:
152 159 for m, v in h5.items():
153 160 for k in ("gross", "net20bps"):
154 161 a = v[k]; rows.append({"model": m, "variant": k, "alpha": 100 * a["alpha_monthly"], "t": a["alpha_t"], "mean_ls": 100 * a["mean_ls"], "sharpe": a["sharpe_ann"], "T": a["T"], "turnover": v["avg_turnover"]})
155 − tex_table(pd.DataFrame(rows), TB / "tab11_economic.tex", [("model", "Score", "str"), ("variant", "Variant", "str"), ("mean_ls", "Mean L/S (\\%/mo)", "{:.2f}"), ("alpha", "FF5+MOM $\\alpha$ (\\%/mo)", "{:+.2f}"),
162 + tex_table(pd.DataFrame(rows), TB / "tab11_economic.tex", group="model", cols=[("model", "Score", "str"), ("variant", "Variant", "str"), ("mean_ls", "Mean L/S (\\%/mo)", "{:.2f}"), ("alpha", "FF5+MOM $\\alpha$ (\\%/mo)", "{:+.2f}"),
156 163 ("t", "$t$ (HAC)", "{:.2f}"), ("sharpe", "Sharpe (ann.)", "{:.2f}"), ("turnover", "Turnover", "{:.2f}"), ("T", "Months", "int")])
157 164 for m, v in h5.items():
158 165 macro(f"alpha{m}gross", 100 * v["gross"]["alpha_monthly"], "{:+.2f}"); macro(f"alphat{m}gross", v["gross"]["alpha_t"], "{:.2f}")
159 166 macro(f"alpha{m}net", 100 * v["net20bps"]["alpha_monthly"], "{:+.2f}"); macro(f"alphat{m}net", v["net20bps"]["alpha_t"], "{:.2f}")
160 167 carr = car[car["var"].isin(["lfds5", "lfds2", "d_lfds"])].copy(); carr["coef_s"] = carr.apply(lambda r: f"{r.coef:.4f}{stars(r.p)}", axis=1); carr["se_s"] = carr.se.map(lambda v: f"({v:.4f})")
161 − tex_table(carr, TB / "tab11b_car.tex", [("spec", "Specification", "str"), ("var", "Regressor", "str"), ("coef_s", "Coef.", "str"), ("se_s", "SE", "str"), ("n", "N", "int")])
168 + tex_table(carr, TB / "tab11b_car.tex", group="spec", cols=[("spec", "Specification", "str"), ("var", "Regressor", "str"), ("coef_s", "Coef.", "str"), ("se_s", "SE", "str"), ("n", "N", "int")])
162 169 for _, r in carr.iterrows():
163 170 macro(f"car{r.spec.replace('_', '')}{r['var'].replace('_', '')}", r.coef, "{:+.4f}"); macro(f"cart{r.spec.replace('_', '')}{r['var'].replace('_', '')}", r.t, "{:.2f}")
164 171 # continuous + power
modified src/wp39/figures.py +197 −93
@@ -1,5 +1,6 @@
1 −"""Journal figures (vector PDF, matplotlib). One axis per panel, thin marks, fixed categorical order, direct labels.
2 −Palette validated with the dataviz validator (light surface): blue, orange, aqua, yellow (+ gray for references)."""
1 +"""Journal-style figures (vector PDF): Times/STIX typography, monochrome marks distinguished by shape and line style,
2 +panel labels, inward ticks, no chart junk. Every exhibit is drawn from saved artifacts; the only schematic (timeline) is
3 +labelled as such. Widths follow journal columns: 3.25 in (single) and 6.5 in (double)."""
3 4 from __future__ import annotations
4 5
5 6 from pathlib import Path
@@ -9,148 +10,251 @@ matplotlib.use("Agg")
9 10 import matplotlib.pyplot as plt # noqa: E402
10 11 import numpy as np # noqa: E402
11 12 import pandas as pd # noqa: E402
13 +from matplotlib import ticker # noqa: E402
12 14
13 −SERIES = {"daily": "#2a78d6", "30m": "#eb6834", "5m": "#1baf7a", "1m": "#eda100", "fund": "#6250d6", "ref": "#8a8a86", "neg": "#e34948"}
14 −MODEL_COLOR = {"B1": SERIES["fund"], "B2": SERIES["daily"], "B3": SERIES["30m"], "B4": SERIES["5m"], "B5": SERIES["1m"], "B5all": "#52514e",
15 − "LB1": SERIES["fund"], "LB2": SERIES["daily"], "LB3": SERIES["30m"], "LB4": SERIES["5m"], "LB5": SERIES["1m"], "LB5all": "#52514e"}
16 −MODEL_LABEL = {"B1": "B1 fundamentals", "B2": "B2 + daily", "B3": "B3 + 30-min", "B4": "B4 + 5-min", "B5": "B5 + 1-min", "B5all": "all frequencies"}
15 +INK, INK2, INK3, INK4 = "#000000", "#4d4d4d", "#8c8c8c", "#c8c8c8"
16 +ACCENT = "#1f4e79" # single restrained accent (1-minute series) — passes the contrast check on white
17 17
18 −plt.rcParams.update({"font.family": "serif", "font.size": 9, "axes.spines.top": False, "axes.spines.right": False,
19 − "axes.edgecolor": "#8a8a86", "axes.linewidth": 0.6, "xtick.color": "#52514e", "ytick.color": "#52514e",
20 − "axes.labelcolor": "#0b0b0b", "grid.color": "#e6e5e1", "grid.linewidth": 0.5, "legend.frameon": False,
21 − "figure.dpi": 150, "savefig.bbox": "tight"})
18 +# Fixed encoding per information set: marker, line style, colour, label
19 +STYLE = {
20 + "B1": dict(marker="x", ls=":", color=INK3, label="B1 fundamentals"),
21 + "B2": dict(marker="o", ls="-", color=INK, label="B2 + daily"),
22 + "B3": dict(marker="s", ls="--", color=INK2, label="B3 + 30-min"),
23 + "B4": dict(marker="^", ls="-.", color=INK3, label="B4 + 5-min"),
24 + "B5": dict(marker="D", ls="-", color=ACCENT, label="B5 + 1-min"),
25 + "B5all": dict(marker="v", ls=":", color=INK2, label="all frequencies"),
26 +}
27 +for k in list(STYLE):
28 + STYLE["L" + k] = dict(STYLE[k], label=STYLE[k]["label"].replace("B", "LB", 1))
29 +
30 +plt.rcParams.update({
31 + "font.family": "serif", "font.serif": ["Times New Roman", "Times", "STIX Two Text", "DejaVu Serif"],
32 + "mathtext.fontset": "stix", "font.size": 9, "axes.titlesize": 9, "axes.labelsize": 9, "legend.fontsize": 8,
33 + "xtick.labelsize": 8, "ytick.labelsize": 8,
34 + "axes.spines.top": False, "axes.spines.right": False, "axes.edgecolor": INK, "axes.linewidth": 0.6,
35 + "xtick.direction": "in", "ytick.direction": "in", "xtick.major.size": 3, "ytick.major.size": 3,
36 + "xtick.major.width": 0.6, "ytick.major.width": 0.6, "axes.grid": False, "legend.frameon": False,
37 + "lines.linewidth": 1.1, "lines.markersize": 4.5, "lines.markeredgewidth": 0.8,
38 + "figure.dpi": 150, "savefig.bbox": "tight", "savefig.pad_inches": 0.02, "pdf.fonttype": 42,
39 +})
40 +W1, W2 = 3.25, 6.5
41 +
42 +
43 +def _panel(ax, letter: str, title: str):
44 + ax.set_title(f"Panel {letter}. {title}", loc="left", fontsize=9, fontweight="bold", pad=6)
45 +
46 +
47 +def _thousands(ax, axis="y"):
48 + (ax.yaxis if axis == "y" else ax.xaxis).set_major_formatter(ticker.FuncFormatter(lambda v, _: f"{v:,.0f}"))
22 49
23 50
24 51 def _save(fig, out: Path, name: str):
25 52 out.mkdir(parents=True, exist_ok=True)
26 − fig.savefig(out / f"{name}.pdf"); fig.savefig(out / f"{name}.png", dpi=200); plt.close(fig)
53 + fig.savefig(out / f"{name}.pdf")
54 + fig.savefig(out / f"{name}.png", dpi=220)
55 + plt.close(fig)
56 +
27 57
58 +def _errorbar(ax, x, y, lo, hi, **kw):
59 + ax.errorbar(x, y, yerr=[np.asarray(y) - np.asarray(lo), np.asarray(hi) - np.asarray(y)], capsize=2.5, capthick=0.7,
60 + elinewidth=0.7, **kw)
28 61
62 +
63 +# ----------------------------------------------------------------------------------------------------- Figure 1
29 64 def fig_timeline(out: Path, h_list=(40, 20, 10, 5, 1)):
30 − """Event-time timeline and the no-leakage rule (schematic drawn from the protocol, labelled as such)."""
31 − fig, ax = plt.subplots(figsize=(6.4, 2.2))
32 − ax.set_xlim(-75, 15); ax.set_ylim(0, 3); ax.axis("off")
33 − ax.plot([-75, 12], [1, 1], color="#52514e", lw=1)
34 − for x, lab, c in ((-63, "$A_{q-1}$\nprevious release", SERIES["ref"]), (0, "$A_q$\nrelease of quarter $q$", SERIES["neg"]),
35 − (1, "$F_q$ filing", SERIES["ref"]), (2, "$V_q=F_q+1$\nfacts available", SERIES["ref"])):
36 − ax.plot([x], [1], marker="|", ms=14, color=c, mew=1.5)
37 − ax.text(x, 0.15 if x not in (1,) else 1.55, lab, ha="center", va="center", fontsize=7, color="#0b0b0b")
38 − for i, h in enumerate(h_list):
39 − ax.plot([-h], [1], marker="o", ms=4, color=SERIES["1m"], mec="white", mew=0.8)
40 − ax.text(-h, 1.35 + 0.28 * (i % 2), f"$c=A_q-{h}$", ha="center", fontsize=6.5, color="#52514e")
41 − ax.add_patch(plt.Rectangle((-40, 0.85), 20, 0.3, color=SERIES["1m"], alpha=0.25, lw=0))
42 − ax.text(-30, 2.6, "20-session feature window ends at $c$; bars after $c$ and facts with $V_j>c$ never enter the information set",
43 − ha="center", fontsize=7, color="#52514e")
44 − ax.set_title("Event-time design (schematic of the protocol, not data)", fontsize=8, loc="left")
65 + """Schematic (not to scale beyond the cutoffs): events after A_q are spread out for legibility."""
66 + fig, ax = plt.subplots(figsize=(W2, 2.5))
67 + ax.set_xlim(-74, 24); ax.set_ylim(-1.9, 2.45); ax.axis("off")
68 + ax.annotate("", xy=(22, 0), xytext=(-74, 0), arrowprops=dict(arrowstyle="-|>", lw=0.9, color=INK))
69 + ax.text(22.5, 0, "time", va="center", ha="left", fontsize=7, color=INK2)
70 + # feature window for h = 20
71 + ax.add_patch(plt.Rectangle((-40, -0.26), 20, 0.52, facecolor=INK4, edgecolor="none"))
72 + ax.text(-30, 0.95, "20-session feature window for $h=20$\n(bars with session $\\leq c$)", ha="center", va="bottom", fontsize=7)
73 + # events with alternating label positions and leader lines
74 + events = [(-63, "$A_{q-1}$: previous release", -1.05, INK2), (0, "$A_q$: earnings release\n(reveal date)", -1.05, INK),
75 + (7, "$F_q$: SEC filing", 1.08, INK2), (12, "$V_q=F_q+1$: facts available", 1.75, INK2)]
76 + for x, lab, ytxt, c in events:
77 + ax.plot([x, x], [-0.42, 0.42], color=c, lw=1.2)
78 + if ytxt < 0:
79 + ax.plot([x, x], [-0.42, ytxt + 0.1], color=c, lw=0.5, ls=":")
80 + ax.text(x, ytxt, lab, ha="center", va="top", fontsize=7, color=c)
81 + else:
82 + ax.plot([x, x], [0.42, ytxt - 0.08], color=c, lw=0.5, ls=":")
83 + ax.text(x, ytxt, lab, ha="center", va="bottom", fontsize=7, color=c)
84 + for h in h_list:
85 + ax.plot([-h], [0], marker="D", ms=4, color=ACCENT, mec="white", mew=0.6, zorder=5)
86 + ax.text(-h, 0.3, f"$-{h}$", ha="center", va="bottom", fontsize=6.5, color=ACCENT)
87 + ax.text(-74, 2.3, "cutoffs $c=A_q-h$ trading days, $h\\in\\{40,20,10,5,1\\}$; admissible iff $c<\\min(A_q,F_q)$ and $c>A_{q-1}$",
88 + ha="left", va="top", fontsize=7.5)
89 + ax.text(22, -1.85, "Schematic of the protocol (not data; spacing after $A_q$ exaggerated).", ha="right", va="bottom", fontsize=6.5, color=INK3, style="italic")
45 90 _save(fig, out, "fig01_timeline")
46 91
47 92
93 +# ----------------------------------------------------------------------------------------------------- Figure 2
48 94 def fig_coverage(out: Path, years: pd.DataFrame, sizes: pd.DataFrame | None = None):
49 − """years: columns year, firms, firm_quarters, adverse_rate."""
50 − fig, axes = plt.subplots(1, 2, figsize=(6.4, 2.4))
95 + fig, axes = plt.subplots(1, 2, figsize=(W2, 2.5))
51 96 ax = axes[0]
52 − ax.bar(years["year"], years["firms"], color=SERIES["daily"], width=0.8)
53 − ax.set_ylabel("Firms in sample"); ax.set_xlabel("Year of cutoff"); ax.grid(axis="y")
54 − for x, v in zip(years["year"].iloc[::3], years["firms"].iloc[::3]):
55 − ax.text(x, v, f"{int(v):,}", ha="center", va="bottom", fontsize=6, color="#52514e")
97 + ax.bar(years["year"], years["firms"], color=INK2, width=0.72, edgecolor="none")
98 + _panel(ax, "A", "Firms in the primary cohort by year")
99 + ax.set_xlabel("Year of cutoff"); ax.set_ylabel("Number of firms"); _thousands(ax)
100 + ax.set_xticks(years["year"][::2]); ax.tick_params(axis="x", rotation=0)
56 101 ax = axes[1]
57 − ax.plot(years["year"], years["adverse_rate"], color=SERIES["neg"], lw=2, marker="o", ms=4, mec="white")
58 − ax.set_ylabel("Adverse-surprise rate"); ax.set_xlabel("Year of cutoff"); ax.grid(axis="y"); ax.set_ylim(0, None)
102 + ax.plot(years["year"], 100 * years["adverse_rate"], color=INK, marker="o", ms=4, mfc="white")
103 + _panel(ax, "B", "Adverse-surprise rate by year (%)")
104 + ax.set_xlabel("Year of cutoff"); ax.set_ylabel("Share of firm-quarters (%)"); ax.set_ylim(0, None)
105 + ax.set_xticks(years["year"][::2])
106 + ax.axvspan(2020.5, 2026.9, color=INK4, alpha=0.35, lw=0); ax.text(2023.7, 2, "test period", ha="center", fontsize=7, color=INK2)
107 + fig.subplots_adjust(wspace=0.32)
59 108 _save(fig, out, "fig02_coverage")
60 109
61 110
111 +# ----------------------------------------------------------------------------------------------------- Figure 3
62 112 def fig_prevalence(out: Path, prev: pd.DataFrame):
63 − """prev: columns group (str), label, rate, lo, hi — conditional adverse prevalence by ex-ante characteristics."""
64 − fig, ax = plt.subplots(figsize=(6.0, 2.6))
113 + fig, ax = plt.subplots(figsize=(W2, 2.9))
65 114 y = np.arange(len(prev))
66 − ax.hlines(y, prev["lo"], prev["hi"], color="#c3c2b7", lw=2)
67 − ax.plot(prev["rate"], y, "o", color=SERIES["neg"], ms=5, mec="white")
115 + ax.hlines(y, 100 * prev["lo"], 100 * prev["hi"], color=INK, lw=0.9)
116 + ax.plot(100 * prev["rate"], y, "o", color=INK, ms=5, mfc="white")
117 + for yi, r in zip(y, prev.itertuples()):
118 + ax.text(100 * r.hi + 0.4, yi, f"{100 * r.rate:.1f}", va="center", ha="left", fontsize=7, color=INK2)
68 119 ax.set_yticks(y); ax.set_yticklabels(prev["label"]); ax.invert_yaxis()
69 − ax.set_xlabel("Share of firm-quarters with an adverse operating-income surprise (test period)"); ax.grid(axis="x")
120 + ax.set_xlabel("Adverse operating-income surprise, share of firm-quarters (%), test period")
121 + ax.axvline(100 * prev["rate"].iloc[0], color=INK4, lw=0.8, zorder=0)
122 + ax.spines["left"].set_visible(False); ax.tick_params(axis="y", length=0)
70 123 _save(fig, out, "fig03_prevalence")
71 124
72 125
126 +# ----------------------------------------------------------------------------------------------------- Figure 4
73 127 def fig_frequency_gains(out: Path, contrasts: pd.DataFrame):
74 − """Paired Δ log loss vs B2 for B3/B4/B5 (logistic and LightGBM), with 95 % block-bootstrap intervals."""
75 − fam = [("logistic", ""), ("lightgbm", "L")]
76 − fig, axes = plt.subplots(1, 2, figsize=(6.4, 2.6), sharey=True)
77 − for ax, (name, pre) in zip(axes, fam):
78 − sub = contrasts[(contrasts.family == name) & (contrasts.b == pre + "B2") & (contrasts.a.isin([pre + "B3", pre + "B4", pre + "B5", pre + "B5all"]))]
79 − order = [pre + "B3", pre + "B4", pre + "B5", pre + "B5all"]
80 − sub = sub.set_index("a").reindex([o for o in order if o in sub.a.values]).reset_index()
128 + fams = [("logistic", "", "Penalised logistic regression"), ("lightgbm", "L", "Gradient-boosted trees (equal budget)")]
129 + fig, axes = plt.subplots(1, 2, figsize=(W2, 2.9), sharey=True)
130 + order = ["B3", "B4", "B5", "B5all"]
131 + for ax, (fam, pre, title), letter in zip(axes, fams, "AB"):
132 + sub = contrasts[(contrasts.family == fam) & (contrasts.b == pre + "B2")].copy()
133 + sub["key"] = sub["a"].str.replace("^L", "", regex=True)
134 + sub = sub.set_index("key").reindex([o for o in order if o in sub.key.values]).reset_index()
81 135 x = np.arange(len(sub))
82 136 for i, r in sub.iterrows():
83 − c = MODEL_COLOR.get(r["a"].replace("L", "", 1) if r["a"].startswith("L") else r["a"], "#52514e")
84 − ax.vlines(i, r["ci_lo"], r["ci_hi"], color=c, lw=2)
85 − ax.plot(i, r["d_logloss"], "o", color=c, ms=6, mec="white")
86 − ax.text(i, r["ci_hi"], f"{r['d_logloss']:+.4f}", ha="center", va="bottom", fontsize=6.5, color="#52514e")
87 − ax.axhline(0, color="#8a8a86", lw=0.8)
88 − ax.set_xticks(x); ax.set_xticklabels([MODEL_LABEL[a.replace("L", "", 1) if a.startswith("L") else a].replace("B5all", "all") for a in sub.a], fontsize=7)
89 − ax.set_title(f"{name}", fontsize=8, loc="left"); ax.grid(axis="y")
90 − axes[0].set_ylabel("Δ log loss vs. B2 (daily + accounting)\nnegative = better")
137 + st = STYLE[r["key"]]
138 + _errorbar(ax, [i], [r["d_logloss"]], [r["ci_lo"]], [r["ci_hi"]], fmt=st["marker"], color=st["color"], mfc="white", ms=5.5)
139 + ax.text(i, r["ci_hi"] + 0.0002, f"{r['d_logloss']:+.4f}", ha="center", va="bottom", fontsize=7)
140 + ax.axhline(0, color=INK, lw=0.7)
141 + ax.set_xticks(x); ax.set_xticklabels(["+ 30-min", "+ 5-min", "+ 1-min", "+ all"][: len(sub)])
142 + ax.set_xlim(-0.6, len(sub) - 0.4)
143 + _panel(ax, letter, title)
144 + axes[0].set_ylabel("$\\Delta$ log loss relative to B2 (daily + accounting)")
145 + axes[0].text(-0.5, axes[0].get_ylim()[0] * 0.95 if axes[0].get_ylim()[0] < 0 else 0, "", fontsize=7)
146 + axes[0].yaxis.set_major_formatter(ticker.FormatStrFormatter("%+.3f"))
147 + fig.subplots_adjust(wspace=0.12)
91 148 _save(fig, out, "fig04_frequency_gains")
92 149
93 150
151 +# ----------------------------------------------------------------------------------------------------- Figure 5
94 152 def fig_reliability(out: Path, rel: pd.DataFrame):
95 − fig, ax = plt.subplots(figsize=(3.6, 3.2))
96 − ax.plot([0, rel[["mean_pred", "obs_rate"]].max().max()], [0, rel[["mean_pred", "obs_rate"]].max().max()], color="#8a8a86", lw=0.8, ls="--")
97 − for m in ("LB2", "LB5", "B2", "B5"):
98 − s = rel[rel.model == m]
99 − if len(s):
100 − ax.plot(s["mean_pred"], s["obs_rate"], marker="o", ms=4, mec="white", lw=1.6, color=MODEL_COLOR[m], label=m)
101 − ax.set_xlabel("Mean predicted probability (decile)"); ax.set_ylabel("Observed adverse rate"); ax.legend(fontsize=7); ax.grid()
153 + fig, axes = plt.subplots(1, 2, figsize=(W2, 2.9), sharey=True)
154 + for ax, models, letter, title in ((axes[0], ("B2", "B5"), "A", "Logistic"), (axes[1], ("LB2", "LB5"), "B", "Gradient-boosted trees")):
155 + m = rel[rel.model.isin(models)]
156 + top = max(m["mean_pred"].max(), m["obs_rate"].max()) * 1.05 if len(m) else 1
157 + ax.plot([0, top], [0, top], color=INK4, lw=0.8)
158 + for mod in models:
159 + s = rel[rel.model == mod]
160 + st = STYLE[mod]
161 + ax.plot(s["mean_pred"], s["obs_rate"], marker=st["marker"], ls=st["ls"], color=st["color"], mfc="white", label=st["label"])
162 + ax.set_xlabel("Mean predicted probability (decile)"); _panel(ax, letter, title); ax.legend(loc="upper left")
163 + ax.set_xlim(0, top); ax.set_ylim(0, top)
164 + axes[0].set_ylabel("Observed adverse rate")
165 + fig.subplots_adjust(wspace=0.08)
102 166 _save(fig, out, "fig05_reliability")
103 167
104 168
169 +# ----------------------------------------------------------------------------------------------------- Figure 6
105 170 def fig_signal_evolution(out: Path, ladder: pd.DataFrame):
106 − """Mean AUC and Δ log loss by horizon (all firm-quarters, no selection)."""
107 − fig, axes = plt.subplots(1, 2, figsize=(6.4, 2.5))
108 − for fam, c in (("logistic", SERIES["daily"]), ("lightgbm", SERIES["1m"])):
109 − s = ladder[ladder.family == fam].sort_values("h", ascending=False)
110 − axes[0].plot(-s["h"], s["auc_a"], marker="o", ms=4, mec="white", lw=1.6, color=c, label=f"{fam}: B5")
111 − axes[0].plot(-s["h"], s["auc_b"], marker="s", ms=3.5, mec="white", lw=1.2, ls="--", color=c, label=f"{fam}: B2")
112 − axes[1].errorbar(-s["h"], s["d_logloss"], yerr=[s["d_logloss"] - s["ci_lo"], s["ci_hi"] - s["d_logloss"]], fmt="o-", ms=4, lw=1.4,
113 − color=c, capsize=2, label=fam)
114 − axes[0].set_xlabel("Trading days before the release (−h)"); axes[0].set_ylabel("Test AUC"); axes[0].legend(fontsize=6); axes[0].grid()
115 − axes[1].axhline(0, color="#8a8a86", lw=0.8); axes[1].set_xlabel("Trading days before the release (−h)")
116 − axes[1].set_ylabel("Δ log loss B5 − B2"); axes[1].legend(fontsize=7); axes[1].grid()
171 + lad = ladder[ladder.h > 0]
172 + hs = [40, 20, 10, 5, 1]
173 + pos = {h: i for i, h in enumerate(hs)}
174 + fig, axes = plt.subplots(1, 2, figsize=(W2, 2.9))
175 + ax = axes[0]
176 + for fam, b, a in (("logistic", "B2", "B5"), ("lightgbm", "LB2", "LB5")):
177 + s = lad[lad.family == fam].sort_values("h", ascending=False)
178 + x = [pos[h] for h in s["h"]]
179 + ax.plot(x, s["auc_b"], marker=STYLE[b]["marker"], ls=STYLE[b]["ls"], color=STYLE[b]["color"], mfc="white", label="B2 (+ daily)" if fam == "logistic" else None)
180 + ax.plot(x, s["auc_a"], marker=STYLE[a]["marker"], ls=STYLE[a]["ls"], color=STYLE[a]["color"], mfc="white", label="B5 (+ 1-min)" if fam == "logistic" else None)
181 + ax.text(len(hs) - 0.9, float(s["auc_b"].iloc[-1]) + 0.012, "gradient-boosted trees" if fam == "lightgbm" else "logistic", ha="right", va="bottom", fontsize=7.5, color=INK2)
182 + ax.set_xlabel("Trading days before the release"); ax.set_ylabel("Test AUC"); _panel(ax, "A", "Discrimination by horizon")
183 + ax.legend(loc="center", fontsize=7.5); ax.set_ylim(0.70, 0.84)
184 + ax = axes[1]
185 + for fam, mk, c, lab, off in (("logistic", "o", INK, "logistic", -0.08), ("lightgbm", "D", ACCENT, "gradient-boosted trees", 0.08)):
186 + s = lad[lad.family == fam].sort_values("h", ascending=False)
187 + x = np.array([pos[h] for h in s["h"]]) + off
188 + _errorbar(ax, x, s["d_logloss"], s["ci_lo"], s["ci_hi"], fmt=mk, color=c, mfc="white", ls="-", lw=0.9, label=lab)
189 + ax.axhline(0, color=INK, lw=0.7); ax.set_xlabel("Trading days before the release")
190 + ax.set_ylabel("$\\Delta$ log loss, B5 $-$ B2"); _panel(ax, "B", "The 1-minute increment by horizon"); ax.legend(loc="upper right", fontsize=7.5)
191 + ax.yaxis.set_major_formatter(ticker.FormatStrFormatter("%+.3f"))
192 + for a_ in axes:
193 + a_.set_xticks(range(len(hs))); a_.set_xticklabels([str(h) for h in hs]); a_.set_xlim(-0.5, len(hs) - 0.5)
194 + fig.subplots_adjust(wspace=0.32)
117 195 _save(fig, out, "fig06_signal_evolution")
118 196
119 197
198 +# ----------------------------------------------------------------------------------------------------- Figure 7
120 199 def fig_heterogeneity(out: Path, sub: pd.DataFrame, groups: list[str]):
200 + labels = {"all": "All firm-quarters", "size_small": "Size: bottom tercile", "size_large": "Size: top tercile",
201 + "illiq_high": "Illiquidity: top tercile", "illiq_low": "Illiquidity: bottom tercile", "lev_high": "Leverage: top tercile",
202 + "lev_low": "Leverage: bottom tercile", "loss_firms": "Loss-making firms", "profit_firms": "Profitable firms",
203 + "covid_2020": "2020", "ex_covid": "Excluding 2020", "window_clean": "Window excludes previous release",
204 + "window_straddles_prev_release": "Window contains previous release", "dense_bars": "Dense bars ($\\geq$ 98%)",
205 + "sparse_bars": "Sparser bars", "sector_finance": "Financials", "sector_nonfinance": "Non-financials",
206 + "after_close_release": "Release after close", "before_open_release": "Release before open"}
121 207 s = sub[(sub.family == "lightgbm") & sub.subgroup.isin(groups)].set_index("subgroup").reindex(groups).reset_index().dropna(subset=["n"])
122 − fig, ax = plt.subplots(figsize=(6.0, 2.8))
208 + fig, ax = plt.subplots(figsize=(W2, 3.6))
123 209 y = np.arange(len(s))
124 − ax.hlines(y, s["ci_lo"], s["ci_hi"], color="#c3c2b7", lw=2)
125 − ax.plot(s["mean"], y, "o", color=SERIES["1m"], ms=5, mec="white")
210 + ax.hlines(y, s["ci_lo"], s["ci_hi"], color=INK, lw=0.9)
211 + ax.plot(s["mean"], y, "D", color=ACCENT, ms=4.5, mfc="white")
212 + xmax = s["ci_hi"].max()
126 213 for yi, r in zip(y, s.itertuples()):
127 − ax.text(r.ci_hi, yi, f" n={int(r.n):,}", va="center", fontsize=6, color="#52514e")
128 − ax.axvline(0, color="#8a8a86", lw=0.8)
129 − ax.set_yticks(y); ax.set_yticklabels([g.replace("_", " ") for g in s["subgroup"]]); ax.invert_yaxis()
130 − ax.set_xlabel("Δ log loss LB5 − LB2 (test period), 95 % block-bootstrap CI"); ax.grid(axis="x")
214 + ax.text(xmax + 0.0004, yi, f"{int(r.n):,}", va="center", ha="left", fontsize=7, color=INK2)
215 + ax.text(xmax + 0.0004, -1.0, "$N$", ha="left", va="center", fontsize=7, color=INK2)
216 + ax.axvline(0, color=INK, lw=0.7)
217 + ax.set_yticks(y); ax.set_yticklabels([labels.get(g, g) for g in s["subgroup"]]); ax.invert_yaxis()
218 + ax.set_xlabel("$\\Delta$ log loss, LB5 $-$ LB2 (test period); 95% block-bootstrap interval")
219 + ax.xaxis.set_major_formatter(ticker.FormatStrFormatter("%+.3f"))
220 + ax.spines["left"].set_visible(False); ax.tick_params(axis="y", length=0)
221 + ax.set_ylim(len(s) - 0.4, -1.4)
131 222 _save(fig, out, "fig07_heterogeneity")
132 223
133 224
225 +# ----------------------------------------------------------------------------------------------------- Figure 8
134 226 def fig_rolling(out: Path, wf: pd.DataFrame, wfc: pd.DataFrame):
135 − fig, axes = plt.subplots(1, 2, figsize=(6.4, 2.5))
136 − for m in ("LB2", "LB5", "B2", "B5"):
227 + fig, axes = plt.subplots(1, 2, figsize=(W2, 2.9))
228 + ax = axes[0]
229 + for m in ("B2", "B5", "LB2", "LB5"):
137 230 s = wf[wf.model == m].sort_values("year")
138 231 if len(s):
139 − axes[0].plot(s["year"], s["auc"], marker="o", ms=3.5, mec="white", lw=1.4, color=MODEL_COLOR[m], ls="-" if m.startswith("L") else "--", label=m)
140 − axes[0].set_ylabel("Out-of-fold AUC (annual refits)"); axes[0].legend(fontsize=6, ncol=2); axes[0].grid()
141 − for fam, c in (("lightgbm", SERIES["1m"]), ("logistic", SERIES["daily"])):
142 − s = wfc[(wfc.family == fam) & (wfc.year != "all")].copy(); s["year"] = s["year"].astype(int); s = s.sort_values("year")
143 − axes[1].errorbar(s["year"], s["d_logloss"], yerr=[s["d_logloss"] - s["ci_lo"], s["ci_hi"] - s["d_logloss"]], fmt="o-", ms=3.5, lw=1.2, capsize=2, color=c, label=fam)
144 − axes[1].axhline(0, color="#8a8a86", lw=0.8); axes[1].set_ylabel("Δ log loss B5 − B2 by year"); axes[1].legend(fontsize=7); axes[1].grid()
232 + st = STYLE[m]
233 + ax.plot(s["year"], s["auc"], marker=st["marker"], ls=st["ls"], color=st["color"], mfc="white", label=st["label"])
234 + ax.set_ylabel("Out-of-fold AUC"); ax.set_xlabel("Year of cutoff"); _panel(ax, "A", "Annual expanding-window refits")
235 + ax.legend(ncol=2, fontsize=7, loc="lower center"); ax.set_ylim(0.66, 0.86)
236 + ax = axes[1]
237 + for fam, mk, c, lab, off in (("logistic", "o", INK, "logistic", -0.12), ("lightgbm", "D", ACCENT, "gradient-boosted trees", 0.12)):
238 + s = wfc[(wfc.family == fam) & (wfc.year.astype(str) != "all")].copy(); s["year"] = s["year"].astype(int); s = s.sort_values("year")
239 + _errorbar(ax, s["year"] + off, s["d_logloss"], s["ci_lo"], s["ci_hi"], fmt=mk, color=c, mfc="white", ls="none", label=lab)
240 + ax.axhline(0, color=INK, lw=0.7); ax.set_ylabel("$\\Delta$ log loss, B5 $-$ B2"); ax.set_xlabel("Year of cutoff")
241 + _panel(ax, "B", "The 1-minute increment by year"); ax.legend(loc="upper left"); ax.yaxis.set_major_formatter(ticker.FormatStrFormatter("%+.3f"))
242 + for a in axes:
243 + a.set_xticks(range(2016, 2027, 2))
244 + fig.subplots_adjust(wspace=0.32)
145 245 _save(fig, out, "fig08_rolling")
146 246
147 247
248 +# ----------------------------------------------------------------------------------------------------- Figure 9
148 249 def fig_economic(out: Path, port: pd.DataFrame, alphas: dict):
149 − fig, ax = plt.subplots(figsize=(4.2, 2.6))
250 + fig, ax = plt.subplots(figsize=(W1 + 0.6, 2.7))
150 251 dm = port.groupby("decile")["ret"].mean() * 100
151 − ax.bar(dm.index, dm.values, color=SERIES["daily"], width=0.8)
152 − ax.axhline(0, color="#8a8a86", lw=0.8); ax.set_xlabel("Decile of ex-ante deterioration score (10 = most likely)")
153 − ax.set_ylabel("Mean monthly return (%)"); ax.grid(axis="y")
154 − ax.set_title(f"L/S 1−10 alpha: {alphas['gross']['alpha_monthly']*100:+.2f}% (t={alphas['gross']['alpha_t']:.1f}) gross, "
155 − f"{alphas['net20bps']['alpha_monthly']*100:+.2f}% net of 20 bps", fontsize=7, loc="left")
252 + se = port.groupby("decile")["ret"].std() / np.sqrt(port.groupby("decile")["ret"].count()) * 100
253 + ax.bar(dm.index, dm.values, color=INK3, width=0.7, edgecolor="none")
254 + ax.errorbar(dm.index, dm.values, yerr=1.96 * se.values, fmt="none", ecolor=INK, elinewidth=0.7, capsize=2)
255 + ax.axhline(0, color=INK, lw=0.7); ax.set_xticks(range(1, 11))
256 + ax.set_xlabel("Decile of ex-ante deterioration score (10 = most likely)"); ax.set_ylabel("Mean monthly return (%)")
257 + g, n = alphas["gross"], alphas["net20bps"]
258 + ax.set_title(f"L/S (1$-$10) FF5+MOM alpha: {100 * g['alpha_monthly']:+.2f}% per month ($t={g['alpha_t']:.2f}$) gross, "
259 + f"{100 * n['alpha_monthly']:+.2f}% net of 20 bps", loc="left", fontsize=7.5)
156 260 _save(fig, out, "fig09_economic")
157 261
Menu

Theme