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>
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 | |