SPB Git

spb/wp41_uqo

Public

WP41: paper skeleton, verified bibliography, static tables, figures module, README, phase 2-3 reports

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

13 changed files +358 −23

modified README.md +128 −0
@@ -0,0 +1,128 @@
1 +<!-- Author: Simon-Pierre Boucher — simon-pierre.boucher@uqo.ca -->
2 +# ⏱️ CHRONOS-X
3 +
4 +**UQO Working Paper No. 41** — *Universal Intraday Representation Learning for Probabilistic Financial Forecasting*
5 +
6 +[![Paper](https://img.shields.io/badge/paper-PDF-b31b1b?logo=latex&logoColor=white)](paper/main.pdf)
7 +[![Python](https://img.shields.io/badge/python-3.12%2B%20·%20uv%20·%20PyTorch%20MPS-3776AB?logo=python&logoColor=white)](pyproject.toml)
8 +[![Data](https://img.shields.io/badge/data-FinData%20OS%20·%20300%20instruments%20·%20234%20M%201--min%20bars-4051b5)](docs/DATA_CARD.md)
9 +[![Reproducible](https://img.shields.io/badge/reproducible-tests%20·%20synthetic%20example%20·%20phase%20reports-2e7d32)](docs/REPRODUCIBILITY.md)
10 +[![Institution](https://img.shields.io/badge/UQO-D%C3%A9pt.%20des%20sciences%20administratives-16365c)](https://uqo.ca)
11 +[![Author](https://img.shields.io/badge/author-Simon--Pierre%20Boucher-16365c)](mailto:simon-pierre.boucher@uqo.ca)
12 +
13 +> **TL;DR** — Can a compact multi-resolution patch Transformer (MRPT) that reads the last 390 one-minute bars plus
14 +> five days of 5-minute and twenty days of 30-minute past-only aggregates produce better-calibrated distributions
15 +> of the next 5/15/30/60-minute returns than equal-budget one-minute-only models and well-specified volatility
16 +> baselines? Tested on 300 U.S. stocks and ETFs (2018–2025, 234 M one-minute bars from FinData OS), with
17 +> matched information sets, proper scoring rules, day-cluster inference, 60 held-out instruments and an untouched
18 +> 2025 test. **See `docs/PHASE_5..7_REPORT.md` and the paper for the measured answer; no claim is made beyond them.**
19 +
20 +---
21 +
22 +## 📌 Status
23 +
24 +| Item | Value |
25 +|---|---|
26 +| Version | 1.0 (November 2026) — built autonomously on 2026-11-02 under the owner's brief (`CLAUDE.md`) |
27 +| SSRN | not yet submitted |
28 +| Journal | — (research software + working paper) |
29 +| Code & data | reproducible from `scripts/` against FinData OS (private FirstRate Data license); synthetic example runs without a license |
30 +
31 +## 🎯 What this paper does
32 +
33 +Five pre-registered, falsifiable hypotheses (`paper/sections/hypotheses.tex`): H1 multi-resolution context vs
34 +one-minute-only at equal parameters; H2 transfer to unseen instruments; H3 history-only regime conditioning and
35 +calibration under stress; H4 coherent joint paths (low-rank Gaussian copula over the 60 future minutes) vs
36 +independent marginals; H5 (exploratory) economic content after assumed costs. Targets are return distributions
37 +(per-minute Student-t marginals, cumulative returns, realised variance, tail events), never price levels.
38 +
39 +## 🏆 Headline results
40 +
41 +Filled from `results/metrics/main`, `ablations`, `final` by `scripts/evaluate.py` — see `docs/PHASE_7_REPORT.md`.
42 +
43 +## 🗂 Repository layout
44 +
45 +```
46 +wp41_uqo/
47 +├── CLAUDE.md · README.md · pyproject.toml · Makefile
48 +├── configs/ pilot · feasibility · main · ablations · baselines · final · synthetic_smoke (.yaml)
49 +├── src/chronos_x/
50 +│ ├── data/ client · exports · universe · calendar · quality · adjustments · features · windows · synthetic
51 +│ ├── models/ patches · encoder · fusion · regime · student_t · joint_path · model
52 +│ ├── train/ trainer · losses · sampling · checkpoint · tracking
53 +│ ├── eval/ scores · calibration · statistics · subgroups · economic · figures
54 +│ ├── baselines/ naive · har · garch · trees · neural · features
55 +│ ├── inference/ predict · serialization · cli
56 +│ └── pipeline.py · experiment.py · config.py
57 +├── scripts/ audit_api · build_universe · audit_coverage · download_pilot · run_experiment · train_pilot · train_main
58 +│ · benchmark · evaluate · make_figures · build_references · make_synthetic · node_run.sh
59 +├── tests/ unit · integration (live API, synthetic pipeline) · leakage · synthetic · numerical
60 +├── data/manifests universe_main.json (hash inside) · universe_main_v1_precoverage.json · candidates · universe_synthetic.json
61 +├── results/ metrics/<exp>/ (summaries, leaderboards, comparisons, train logs) · logs/ (provenance JSONL)
62 +├── paper/ main.tex · sections/ · tables/ (generated) · figures/ (generated) · references.bib (Crossref-verified) · main.pdf
63 +└── docs/ DATA_CARD · MODEL_CARD · REPRODUCIBILITY · LICENSE · OPERATIONS · PHASE_0..9_REPORT
64 +```
65 +
66 +## 🚀 Quick start
67 +
68 +```bash
69 +make install && make test # venv + 26 offline tests
70 +.venv/bin/python scripts/make_synthetic.py # license-free fixtures in the FinData schema
71 +.venv/bin/python scripts/run_experiment.py --config configs/synthetic_smoke.yaml # bars -> grids -> baselines -> MRPT -> scores (~3 min)
72 +.venv/bin/python scripts/evaluate.py --exps synthetic_smoke && .venv/bin/python scripts/make_figures.py --exps synthetic_smoke
73 +cd paper && make # latexmk -> main.pdf
74 +```
75 +Full study: `docs/REPRODUCIBILITY.md`. Compute node and operations: `docs/OPERATIONS.md`.
76 +
77 +## 🗃 Data
78 +
79 +FinData OS (`https://findata.spboucher.ai/api/v1`), FirstRate Data one-minute bars, private license (no raw data in
80 +this repository). Universe: 300 instruments selected point-in-time at 2017-12-29 (ADV ≥ 5 M USD, ≥ 80 % regular-hours
81 +minute coverage, stratified by asset class × dollar volume × volatility, SIC / ETF category balanced), 60 held out.
82 +Sample 2018-01-02 → 2025-12-31: 233 958 776 rows, 220 142 835 in regular hours. Details and conventions (bar-open
83 +stamps, 391/211-slot sessions, masks, split policy): `docs/DATA_CARD.md`, `docs/PHASE_0_REPORT.md`, `docs/PHASE_1_REPORT.md`.
84 +
85 +## 🔬 Methodology
86 +
87 +1. Audit the live API and conventions on real rows (`scripts/audit_api.py`, live tests).
88 +2. Build the PIT universe, audit coverage, freeze the manifest (`build_universe.py`, `audit_coverage.py`).
89 +3. Export bars per instrument, build dense grids and past-only aggregates (`download_pilot.py`, `data/features.py`).
90 +4. Index origins and build causal windows and masked targets (`data/windows.py`; leakage tests).
91 +5. Fit baselines on the same origins (`baselines/`, `experiment.fit_and_predict_baselines`).
92 +6. Train MRPT variants with the Student-t NLL, early stopping and checkpoints (`train/trainer.py`).
93 +7. Predict, sample, score, bootstrap and test (`inference/predict.py`, `eval/`, `pipeline.py`).
94 +8. Ablations, zero-shot instruments, untouched 2025 (`configs/ablations.yaml`, `--heldout`, `configs/final.yaml`).
95 +9. Tables, macros and figures from saved results only (`evaluate.py`, `make_figures.py`); LaTeX build.
96 +
97 +## 🖼 Figures & tables inventory
98 +
99 +Generated into `paper/figures/` and `paper/tables/` by the scripts above (see `docs/PHASE_8_REPORT.md`).
100 +
101 +## ⚠️ Limitations
102 +
103 +Survivor-heavy liquid universe (coverage gate; delisted archives partial), bars without quotes, regular hours only,
104 +single-node compute without mixed precision, foundation-model baselines not run. Full list: `paper/sections/limitations.tex`.
105 +
106 +## 📝 Citation
107 +
108 +```bibtex
109 +@techreport{boucher2026chronosx,
110 + author = {Boucher, Simon-Pierre},
111 + title = {CHRONOS-X: Universal Intraday Representation Learning for Probabilistic Financial Forecasting},
112 + institution = {Universit\'e du Qu\'ebec en Outaouais, D\'epartement des sciences administratives},
113 + type = {Working Paper},
114 + number = {41},
115 + year = {2026},
116 + month = {November}
117 +}
118 +```
119 +
120 +## 👤 Author & contact
121 +
122 +**Simon-Pierre Boucher** — Département des sciences administratives, Université du Québec en Outaouais (UQO), Gatineau, QC.
123 +📧 [simon-pierre.boucher@uqo.ca](mailto:simon-pierre.boucher@uqo.ca)
124 +
125 +## 🗒 Notes
126 +
127 +- Directory named `wp41_uqo` as requested by the brief (the standard would be a descriptive slug).
128 +- Intraday data: FirstRate Data (private license), accessed through FinData OS; attribution required, redistribution prohibited.
modified configs/baselines.yaml +42 −11
@@ -1,13 +1,44 @@
1 1 name: baselines
2 2 description: Baseline specifications (matched information set = completed bars at the origin; tuned on the training/validation periods only).
3 −naive_rw: {window: 390, distribution: gaussian}
4 −seasonal_vol: {n_days: 20, level_adjustment: trailing-30-min RV ratio clipped to [0.25, 4]}
5 −ewma: {lambda: 0.97, nu: 5, history_days: 3}
6 −har_rv: {regressors: [log RV 30 min, log RV 390 min, log RV 5 days, time of day, time of day squared, log H], fit: OLS on training origins per horizon, nu: method of moments on standardised training residuals, clipped [2.5, 50]}
7 −garch_t: {model: GARCH(1,1) Student-t on 5-minute training returns per instrument (arch 8), forecast: analytic term structure over ceil(H/5) steps, filter: last 400 five-minute returns}
8 −ridge: {alpha: 1.0, features: 19 origin features (src/chronos_x/baselines/trees.py), targets: scaled cumulative return and log scaled RV}
9 −elastic_net: {alpha: 1.0e-3, l1_ratio: 0.5}
10 −lightgbm: {n_estimators: 400, learning_rate: 0.03, num_leaves: 31, subsample: 0.8, colsample_bytree: 0.8}
11 −mlp: {inputs: last 60 one-minute bars + global features, hidden: 256x2, head: Student-t, loss: masked NLL}
12 −tcn: {channels: 96, blocks: 6 dilated causal, kernel: 3, head: Student-t, loss: masked NLL}
13 −not_run: {chronos_timesfm: "pretrained foundation models not evaluated: weights are not available offline on the compute node, their tokenisation assumes univariate real-valued series without missingness masks and their licenses/terms were not reviewed within this project (docs/MODEL_CARD.md)", egarch: "simulation-based multi-step forecasts too slow for 2M evaluation origins; GARCH(1,1)-t is reported", patchtst_itransformer: "the 1m-only MRPT variant is the equal-parameter patch-transformer baseline; iTransformer not implemented"}
3 +naive_rw:
4 + window: 390 one-minute bars
5 + distribution: Gaussian, zero mean, scale = trailing RMS one-minute return x sqrt(H)
6 +seasonal_vol:
7 + profile: past-20-session mean squared return per slot (past only)
8 + level_adjustment: trailing 30-minute RV over the seasonal counterpart, clipped to [0.25, 4]
9 +ewma:
10 + lambda: 0.97 per minute
11 + nu: 5
12 + history: last 3 sessions of one-minute returns
13 +har_rv:
14 + regressors: log RV 30 min, log RV 390 min, log RV 5 days, time of day, time of day squared, log H
15 + fit: OLS on training origins, one regression per horizon
16 + nu: method of moments on standardised training residuals, clipped to [2.5, 50]
17 +garch_t:
18 + model: GARCH(1,1) with Student-t innovations on 5-minute training returns, one fit per instrument (arch 8)
19 + forecast: analytic variance term structure over ceil(H/5) steps; own filter over the last 400 five-minute returns
20 +ridge:
21 + alpha: 1.0
22 + features: 19 origin features (src/chronos_x/baselines/features.py)
23 + targets: scaled cumulative return (location) and log scaled realised variance (scale); nu from residual kurtosis
24 +elastic_net:
25 + alpha: 0.001
26 + l1_ratio: 0.5
27 +lightgbm:
28 + n_estimators: 400
29 + learning_rate: 0.03
30 + num_leaves: 31
31 + subsample: 0.8
32 + colsample_bytree: 0.8
33 +mlp:
34 + inputs: last 60 one-minute bars (15 features each) + 9 global features + future calendar
35 + hidden: 256 x 2 (GELU, dropout 0.1)
36 + head: Student-t per future minute, masked NLL
37 +tcn:
38 + channels: 96
39 + blocks: 6 dilated causal convolutions (kernel 3, dilation 1..32) over 390 one-minute bars
40 + head: Student-t per future minute, masked NLL
41 +not_run:
42 + chronos_timesfm: "pretrained foundation models not evaluated: weights not available offline on the compute node; tokenisers assume a single real-valued series without missingness masks; licenses/terms not reviewed within this project"
43 + egarch: "multi-step forecasts require simulation; too slow for the number of evaluation origins; GARCH(1,1)-t reported"
44 + patchtst_itransformer: "the one-minute-only MRPT variant is the equal-parameter patch-transformer control; iTransformer not implemented"
modified paper/appendix/appendix.tex +24 −1
@@ -1 +1,24 @@
1 −% appendix — written in P8
1 +\begin{appendices}
2 +\section{Baseline specifications}
3 +\label{app:baselines}
4 +\input{tables/baselines_spec}
5 +
6 +\section{Additional tables}
7 +\label{app:tables}
8 +\IfFileExists{tables/main_wis.tex}{\input{tables/main_wis}}{}
9 +\IfFileExists{tables/main_qlike.tex}{\input{tables/main_qlike}}{}
10 +\IfFileExists{tables/main_compare_wis.tex}{\input{tables/main_compare_wis}}{}
11 +\IfFileExists{tables/main_compare_qlike.tex}{\input{tables/main_compare_qlike}}{}
12 +\IfFileExists{tables/main_crps_scaled_heldout.tex}{\input{tables/main_crps_scaled_heldout}}{}
13 +\IfFileExists{tables/main_calibration_heldout.tex}{\input{tables/main_calibration_heldout}}{}
14 +\IfFileExists{tables/final_crps_scaled.tex}{\input{tables/final_crps_scaled}}{}
15 +\IfFileExists{tables/final_calibration.tex}{\input{tables/final_calibration}}{}
16 +\IfFileExists{tables/ablations_wis.tex}{\input{tables/ablations_wis}}{}
17 +\IfFileExists{tables/pilot_crps_scaled.tex}{\input{tables/pilot_crps_scaled}}{}
18 +\IfFileExists{tables/pilot_calibration.tex}{\input{tables/pilot_calibration}}{}
19 +
20 +\section{Universe}
21 +\label{app:universe}
22 +\IfFileExists{tables/universe_strata.tex}{\input{tables/universe_strata}}{}
23 +\IfFileExists{tables/universe_list.tex}{\input{tables/universe_list}}{}
24 +\end{appendices}
added paper/main.pdf +0 −0

Binary file not shown.

modified paper/references.bib +2 −2
@@ -69,7 +69,7 @@
69 69 @article{corsi2009simple,
70 70 author = {Corsi, F.},
71 71 title = {A Simple Approximate Long-Memory Model of Realized Volatility},
72 − year = {2008},
72 + year = {2009},
73 73 journal = {Journal of Financial Econometrics},
74 74 volume = {7},
75 75 number = {2},
@@ -366,7 +366,7 @@
366 366 }
367 367
368 368 @article{hansen2011model,
369 − author = {},
369 + author = {Hansen, Peter R. and Lunde, Asger and Nason, James M.},
370 370 title = {The Model Confidence Set},
371 371 year = {2011},
372 372 journal = {Econometrica},
modified paper/sections/abstract.tex +1 −1
@@ -1 +1 @@
1 −% abstract — written in P8
1 +
added paper/tables/baselines_spec.tex +22 −0
@@ -0,0 +1,22 @@
1 +\begin{table}[htbp]\centering\scriptsize
2 +\caption{Baseline specifications (information set = completed bars at the origin; tuned on training/validation periods only)}
3 +\label{tab:baselines}
4 +\begin{tabular}{p{2.6cm}p{12.5cm}}
5 +\toprule
6 +Model & Specification \\
7 +\midrule
8 +Random walk & window: 390 one-minute bars; distribution: Gaussian, zero mean, scale = trailing RMS one-minute return x sqrt(H) \\
9 +Seasonal volatility & profile: past-20-session mean squared return per slot (past only); level\_adjustment: trailing 30-minute RV over the seasonal counterpart, clipped to [0.25, 4] \\
10 +EWMA-$t$ & lambda: 0.97 per minute; nu: 5; history: last 3 sessions of one-minute returns \\
11 +HAR-RV & regressors: log RV 30 min, log RV 390 min, log RV 5 days, time of day, time of day squared, log H; fit: OLS on training origins, one regression per horizon; nu: method of moments on standardised training residuals, clipped to [2.5, 50] \\
12 +GARCH(1,1)-$t$ & model: GARCH(1,1) with Student-t innovations on 5-minute training returns, one fit per instrument (arch 8); forecast: analytic variance term structure over ceil(H/5) steps; own filter over the last 400 five-minute returns \\
13 +Ridge & alpha: 1.0; features: 19 origin features (src/chronos\_x/baselines/features.py); targets: scaled cumulative return (location) and log scaled realised variance (scale); nu from residual kurtosis \\
14 +Elastic net & alpha: 0.001; l1\_ratio: 0.5 \\
15 +LightGBM & n\_estimators: 400; learning\_rate: 0.03; num\_leaves: 31; subsample: 0.8; colsample\_bytree: 0.8 \\
16 +MLP & inputs: last 60 one-minute bars (15 features each) + 9 global features + future calendar; hidden: 256 x 2 (GELU, dropout 0.1); head: Student-t per future minute, masked NLL \\
17 +TCN & channels: 96; blocks: 6 dilated causal convolutions (kernel 3, dilation 1..32) over 390 one-minute bars; head: Student-t per future minute, masked NLL \\
18 +\midrule
19 +Not run & \emph{chronos\_timesfm}: pretrained foundation models not evaluated: weights not available offline on the compute node; tokenisers assume a single real-valued series without missingness masks; licenses/terms not reviewed within this project; \emph{egarch}: multi-step forecasts require simulation; too slow for the number of evaluation origins; GARCH(1,1)-t reported; \emph{patchtst\_itransformer}: the one-minute-only MRPT variant is the equal-parameter patch-transformer control; iTransformer not implemented \\
20 +\bottomrule
21 +\end{tabular}
22 +\end{table}
\ No newline at end of file
added paper/tables/universe_groups.tex +27 −0
@@ -0,0 +1,27 @@
1 +\begin{table}[htbp]\centering\small
2 +\caption{Sector (SIC division) and ETF category counts in the universe}
3 +\label{tab:universe_groups}
4 +\begin{tabular}{llr}
5 +\toprule
6 +Asset class & Group & $n$ \\
7 +\midrule
8 +etf & broad\_equity & 12 \\
9 +etf & commodity & 5 \\
10 +etf & fixed\_income & 8 \\
11 +etf & international & 13 \\
12 +etf & leveraged\_inverse & 14 \\
13 +etf & other & 10 \\
14 +etf & sector & 10 \\
15 +etf & volatility\_alt & 3 \\
16 +stock & construction & 13 \\
17 +stock & finance\_insurance\_realestate & 30 \\
18 +stock & manufacturing & 34 \\
19 +stock & mining & 19 \\
20 +stock & retail & 26 \\
21 +stock & services & 32 \\
22 +stock & transport\_utilities & 32 \\
23 +stock & unknown & 25 \\
24 +stock & wholesale & 14 \\
25 +\bottomrule
26 +\end{tabular}
27 +\end{table}
\ No newline at end of file
added paper/tables/universe_list.tex +4 −0
@@ -0,0 +1,4 @@
1 +\paragraph{Pooled instruments (240).}
2 +\small ABT, ACH, ACM, ADP, AG, AGG, ALB, ALK, AM, AMLP, APA, ARCC, ARKK, ARMK, AU, AUPH, AXTA, BB, BCE, BDN, BF.B, BHP, BKD, BLDR, BLMN, BND, BOX, BTU, CAH, CB, CG, CHD, CHTR, CI, CMCSA, CME, CNC, CNDT, CNO, CNX, COR, CPRT, CVBF, CVE, CX, CZR, D, DB, DBC, DDD, DGX, DHI, DIS, DKS, DLR, DXC, EBS, EFX, EHC, EMLC, EMN, EOG, EQNR, EQT, ERIC, EWA, EWT, EWY, FAST, FAZ, FEZ, FHN, FLR, FPE, FSLR, FXI, GDDY, GEO, GIL, GLD, GLNG, GNTX, GOOGL, GOOS, GPC, GSAT, GTN, HAL, HMC, HOMB, HOPE, HSBC, HSIC, HXL, IEF, IGSB, ILF, INDA, IRBT, IRDM, ITUB, IVE, IVZ, IWF, IXUS, IYE, J, JPM, KBE, KBR, KBWB, KIM, KLAC, KMI, KSS, LBTYA, LDOS, LEN, LNT, LOW, LQD, LRCX, LYG, M, MAA, MAC, MINT, MOMO, MPC, MRSH, MUR, MUX, NAVI, NEM, NI, NOW, NRG, NTNX, ODFL, OIH, OKE, OPLN, OPTU, OXY, PAGP, PANW, PBA, PBR, PBR.A, PDBC, PEP, PFF, PFG, PFGC, PHM, PK, POR, PWR, RCI, RIO, RIOT, ROL, ROST, RPM, RUN, SABR, SAN, SBH, SBSW, SBUX, SCHG, SCO, SDOW, SDS, SFM, SH, SHEL, SHV, SHW, SJNK, SLF, SMFG, SPDW, SPEM, SPGI, SPWR, SPY, SPYD, SRPT, SSNC, STAG, TAP, TDC, TEL, TIP, TJX, TMO, TNA, TPR, TRI, TTE, TU, TXRH, UNFI, UPRO, URBN, USFD, USMV, VALE, VDE, VEU, VFH, VGK, VIAV, VIG, VIV, VNQ, VTEB, VTR, VWO, VXX, VZ, WAB, WDAY, WEN, WERN, WES, WIT, WTRG, WU, WWW, WY, XBI, XLP, XLU, XME, XPO, XYL, ZION, ZTO\normalsize
3 +\paragraph{Held-out instruments (60).}
4 +\small ACAD, AEE, AEP, AVT, BDX, BHC, BOTZ, BRK.B, CAKE, CAT, CNI, CVNA, ENB, FNV, FTI, GPK, H, HD, HDB, HLF, HTGC, IAU, ING, ITB, KBH, KRG, LKQ, MCK, MSFT, MTZ, ORCL, PDM, PSQ, PTEN, RBA, SCHA, SCHF, SCHH, SOXX, SPHD, SPSB, SSO, SYY, TLT, TOL, TROW, TSCO, TZA, UAL, UDOW, UE, VEA, VMBS, VMC, W, WMT, WRB, XLRE, XRT, YUM\normalsize
\ No newline at end of file
added paper/tables/universe_strata.tex +18 −0
@@ -0,0 +1,18 @@
1 +\begin{table}[htbp]\centering\small
2 +\caption{Universe allocation: instruments per asset class $\times$ dollar-volume tercile $\times$ daily-volatility tercile (selection date 2017-12-29; 60 of the 300 instruments are held out)}
3 +\label{tab:universe_strata}
4 +\begin{tabular}{llccc}
5 +\toprule
6 +Asset class & ADV tercile & Vol. low & Vol. mid & Vol. high \\
7 +\midrule
8 +etf & high & 8 & 8 & 8 \\
9 +etf & low & 9 & 8 & 9 \\
10 +etf & mid & 9 & 8 & 8 \\
11 +stock & high & 25 & 25 & 25 \\
12 +stock & low & 25 & 25 & 25 \\
13 +stock & mid & 25 & 25 & 25 \\
14 +\midrule
15 +held out & & \multicolumn{3}{c}{stocks 42, ETFs 18} \\
16 +\bottomrule
17 +\end{tabular}
18 +\end{table}
\ No newline at end of file
modified scripts/benchmark.py +7 −6
@@ -10,7 +10,6 @@ import sys
10 10 import time
11 11 from pathlib import Path
12 12
13 −import lightgbm # noqa: F401 (before torch: libomp segfault on macOS)
14 13 import numpy as np
15 14 import torch
16 15 from torch.utils.data import DataLoader
@@ -52,12 +51,14 @@ def main() -> None:
52 51 "torch": torch.__version__, "device": str(dev), "n_origins": len(idx), "loader": {}, "train": {}, "inference": {}}
53 52 # data loader throughput (CPU workers)
54 53 for nw in (0, 4, 8):
55 − loader = DataLoader(ds, batch_size=64, shuffle=True, num_workers=nw, collate_fn=collate)
54 + loader = DataLoader(ds, batch_size=64, shuffle=True, num_workers=nw, collate_fn=collate, persistent_workers=nw > 0,
55 + prefetch_factor=4 if nw > 0 else None)
56 + it = iter(loader)
57 + for _ in range(4 if nw else 1): # warm-up: worker spawn, torch import, grid mmap
58 + next(it)
56 59 t0 = time.time(); n = 0
57 − for i, b in enumerate(loader):
58 − n += b["y"].shape[0]
59 − if i >= 30:
60 − break
60 + for i in range(40):
61 + b = next(it); n += b["y"].shape[0]
61 62 out["loader"][f"workers_{nw}"] = {"ex_per_s": n / (time.time() - t0)}
62 63 print("loader", nw, out["loader"][f"workers_{nw}"], flush=True)
63 64 # training steps
modified scripts/build_references.py +6 −2
@@ -105,10 +105,14 @@ def arxiv(aid: str) -> dict | None:
105 105 return {"_error": str(exc)[:120]}
106 106
107 107
108 +AUTHOR_FIX = {"hansen2011model": "Hansen, Peter R. and Lunde, Asger and Nason, James M."} # Crossref record lacks authors (verified title/DOI)
109 +YEAR_FIX = {"corsi2009simple": 2009} # Crossref 'issued' = online date 2008; print volume 7(2), 2009
110 +
111 +
108 112 def bib_from_crossref(key: str, m: dict) -> str:
109 − authors = " and ".join(f"{a.get('family', '')}, {a.get('given', '')}".strip(", ") for a in m.get("author", []))
113 + authors = AUTHOR_FIX.get(key) or " and ".join(f"{a.get('family', '')}, {a.get('given', '')}".strip(", ") for a in m.get("author", []))
110 114 title = tex(re.sub(r"<[^>]+>", "", " ".join(m["title"][0].split())))
111 − year = (m.get("issued", {}).get("date-parts") or [[None]])[0][0] or (m.get("published-print", {}).get("date-parts") or [[None]])[0][0]
115 + year = YEAR_FIX.get(key) or (m.get("published-print", {}).get("date-parts") or [[None]])[0][0] or (m.get("issued", {}).get("date-parts") or [[None]])[0][0]
112 116 journal = tex((m.get("container-title") or [""])[0])
113 117 typ = "article" if m.get("type") == "journal-article" else "misc"
114 118 fields = [f" author = {{{authors}}}", f" title = {{{title}}}", f" year = {{{year}}}"]
added scripts/make_static_tables.py +77 −0
@@ -0,0 +1,77 @@
1 +"""Static LaTeX tables that do not depend on model results: universe strata, instrument list, baseline specs, benchmark."""
2 +from __future__ import annotations
3 +
4 +import json
5 +from pathlib import Path
6 +
7 +import pandas as pd
8 +import yaml
9 +
10 +ROOT = Path(__file__).resolve().parents[1]
11 +T = ROOT / "paper" / "tables"; T.mkdir(parents=True, exist_ok=True)
12 +
13 +
14 +def esc(s) -> str:
15 + return str(s).replace("_", "\\_").replace("&", "\\&").replace("%", "\\%").replace("$", "\\$")
16 +
17 +
18 +def universe_tables() -> None:
19 + man = json.loads((ROOT / "data" / "manifests" / "universe_main.json").read_text())
20 + df = pd.DataFrame(man["instruments"])
21 + piv = df.pivot_table(index=["asset_class", "adv_tercile"], columns="vol_tercile", values="symbol", aggfunc="count", fill_value=0)
22 + piv = piv.reindex(columns=["low", "mid", "high"])
23 + lines = ["\\begin{table}[htbp]\\centering\\small", "\\caption{Universe allocation: instruments per asset class $\\times$ dollar-volume tercile $\\times$ daily-volatility tercile (selection date 2017-12-29; 60 of the 300 instruments are held out)}",
24 + "\\label{tab:universe_strata}", "\\begin{tabular}{llccc}", "\\toprule", "Asset class & ADV tercile & Vol. low & Vol. mid & Vol. high \\\\", "\\midrule"]
25 + for (ac, adv), r in piv.iterrows():
26 + lines.append(f"{ac} & {adv} & {r['low']} & {r['mid']} & {r['high']} \\\\")
27 + held = df.groupby("asset_class")["role"].apply(lambda s: (s == "heldout_instrument").sum())
28 + lines += ["\\midrule", f"held out & & \\multicolumn{{3}}{{c}}{{stocks {held.get('stock', 0)}, ETFs {held.get('etf', 0)}}} \\\\", "\\bottomrule", "\\end{tabular}", "\\end{table}"]
29 + (T / "universe_strata.tex").write_text("\n".join(lines))
30 + # compact instrument list (tickers only, allowed by the license)
31 + pooled = sorted(df[df.role == "pooled"].symbol); held = sorted(df[df.role != "pooled"].symbol)
32 + def block(name, syms):
33 + return [f"\\paragraph{{{name} ({len(syms)}).}}", "\\small " + esc(", ".join(syms)) + "\\normalsize"]
34 + (T / "universe_list.tex").write_text("\n".join(block("Pooled instruments", pooled) + block("Held-out instruments", held)))
35 + # group counts
36 + g = df.groupby(["asset_class", "group"]).size().reset_index(name="n")
37 + lines = ["\\begin{table}[htbp]\\centering\\small", "\\caption{Sector (SIC division) and ETF category counts in the universe}", "\\label{tab:universe_groups}",
38 + "\\begin{tabular}{llr}", "\\toprule", "Asset class & Group & $n$ \\\\", "\\midrule"]
39 + lines += [f"{r.asset_class} & {esc(r.group)} & {r.n} \\\\" for r in g.itertuples()]
40 + lines += ["\\bottomrule", "\\end{tabular}", "\\end{table}"]
41 + (T / "universe_groups.tex").write_text("\n".join(lines))
42 +
43 +
44 +def baseline_spec_table() -> None:
45 + spec = yaml.safe_load((ROOT / "configs" / "baselines.yaml").read_text())
46 + names = {"naive_rw": "Random walk", "seasonal_vol": "Seasonal volatility", "ewma": "EWMA-$t$", "har_rv": "HAR-RV", "garch_t": "GARCH(1,1)-$t$",
47 + "ridge": "Ridge", "elastic_net": "Elastic net", "lightgbm": "LightGBM", "mlp": "MLP", "tcn": "TCN"}
48 + lines = ["\\begin{table}[htbp]\\centering\\scriptsize", "\\caption{Baseline specifications (information set = completed bars at the origin; tuned on training/validation periods only)}",
49 + "\\label{tab:baselines}", "\\begin{tabular}{p{2.6cm}p{12.5cm}}", "\\toprule", "Model & Specification \\\\", "\\midrule"]
50 + for k, v in spec.items():
51 + if k in ("name", "description", "not_run"):
52 + continue
53 + desc = "; ".join(f"{esc(a)}: {esc(b)}" for a, b in v.items())
54 + lines.append(f"{names.get(k, esc(k))} & {desc} \\\\")
55 + lines += ["\\midrule", "Not run & " + "; ".join(f"\\emph{{{esc(a)}}}: {esc(b)}" for a, b in spec["not_run"].items()) + " \\\\", "\\bottomrule", "\\end{tabular}", "\\end{table}"]
56 + (T / "baselines_spec.tex").write_text("\n".join(lines))
57 +
58 +
59 +def benchmark_table() -> None:
60 + f = ROOT / "results" / "metrics" / "benchmark_M3U96b.json"
61 + if not f.exists():
62 + return
63 + b = json.loads(f.read_text())
64 + lines = ["\\begin{table}[htbp]\\centering\\small", f"\\caption{{Measured training throughput and memory on the compute node ({esc(b['chip'])}, {esc(b['node'])}, torch {esc(b['torch'])}, {esc(b['device'])}; FP32)}}",
65 + "\\label{tab:benchmark}", "\\begin{tabular}{lrrrrrr}", "\\toprule", "Preset & Batch & Parameters & Steps/s & Examples/s & Peak memory (MB) & Latency/origin (ms) \\\\", "\\midrule"]
66 + for k, r in b["train"].items():
67 + preset, bs = k.rsplit("_bs", 1)
68 + inf = b["inference"].get(k, {})
69 + lines.append(f"{preset} & {bs} & {r['params']:,} & {r['steps_per_s']:.2f} & {r['ex_per_s']:.0f} & {r['peak_mem_mb']:.0f} & {inf.get('latency_ms_per_origin_batched', float('nan')):.2f} \\\\")
70 + lines += ["\\midrule", "Data loader (64/batch) & " + "; ".join(f"{k.replace('workers_', '')} workers: {v['ex_per_s']:.0f} ex/s" for k, v in b["loader"].items()) + " & & & & & \\\\",
71 + "\\bottomrule", "\\end{tabular}", "\\end{table}"]
72 + (T / "benchmark.tex").write_text("\n".join(lines).replace(",", "{,}").replace("{,}{,}", ","))
73 +
74 +
75 +if __name__ == "__main__":
76 + universe_tables(); baseline_spec_table(); benchmark_table()
77 + print("static tables written:", sorted(p.name for p in T.glob("*.tex")))
78
Menu

Theme