SPB Git

spb/wp17_uqo

Public

FIRC-0002 v2.0 (2026-10-28): Russell 3000 extension of 'Does the Language of AI Move Markets?' — commit of the working tree before peer review (large data/ inputs gitignored)

Simon-Pierre Boucher committed (Nov 5, 2026) parent b3eabf6 Browse files

7 changed files +19,793 −180

modified .gitignore +6 −0
@@ -40,3 +40,9 @@ logs/
40 40 # Claude Code local settings
41 41 .claude/
42 42 **/.claude/
43 +
44 +# v2.0 Russell 3000 extension: large inputs stay out of git (factor panel, filing-level scores, prices, shares)
45 +data/raw_russell3000/*
46 +!data/raw_russell3000/universe.json
47 +!data/raw_russell3000/spid_firm_year_signals.csv
48 +data/processed_r3k/*
added AI-Disclosure-Stock-Returns_WP0002_v2.0_2026-10-28.docx +0 −0

Binary file not shown.

added AI-Disclosure-Stock-Returns_WP17_v1.1_2026-10-26.docx +0 −0

Binary file not shown.

modified README.md +31 −180
@@ -1,28 +1,9 @@
1 1 <!-- Authors: Manel Kammoun, Djerrycharli Tandjambianda, Simon-Pierre Boucher — simon-pierre.boucher@uqo.ca -->
2 2 # 🧠 Does the Language of Artificial Intelligence Move Markets?
3 3
4 −**UQO Working Paper No. 17** — *A Semantic Analysis of S&P 500 SEC Filings and the Cross-Section of Stock Returns, 2010–2026*
4 +**UQO Working Paper No. 0002 (FIRC-funded)** — *A Semantic Analysis of Russell 3000 SEC Filings and the Cross-Section of Stock Returns, 2010–2026* — **Version 2.0, October 28, 2026** (Russell 3000 extension of the S&P 500 study, formerly WP 17 v1.1, archived in `docs/paper_v1.1_sp500_20261026/`). Research funded by the FIRC (Fonds institutionnel de recherche et de création, UQO).
5 5
6 −[![Paper](https://img.shields.io/badge/paper-PDF%20(54%20p.)-b31b1b?logo=latex&logoColor=white)](paper/main.pdf)
7 −[![Python](https://img.shields.io/badge/python-3.11%2B-3776AB?logo=python&logoColor=white)](requirements.txt)
8 −[![Data](https://img.shields.io/badge/data-7%2C869%20firm--years%20·%20500%20S%26P%20500%20firms%20·%208--K%20%2B%2010--Q%20embeddings-4051b5)](data/processed/)
9 −[![Reproducible](https://img.shields.io/badge/reproducible-end--to--end%20pipeline%20(12%20scripts)-2e7d32)](scripts/)
10 −[![Institution](https://img.shields.io/badge/UQO-D%C3%A9pt.%20des%20sciences%20administratives-16365c)](https://uqo.ca)
11 −[![Authors](https://img.shields.io/badge/authors-Kammoun%20·%20Tandjambianda%20·%20Boucher-16365c)](mailto:simon-pierre.boucher@uqo.ca)
12 −
13 −> **TL;DR** — We embed the SEC filings of the 500 current S&P 500 constituents (Form 8-K current
14 −> reports for all firms and Form 10-Q reports for 84 of them, filed 2010–2026; no 10-K was parsed in
15 −> the database version used) with a sentence transformer and score each passage by cosine similarity
16 −> to AI “anchor” concepts (adoption, infrastructure, disruption risk). Linked to look-ahead-free
17 −> forward returns (signal of filing year Y → July Y+1 to June Y+2), the panel of 7,869 firm-years shows
18 −> a **weak unconditional relation** (Q5–Q1 ≈ +5 pp/yr, t < 2; FF6 alpha 1.9 %/yr, insignificant) and a
19 −> **post-ChatGPT increase** (+2.5 → +14.6 pp/yr; long–short Sharpe 0.22 → 1.32; +10.1 pp High–Low
20 −> CAR one year after the launch, portfolio t = 2.3). The revision shows that this post-2022 evidence
21 −> is **thin**: it rests on three cohorts and essentially on the 2024 cohort (+38 pp vs +9 and −4 pp;
22 −> without it the spread is +2.6 pp), winsorizing halves the pooled spread (+14.7 → +7.2 pp), an IT
23 −> sector factor absorbs most of the post-period alpha (5.2 → 1.9 %/yr), and sector-by-year fixed
24 −> effects leave the slope insignificant. Reading: a sector-level re-rating of persistent AI exposure
25 −> during the 2024–2026 AI investment cycle, not a stable premium nor a sharp attention shock.
6 +> **TL;DR** — We embed every Form 10-K, 10-Q and 8-K of ~2,500 U.S. firms (S&P 500 ∪ iShares Russell 3000 holdings, 527,548 filings, 2010–2026) with a sentence transformer, score each chunk against AI anchor concepts, and link the filing-year signal to look-ahead-free forward returns (July Y+1 → June Y+2), with S&P 500 members and the full universe reported side by side. In the broad market AI language carries a **modest, persistent premium** (EW Q5–Q1 +5.5 pp/yr, t = 2.4; Fama–MacBeth +3.1 pp/SD, t = 2.8; within-sector +1.5 pp/SD, t = 4.4), strongest among smaller firms **before** 2022. The **post-ChatGPT jump replicates in the S&P 500 (+5.0 → +15.0 pp) but not in the Russell 3000 (+4.9 → +7.9, 2022 cohort negative, placebo break 2022 ranks 8/10, post-launch FF6 alpha −0.3 %/yr)**: it is a large-cap re-rating (largest 1,000 firms +2.9 → +12.8; event-study 1-year H−L +19.5 pp vs +9.9 for the rest). **Disclosed AI projects** (SPID database) carry no premium; **10-K language** carries the strongest.
26 7
27 8 ---
28 9
@@ -30,177 +11,47 @@
30 11
31 12 | Item | Value |
32 13 |---|---|
33 −| Version | **1.1 (October 2026) — revised 2026-10-17 by the editorial-board review; à valider par les co-auteurs** (v1.0 June 2026; `main.tex` reconstructed 2026-10-17) |
34 −| SSRN | not yet submitted |
35 −| Journal | — (draft) |
36 −| Review documents | [`docs/REVIEW-REPORT.md`](docs/REVIEW-REPORT.md) (scientific review) · [`docs/CHANGELOG-REVISION.md`](docs/CHANGELOG-REVISION.md) (audit trail) · pre-revision PDF `docs/main_pre-revision-20261017.pdf` (tag `pre-revision-20261017`) |
37 −| Code & data | reproducible from `scripts/` on the committed processed panel (`data/processed/`, 20 MB); raw filing text and embeddings come from the EDGAR Research Database (not redistributed) |
38 −
39 −## 🎯 What this paper does
40 −
41 −Did the *language* of AI in firms' SEC filings carry information about their subsequent stock
42 −returns, when did that information begin to be priced, and through which channel? Three
43 −ingredients are combined: a machine-readable filing corpus (EDGAR Research Database), a semantic
44 −measurement technology (sentence embeddings scored against AI anchor concepts, section-weighted
45 −within filings and averaged across the filings of a calendar year), and a look-ahead-free mapping
46 −from the filing year to tradeable forward returns (July Y+1 → June Y+2, at least six months after the
47 −last filing).
48 −
49 −```
50 −r_{i,Y+1} = α + β · AI_{i,Y} + γ' X_{i,Y} + δ · (AI_{i,Y} × Post-ChatGPT) + ε_{i,Y+1}
51 −```
52 −
53 −Signals: `AI_ADOPT` (internal AI use), `AI_INFRA` (compute / capex exposure), `AI_RISK`
54 −(product-disruption risk), `AI_COMPOS` = ½[z(ADOPT) + z(INFRA)]. Methods: within-year z-scoring,
55 −quintile portfolio sorts (EW/VW, raw and market-adjusted) with Newey–West t-statistics, FF5 +
56 −momentum (+ IT-sector factor) alphas, two-way fixed-effects panels with double-clustered errors,
57 −Fama–MacBeth (with and without sector dummies), post-ChatGPT (30 November 2022) interaction, DiD and
58 −event-time profile on pre-determined exposure, market-model event study with dependence-robust
59 −portfolio tests, placebo on five non-AI semantic factors, 8-K-only signal, seasoned-firm universe.
14 +| Version | **2.0 (October 28, 2026)** — Russell 3000 extension; à valider par les co-auteurs |
15 +| Funding | FIRC (UQO) |
16 +| Previous versions | v1.1 (S&P 500, 2026-10-17 review) and v1.0 (June 2026): `docs/paper_v1.1_sp500_20261026/`, `docs/REVIEW-REPORT.md`, `docs/CHANGELOG-REVISION.md` |
17 +| Code & data | fully reproducible from `scripts/r3k_*.py` on `data/raw_russell3000/` (factor panel, filing-level scores, SPID counts, universe) and `data/processed_r3k/` (prices, SEC share counts, factors); raw filing text and embeddings come from the EDGAR Research Database (UQO WP 0001), not redistributed |
60 18
61 −## 🏆 Headline results
19 +## 🏆 Headline results (Russell 3000 unless stated)
62 20
63 21 | Object | Estimate |
64 22 |---|---|
65 −| Panel | 7,869 firm-years, 500 firms, filing years 2010–2026 (estimation sample 6,852 firm-years, 497 firms, formation years 2010–2024) |
66 −| Scored corpus | 103,926 Form 8-K (all 500 firms) + 3,710 Form 10-Q (84 firms); 0 Form 10-K; median 28 chunks per firm-year |
67 −| Full-sample Q5–Q1 spread (composite, EW) | +4.9 pp/yr (t = 1.88); FF6 alpha +1.9 %/yr (t = 1.24); with IT-sector factor +0.4 %/yr |
68 −| Q5–Q1 spread, pre- / post-ChatGPT | **+2.5 pp/yr → +14.6 pp/yr**; post cohorts 2022 / 2023 / 2024 = **+9.3 / −4.0 / +38.4**; post mean without 2024 = +2.6 |
69 −| Pooled post-period Q5 vs Q1 (firm-clustered) | +14.7 pp (t = 2.18) raw; **+7.2 pp (t = 2.21) winsorized**; 2022–2023 only +2.6 pp (t = 0.46) |
70 −| Monthly long–short, pre / post (36 months) | mean 1.1 → 7.5 %/yr; Sharpe **0.22 → 1.32**; FF6 alpha post +5.2 %/yr (t = 1.72); **FF6 + TECH alpha +1.9 %/yr (t = 0.86), β_TECH = 0.47** |
71 −| Event study: ChatGPT launch, High–Low 1-year CAR | **+10.1 pp**; Welch t = 1.97 (independence); portfolio t = 2.28 (estimation-window SD) / 2.60 (event-window SD); 60 days: +2.9 pp, t = 1.35 |
72 −| Placebo (5 non-AI semantic factors) | ESG −8.8, climate −5.0, macro −0.5 pp; but cybersecurity +13.5 and automation/labour +7.2 pp |
73 −| Event-time premium on 2019–21 exposure (firm-clustered SE) | 2015 +3.9 (t 2.8), 2016 +3.0 (2.3), 2022 +3.4 (2.2), 2023 +0.5 (0.3), 2024 +6.8 (3.0) — pre-trend not flat |
74 −| Sector controls | FM post slope +5.5 (t 2.0) → +2.9 (t 1.4) with sector dummies; sector×year FE +1.3 (t 1.2); sector-neutral sort +3.8 (t 0.8) |
75 −| 8-K-only signal / seasoned firms / Mag-7 excluded | +1.6 → +16.3 pp; +1.0 → +17.3 pp; +1.7 → +14.1 pp (break unchanged) |
23 +| Panel | 27,468 firm-years, 2,379 firms, formation years 2010–2024 (6,793 firm-years of S&P 500 members) |
24 +| Full-sample Q5–Q1 spread (composite) | EW +5.5 pp/yr (t 2.41); VW +13.9 (t 2.59); S&P 500 members EW +7.0 (t 2.56) |
25 +| Fama–MacBeth slope (composite) | +3.07 pp/SD (t 2.80); with sector dummies +1.46 (t 4.38); S&P 500 +2.07 (t 2.17) |
26 +| Pre / post ChatGPT, Russell 3000 | +4.9 → +7.9 pp (post t 1.34); cohorts 2022/23/24 = −8.4 / +11.8 / +20.4; pooled post +8.2 (t 1.80), winsorized +3.0; post LS Sharpe 0.23 (pre 0.50) |
27 +| Pre / post, S&P 500 members | +5.0 → +15.0 (t 2.21); cohorts +13.5 / −6.9 / +38.4; pooled +15.1 (t 2.22); post Sharpe 0.66 |
28 +| Size | largest 1,000: +2.9 → +12.8 (t 2.09); remaining firms: +9.6 → +8.4; size-neutral full-sample +7.2 (t 2.73) |
29 +| Placebo break dates | 2022 ranks 8th of 10 candidate first-post-cohorts (best: 2016) |
30 +| Event study (IWV market, N = 2,226) | 1-year H−L +14.0 pp (Welch 3.94, CDA 2.21); 60 days +0.9; largest 1,000 +19.5 (5.09) vs rest +9.9 (1.55) |
31 +| Sector | post-launch sector×year FE slope +0.10 (t 0.11); full-sample +0.88 (t 2.48); β_TECH 0.38 |
32 +| Form-specific signals | 10-K only: pre +6.9 / post +16.7 (t 2.61), FM +4.66; 8-K only: +1.1 / +6.6; horse race 10-K +6.06 (1.71), 8-K +0.90 |
33 +| SPID AI projects | tech-project quintile spread −1.9 (pre) / −1.3 (post); FM −0.22 (t −0.24) |
34 +| Placebo factors (post) | ESG −15.9, climate −8.6, macro −16.3; cybersecurity +13.5, automation +7.8, AI disruption risk +22.8 |
76 35
77 36 ## 🗂 Repository layout
78 37
79 38 ```
80 39 ai-disclosure-stock-returns/
81 −├── data/processed/ # analysis_panel, merged_panel, filing_type_scores, prices, market caps, FF5, event CARs (parquet, 20 MB, committed)
82 −├── scripts/ # the pipeline, in order
83 −│ ├── 01_download_prices.py # monthly prices (500 tickers), SPY, market caps [needs network; outputs committed]
84 −│ ├── 02_build_panel.py # look-ahead-free forward returns (July formation) + controls [needs the EDGAR DB panel]
85 −│ ├── 03_analysis.py # sorts, FF6 alphas, two-way FE, Fama–MacBeth, post-ChatGPT break
86 −│ ├── 04_figures.py # figures 1–5
87 −│ ├── 05_make_tables.py # LaTeX tables (core) — the committed fragments were hand-edited afterwards (see CHANGELOG)
88 −│ ├── 06_extended.py # placebo, sector-neutral / double sorts, Mag-7 exclusion, PCA, DiD, event-time (firm-clustered SE since v1.1)
89 −│ ├── 07_event_study.py # ChatGPT market-model event study [daily prices committed]
90 −│ ├── 08_figures_ext.py # figures 6–13
91 −│ ├── 09_make_tables_ext.py # LaTeX tables (extended)
92 −│ ├── 10_filing_type_scores.py # v1.1: 8-K vs 10-Q filing-level scores from the EDGAR DB → data/processed/filing_type_scores.parquet
93 −│ ├── 11_review_reanalysis.py # v1.1: cohorts, pooled tests, monthly Sharpe/alphas + TECH factor, 8-K-only, seasoned firms, sector FE, event-study CDA
94 −│ └── 12_make_tables_review.py # v1.1: tab_corpus, tab_postinference, tab_review_robust, tab_event_cda
95 −├── results/tables/ # results.json, results2.json, results_event.json, results_review.json, corpus_composition.json, CSVs
96 −├── figures/ # 13 figures (PNG) — copied to paper/figures/
97 −├── paper/ # main.tex, sections/, appendix/, tables/, references.bib (57 entries, DOIs), main.pdf
98 −└── docs/ # REVIEW-REPORT.md, CHANGELOG-REVISION.md, README-analysis.md (June 2026), original reports, pre-revision PDF
40 +├── data/raw_russell3000/ # factor panels, filing_scores, filings, companies, universe.json, spid_firm_year_signals.csv (from the EDGAR DB)
41 +├── data/processed_r3k/ # prices (Yahoo), SEC share history, FF factors, merged/analysis panels, LS series, event CARs
42 +├── scripts/r3k_00..07_*.py # the v2.0 pipeline, in order (01: Yahoo prices · 01c: SEC share counts · 02: panel · 03: core · 04: extended · 05: event · 06: figures · 07: tables + macros)
43 +├── results/r3k/ # results.json, results2.json, results_event.json, CSVs
44 +├── figures/r3k/ # 14 figures (pdf + png)
45 +├── paper/ # main.tex, sections/, appendix/, tables/ (generated, incl. macros.tex), figures_r3k/ → main.pdf
46 +├── code/tex2docx/ # LaTeX → Word conversion (prep_tex.py, post_docx.py)
47 +└── docs/ # v1.1 archive, review report, changelog
99 48 ```
100 49
101 50 ## 🚀 Quick start
102 51
103 52 ```bash
104 −pip install -r requirements.txt
105 −# Re-analyses (no network, < 1 min each) from the committed parquet files:
106 −python scripts/03_analysis.py && python scripts/06_extended.py && python scripts/07_event_study.py
107 −python scripts/11_review_reanalysis.py && python scripts/12_make_tables_review.py
108 −python scripts/04_figures.py && python scripts/08_figures_ext.py && cp figures/*.png paper/figures/
109 −cd paper && make # latexmk → main.pdf
53 +python3 -m pip install -r requirements.txt
54 +python3 scripts/r3k_00_factors.py && python3 scripts/r3k_01_download_prices.py && python3 scripts/r3k_01c_sec_shares.py
55 +for s in 02_build_panel 03_analysis 04_extended 05_event_study 06_figures 07_tables; do python3 scripts/r3k_$s.py; done
56 +cd paper && latexmk # → main.pdf (every number in the text is a macro from tables/macros.tex)
110 57 ```
111 −
112 −`01_download_prices.py` and `02_build_panel.py` require, respectively, network access to Yahoo Finance and the
113 −EDGAR Research Database panel (`annual_final.parquet`); their outputs are committed. `10_filing_type_scores.py`
114 −also reads the EDGAR Research Database; its output is committed.
115 −
116 −## 🗃 Data
117 −
118 −- **Disclosure signals**: section-weighted cosine similarity of filing text chunks to AI anchor concepts, computed in the
119 − EDGAR Research Database (`~/Desktop/UQO/UQO_RESEARCH_DB`, `all-MiniLM-L6-v2` embeddings). **Scored corpus**: all Form 8-K
120 − filings of the 500 current constituents (103,926) and the Form 10-Q filings of 84 firms (3,710); no Form 10-K was parsed.
121 − The panel year is the **calendar year of the filing date**. Only firm-year scores (and, since v1.1, firm-year-by-form-type
122 − scores) enter this repository.
123 −- **Prices and factors**: monthly prices and market capitalisations for the 500 constituents and SPY (Yahoo Finance, downloaded
124 − 17 June 2026), Fama–French five factors and momentum (Kenneth French data library, through April 2026).
125 −- **Timing**: filings of calendar year Y → forward 12-month return July (Y+1) to June (Y+2); gap ≥ 6 months.
126 −- **Universe**: current (June 2026) S&P 500 membership — **not point-in-time**; survivorship and entrant bias are discussed in
127 − the paper and tested with listing-history restrictions (not eliminated).
128 −- Sample cascade: 7,869 firm-years → 6,852 firm-years (497 firms) with a forward return (Table 2 of the paper).
129 −
130 −## 🔬 Methodology
131 −
132 −1. **Signal construction** (`02_build_panel.py`): within-year z-scores of the three anchors and the composite; controls (momentum, volatility, log market cap).
133 −2. **Portfolio sorts** (`03_analysis.py`): quintiles, EW/VW, raw and market-adjusted, Newey–West inference; long-short spread regressed on FF5 + momentum.
134 −3. **Panel and Fama–MacBeth** (`03_analysis.py`): two-way fixed effects with double-clustered errors; year-by-year cross-sectional slopes.
135 −4. **Structural break** (`03_analysis.py`, `06_extended.py`): post-ChatGPT interaction, DiD and event-time profile, sector-neutral and double sorts, Magnificent-7 exclusion, alternative signals/horizons, PCA.
136 −5. **Placebo** (`06_extended.py`): five non-AI semantic factors through the same pipeline.
137 −6. **Event study** (`07_event_study.py`): market-model CARs around 30 November 2022, binscatter on pre-event AI intensity.
138 −7. **Review re-analyses** (`11_review_reanalysis.py`): cohort-by-cohort and leave-one-out spreads, pooled firm-clustered tests (raw and winsorized), monthly Sharpe ratios and sub-period alphas with an IT-sector factor, 8-K-only signal, seasoned-firm universe, alternative break dates, Fama–MacBeth with sector dummies and sector×year FE, event-study portfolio tests (Brown–Warner CDA, event-window SD, Kolari–Pynnönen ρ̄).
139 −
140 −## 🖼 Figures & tables inventory
141 −
142 −| Figure | Content |
143 −|---|---|
144 −| `fig1_quintile_returns` | forward returns by AI-language quintile (full / pre / post) |
145 −| `fig2_cumulative_ls` | cumulative long-short returns by signal |
146 −| `fig3_annual_spread` | Q5–Q1 spread by formation (filing) year |
147 −| `fig4_ai_language_trend` | AI-language intensity over time |
148 −| `fig5_binscatter` | forward return vs AI intensity, pre/post |
149 −| `fig6_fm_byyear` | Fama–MacBeth slopes by year |
150 −| `fig7_placebo` | AI vs five placebo semantic factors |
151 −| `fig8_event_car` | cumulative abnormal returns around ChatGPT |
152 −| `fig9_double_sort` | AI × size double sorts pre/post |
153 −| `fig10_mag7` | Magnificent-7 exclusion |
154 −| `fig11_eventtime` | event-time premium on pre-determined exposure (firm-clustered 95 % CI) |
155 −| `fig12_sector_box` | AI intensity by sector |
156 −| `fig13_event_binscatter` | 1-year CAR vs pre-event AI intensity |
157 −
158 −Tables (`paper/tables/*.tex`): corpus composition, summary statistics, correlations, top firms, portfolio sorts, alphas,
159 −panel, structural break, post-period inference, double sorts, placebo, event study, event-study inference (CDA), DiD,
160 −robustness, additional robustness (8-K-only, entrants, break definition, sector controls).
161 −
162 −## ⚠️ Limitations
163 −
164 −- The post-2022 period has three formation cohorts and the average spread is driven by the 2024 cohort (holding period ending
165 − with the partial June 2026 observation); a regime claim is a hypothesis for future cohorts, not an established fact.
166 −- The scored corpus is 8-K current reports plus a partial, alphabetically selected set of 10-Qs; no 10-K text. Results hold on
167 − 8-K filings alone, but the original question about annual-report narrative has not been tested.
168 −- Current S&P 500 constituents only (survivorship and entrant bias; return levels biased upward). Entrants are evenly spread
169 − across AI quintiles and results hold among seasoned firms, but the point-in-time index has not been reconstructed.
170 −- AI language is correlated with sector; every sector control halves the post-2022 effect and leaves it insignificant.
171 −- Anchor concepts embed the authors' definitional choices; no alternative anchor set has been tried.
172 −- Market-cap proxy with constant (June 2026) shares; Yahoo Finance adjusted closes rather than CRSP; French factors end April 2026.
173 −
174 −## 📝 Citation
175 −
176 −```bibtex
177 −@techreport{kammoun2026ailanguage,
178 − author = {Kammoun, Manel and Tandjambianda, Djerrycharli and Boucher, Simon-Pierre},
179 − title = {Does the Language of Artificial Intelligence Move Markets? A Semantic Analysis of S\&P~500 SEC Filings and the Cross-Section of Stock Returns, 2010--2026},
180 − institution = {Universit\'e du Qu\'ebec en Outaouais, D\'epartement des sciences administratives},
181 − type = {Working Paper},
182 − number = {17},
183 − year = {2026},
184 − month = {October},
185 − note = {Version 1.1}
186 −}
187 −```
188 −
189 −## 👤 Authors & contact
190 −
191 −**Manel Kammoun**, **Djerrycharli Tandjambianda**, **Simon-Pierre Boucher** — Département des sciences administratives,
192 −Université du Québec en Outaouais (UQO), Gatineau, QC.
193 −📧 [manel.kammoun@uqo.ca](mailto:manel.kammoun@uqo.ca) · [djerrycharli.tandjambianda@uqo.ca](mailto:djerrycharli.tandjambianda@uqo.ca) · [simon-pierre.boucher@uqo.ca](mailto:simon-pierre.boucher@uqo.ca)
194 −
195 −## 🗒 Notes
196 −
197 −- **Revision 2026-10-17 (v1.1, à valider par les co-auteurs)**: complete scientific revision by the editorial-board procedure
198 − (`Recherche-UQO/REVISION-MASTER-PROMPT.md`). The review found that the June 2026 draft misdescribed the corpus (10-K text
199 − was claimed; the scored filings are 8-K and partial 10-Q, by calendar year of filing), overstated the uniformity of the
200 − post-2022 cohorts, used degenerate year-clustered standard errors in the event-time DiD, used an independence-based test in
201 − the event study, and relied on Newey–West t-statistics computed on three annual observations. All numbers were re-derived
202 − from versioned scripts; see `docs/REVIEW-REPORT.md` and `docs/CHANGELOG-REVISION.md`. `docs/README-analysis.md` is the
203 − June 2026 analysis note and keeps the original (incorrect) corpus description for the record.
204 −- **Reconstruction (2026-10-17)**: the working-paper `main.tex` of the June 2026 draft (former folder `UQO_WP_SPB_MK_CT`) had been lost; it was rebuilt from `Recherche-UQO/_modele-working-paper/main.tex` with title, abstract, keywords and JEL taken from the original report (`docs/original_report_full_2026-06.tex`). The 13 figures were regenerated from the committed panel with `scripts/04_figures.py` and `08_figures_ext.py`.
205 −- Deviations from the standard: three author e-mails on the cover (`\WPemail` holds three `\href`s, the title page prints `\WPemail` directly); the long abstract is single-spaced so that Keywords and JEL stay on page 2; `data/processed/*.parquet` (20 MB) is committed because it is the only reproducible input outside the EDGAR Research Database.
206 −- Macros `\tstat` and `\tablelegend` (used by sections and tables) are defined in `main.tex`.
added code/tex2docx/post_docx.py +94 −0
@@ -0,0 +1,94 @@
1 +import sys, re, datetime
2 +from docx import Document
3 +from docx.shared import Pt
4 +from docx.enum.text import WD_ALIGN_PARAGRAPH
5 +path = sys.argv[1]; doc = Document(path)
6 +paras = doc.paragraphs
7 +norm = lambda p: re.sub(r"\s+", " ", p.text.replace(" ", " ")).strip()
8 +title = "Does the Language of Artificial Intelligence Move Markets?"
9 +# --- title page: everything before the Abstract block is centred
10 +i_abs = next(i for i, p in enumerate(paras) if norm(p) == "Abstract")
11 +for p in paras[:i_abs - 1]:
12 + p.alignment = WD_ALIGN_PARAGRAPH.CENTER
13 + if norm(p) == title:
14 + for r in p.runs: r.font.size = Pt(18); r.font.bold = True
15 + elif norm(p).startswith("A Semantic Analysis of Russell"):
16 + for r in p.runs: r.font.size = Pt(13)
17 +# --- move References (pandoc appends it at the end) before the appendix, as in the PDF
18 +h1 = [p for p in paras if p.style.name == "Heading 1"]
19 +refs = next(p for p in h1 if norm(p) == "References")
20 +appA = next(p for p in h1 if re.match(r"^A [A-Z]", norm(p)))
21 +body = doc.element.body; kids = list(body)
22 +i_refs, i_app = kids.index(refs._p), kids.index(appA._p)
23 +moved = [k for k in kids[i_refs:] if k.tag.endswith("}sectPr") is False]
24 +for k in moved: appA._p.addprevious(k)
25 +# --- page breaks where the LaTeX source has \newpage
26 +breaks = 0
27 +paras[i_abs - 1].paragraph_format.page_break_before = True; breaks += 1
28 +for p in h1:
29 + t = norm(p)
30 + if t.startswith("1 Introduction") or t == "References" or p is appA:
31 + p.paragraph_format.page_break_before = True; breaks += 1
32 +# --- document properties
33 +cp = doc.core_properties
34 +cp.title = title
35 +cp.subject = "A Semantic Analysis of Russell 3000 SEC Filings and the Cross-Section of Stock Returns, 2010–2026"
36 +cp.author = "Manel Kammoun; Djerrycharli Tandjambianda; Simon-Pierre Boucher"
37 +cp.keywords = "textual analysis, sentence embeddings, artificial intelligence, disclosure, cross-section of returns, Russell 3000, firm size, event study, generative AI"
38 +cp.comments = "UQO Working Paper No. 0002 (FIRC-funded), Version 2.0 (Russell 3000 extension), October 28, 2026"
39 +cp.created = cp.modified = datetime.datetime(2026, 10, 28, 12, 0)
40 +# --- base fonts closer to the PDF (Times)
41 +from docx.oxml.ns import qn
42 +for st in doc.styles:
43 + try:
44 + if st.type == 1 and st.font is not None and (st.name == "Normal" or st.name.startswith("Heading") or st.name in ("Title", "Body Text", "First Paragraph", "Compact", "Caption", "Bibliography", "Block Text")):
45 + st.font.name = "Times New Roman"
46 + rpr = st.element.get_or_add_rPr(); rf = rpr.find(qn("w:rFonts"))
47 + if rf is None:
48 + rf = rpr.makeelement(qn("w:rFonts"), {}); rpr.append(rf)
49 + for a in ("w:ascii", "w:hAnsi", "w:eastAsia", "w:cs"): rf.set(qn(a), "Times New Roman")
50 + if st.name in ("Normal", "Body Text", "First Paragraph", "Compact"): st.font.size = Pt(12)
51 + except Exception:
52 + pass
53 +# --- wide tables: smaller font and tight cell padding so that 8-10 column tables fit the page width
54 +from docx.shared import Inches
55 +from docx.oxml import OxmlElement
56 +for sec in doc.sections:
57 + sec.left_margin = sec.right_margin = Inches(0.85)
58 +for tbl in doc.tables:
59 + ncol = max(len(r.cells) for r in tbl.rows)
60 + if ncol >= 7:
61 + size = Pt(7) if ncol >= 9 else Pt(8)
62 + tbl.autofit = True
63 + for row in tbl.rows:
64 + for cell in row.cells:
65 + for para in cell.paragraphs:
66 + para.paragraph_format.space_after = Pt(0); para.paragraph_format.space_before = Pt(0)
67 + for run in para.runs: run.font.size = size
68 + tcPr = cell._tc.get_or_add_tcPr(); mar = OxmlElement("w:tcMar")
69 + for side in ("left", "right"):
70 + el = OxmlElement(f"w:{side}"); el.set(qn("w:w"), "30"); el.set(qn("w:type"), "dxa"); mar.append(el)
71 + tcPr.append(mar)
72 +# --- proportional column widths for wide tables (pandoc emits equal 0.55-inch grid columns)
73 +TOTAL = 9792 # twips: 8.5in - 2 x 0.85in margins
74 +for tbl in doc.tables:
75 + ncol = max(len(r.cells) for r in tbl.rows)
76 + if ncol < 7: continue
77 + lens = [1] * ncol
78 + for row in tbl.rows:
79 + for j, cell in enumerate(row.cells[:ncol]):
80 + lens[j] = max(lens[j], min(28, max((len(w) for w in cell.text.split()), default=1)))
81 + weights = [max(l, 4) for l in lens]; tot = sum(weights); widths = [int(TOTAL * wgt / tot) for wgt in weights]
82 + grid = tbl._tbl.find(qn("w:tblGrid"))
83 + if grid is not None:
84 + for g, wd in zip(grid.findall(qn("w:gridCol")), widths): g.set(qn("w:w"), str(wd))
85 + tblPr = tbl._tbl.tblPr
86 + for e in tblPr.findall(qn("w:tblW")): e.set(qn("w:type"), "dxa"); e.set(qn("w:w"), str(TOTAL))
87 + for e in tblPr.findall(qn("w:tblLayout")): e.set(qn("w:type"), "fixed")
88 + for row in tbl.rows:
89 + for j, cell in enumerate(row.cells[:ncol]):
90 + tcPr = cell._tc.get_or_add_tcPr(); tcW = tcPr.find(qn("w:tcW"))
91 + if tcW is None: tcW = OxmlElement("w:tcW"); tcPr.insert(0, tcW)
92 + tcW.set(qn("w:type"), "dxa"); tcW.set(qn("w:w"), str(widths[j]))
93 +doc.save(path)
94 +print(f"post-processed: {i_abs-1} title-page paragraphs centred, {len(moved)} reference paragraphs moved before appendix, {breaks} page breaks, properties set")
added code/tex2docx/prep_tex.py +168 −0
@@ -0,0 +1,168 @@
1 +"""Flatten a LaTeX working paper and resolve numbering from main.aux so that
2 +pandoc produces a Word file with correct section/table/figure/equation numbers."""
3 +import re, sys, pathlib
4 +
5 +PAPER = pathlib.Path(sys.argv[1]).resolve()
6 +OUT = pathlib.Path(sys.argv[2])
7 +warn = lambda *a: print("WARN:", *a, file=sys.stderr)
8 +
9 +def strip_comments(txt):
10 + out = []
11 + for line in txt.splitlines():
12 + if re.match(r'^\s*%', line):
13 + continue
14 + # drop trailing unescaped % comments
15 + line = re.sub(r'(?<!\\)%.*$', '', line)
16 + out.append(line)
17 + return "\n".join(out) + "\n"
18 +
19 +def inline(path):
20 + txt = strip_comments(path.read_text())
21 + def rep(m):
22 + name = m.group(1)
23 + p = PAPER / (name if name.endswith('.tex') else name + '.tex')
24 + if not p.exists():
25 + warn("missing input", p); return m.group(0)
26 + return inline(p)
27 + return re.sub(r'\\input\{([^}]+)\}', rep, txt)
28 +
29 +def brace_arg(s, i):
30 + """s[i] == '{' -> return (content, index after closing brace)"""
31 + assert s[i] == '{', s[i:i+20]
32 + depth = 0
33 + for j in range(i, len(s)):
34 + if s[j] == '{' and (j == 0 or s[j-1] != '\\'): depth += 1
35 + elif s[j] == '}' and s[j-1] != '\\':
36 + depth -= 1
37 + if depth == 0:
38 + return s[i+1:j], j+1
39 + raise ValueError("unbalanced braces")
40 +
41 +# ---------------------------------------------------------------- aux
42 +aux = (PAPER / 'main.aux').read_text()
43 +labels = {}
44 +for m in re.finditer(r'\\newlabel\{([^}]+)\}\{', aux):
45 + outer, _ = brace_arg(aux, m.end()-1)
46 + num, _ = brace_arg(outer, 0)
47 + labels[m.group(1)] = num.strip()
48 +toc = []
49 +for m in re.finditer(r'\\contentsline \{(section|subsection|subsubsection)\}\{\\numberline \{([^}]*)\}(.*?)\}\{\d+\}\{', aux):
50 + toc.append((m.group(1), m.group(2), m.group(3)))
51 +print(f"aux: {len(labels)} labels, {len(toc)} numbered headings")
52 +
53 +# ---------------------------------------------------------------- flatten
54 +tex = inline(PAPER / 'main.tex')
55 +
56 +# ---- WP 28 shims ----------------------------------------------------------
57 +# TikZ pipeline figure -> pre-rendered PNG
58 +tex = re.sub(r'\\begin\{tikzpicture\}.*?\\end\{tikzpicture\}', r'\\includegraphics[width=\\textwidth]{figures/fig_pipeline.png}', tex, flags=re.S)
59 +# PDF figures -> PNG (pandoc/docx cannot embed PDF)
60 +tex = re.sub(r'\\includegraphics(\[[^\]]*\])?\{([^}]+)\.pdf\}', r'\\includegraphics\1{figures_r3k/\2.png}', tex)
61 +# \adjustbox{max width=...}{...} wrapper -> content only
62 +def strip_adjustbox(t):
63 + out = []; pos = 0
64 + while True:
65 + m = re.search(r'\\adjustbox\{[^{}]*\}\{', t[pos:])
66 + if not m: break
67 + start = pos + m.start(); open_ = pos + m.end() - 1
68 + content, end = brace_arg(t, open_)
69 + out.append(t[pos:start]); out.append(content); pos = end
70 + out.append(t[pos:]); return "".join(out)
71 +tex = strip_adjustbox(tex)
72 +# threeparttable / tablenotes -> plain notes paragraph
73 +tex = re.sub(r'\\(begin|end)\{threeparttable\}', '', tex)
74 +tex = re.sub(r'\\begin\{tablenotes\}\\footnotesize\s*\\item\s*(.*?)\\end\{tablenotes\}', lambda m: '\\par\\textit{Notes:} ' + m.group(1).strip(), tex, flags=re.S)
75 +# prompt block (quote + obeylines) -> verbatim
76 +tex = re.sub(r'\\begin\{quote\}\\footnotesize\\ttfamily.*?\\end\{quote\}', lambda m: (PAPER / 'tables' / 'prompt_verbatim.tex').read_text(), tex, flags=re.S)
77 +tex = re.sub(r'\\input\{tables/prompt\}', lambda m: (PAPER / 'tables' / 'prompt_verbatim.tex').read_text(), tex)
78 +# tabularx -> tabular (pandoc handles tabular more reliably); X -> l
79 +def fix_tabularx(m):
80 + spec = m.group(1).replace('X', 'l')
81 + return '\\begin{tabular}{' + spec + '}'
82 +tex = re.sub(r'\\begin\{tabularx\}\{\\textwidth\}\{([^}]*)\}', fix_tabularx, tex)
83 +tex = tex.replace('\\end{tabularx}', '\\end{tabular}')
84 +tex = tex.replace('\\allowbreak', '')
85 +tex = re.sub(r'\\setlength\{\\emergencystretch\}\{[^}]*\}', '', tex)
86 +tex = re.sub(r'\\begin\{tabular\}\{lr@\{\\hspace\{2em\}\}lr@\{\\hspace\{2em\}\}lr\}', r'\\begin{tabular}{lrlrlr}', tex)
87 +tex = re.sub(r'\\begin\{tabular\}\{llllp\{[^}]*\}\}', r'\\begin{tabular}{lllll}', tex)
88 +
89 +# titlepage: pandoc cannot evaluate \ifx -> keep the \else branch only
90 +tex = re.sub(r'\\ifx\\WPsubtitle\\empty\\else(.*?)\\fi', r'\1', tex, flags=re.S)
91 +
92 +# appendix environment / counters that pandoc does not understand
93 +tex = re.sub(r'\\(begin|end)\{appendices\}', '', tex)
94 +tex = re.sub(r'\\renewcommand\{\\the(table|figure)\}\{[^{}]*(\{[^{}]*\})*[^{}]*\}', '', tex)
95 +tex = re.sub(r'\\setcounter\{(table|figure)\}\{0\}', '', tex)
96 +
97 +# ---------------------------------------------------------------- headings
98 +pos = 0; out = []; ti = 0
99 +pat = re.compile(r'\\(section|subsection|subsubsection)\{')
100 +while True:
101 + m = pat.search(tex, pos)
102 + if not m: break
103 + title, end = brace_arg(tex, m.end()-1)
104 + out.append(tex[pos:m.start()])
105 + lvl = m.group(1)
106 + if ti < len(toc) and toc[ti][0] == lvl:
107 + num = toc[ti][1]; ti += 1
108 + if re.sub(r'\W', '', toc[ti-1][2])[:12] != re.sub(r'\W', '', title)[:12]:
109 + warn("heading mismatch:", repr(toc[ti-1][2][:40]), "vs", repr(title[:40]))
110 + out.append(f"\\{lvl}{{{num}~{title}}}")
111 + else:
112 + warn("unnumbered heading kept:", lvl, title[:40])
113 + out.append(tex[m.start():end])
114 + pos = end
115 +out.append(tex[pos:]); tex = "".join(out)
116 +if ti != len(toc): warn(f"only {ti}/{len(toc)} toc entries consumed")
117 +
118 +# ---------------------------------------------------------------- floats: caption prefix
119 +def fix_float(m):
120 + kind, body = m.group(1), m.group(2)
121 + labs = [l for l in re.findall(r'\\label\{([^}]+)\}', body) if l in labels]
122 + if not labs:
123 + warn("float without known label", body[:60]); return m.group(0)
124 + num = labels[labs[0]]
125 + word = 'Table' if kind == 'table' else 'Figure'
126 + cm = re.search(r'\\caption(\[[^\]]*\])?\{', body)
127 + if not cm: return m.group(0)
128 + cap, end = brace_arg(body, cm.end()-1)
129 + body = body[:cm.start()] + f"\\caption{{{word} {num}: {cap}}}" + body[end:]
130 + return f"\\begin{{{kind}}}{body}\\end{{{kind}}}"
131 +tex = re.sub(r'\\begin\{(table|figure)\}(.*?)\\end\{\1\}', fix_float, tex, flags=re.S)
132 +
133 +# ---------------------------------------------------------------- equations: number tag
134 +def fix_eq(m):
135 + body = m.group(1)
136 + lm = re.search(r'\\label\{([^}]+)\}', body)
137 + if lm and lm.group(1) in labels:
138 + body = body.replace(lm.group(0), '')
139 + body = body.rstrip() + f"\\qquad\\text{{({labels[lm.group(1)]})}}\n"
140 + return "\\begin{equation}" + body + "\\end{equation}"
141 +tex = re.sub(r'\\begin\{equation\}(.*?)\\end\{equation\}', fix_eq, tex, flags=re.S)
142 +
143 +# ---------------------------------------------------------------- cross-references
144 +missing = set()
145 +def ref(m):
146 + k = m.group(2)
147 + if k not in labels:
148 + missing.add(k); return '??'
149 + return f"({labels[k]})" if m.group(1) == 'eqref' else labels[k]
150 +tex = re.sub(r'\\(eqref|ref)\{([^}]+)\}', ref, tex)
151 +if missing: warn("unresolved labels:", sorted(missing))
152 +tex = re.sub(r'\\label\{[^}]+\}', '', tex)
153 +
154 +# booktabs partial rules have no Word equivalent and leak as text
155 +tex = re.sub(r'\\cmidrule(\([^)]*\))?\{[^}]*\}', '', tex)
156 +
157 +# ---------------------------------------------------------------- pandoc shims
158 +shims = r"""
159 +\newcommand{\num}[1]{#1}
160 +\renewcommand{\tablelegend}[1]{\par\textit{Notes:} #1}
161 +\renewcommand{\tstat}[1]{(\textit{t}=#1)}
162 +\newcommand{\textperiodcentered}{·}
163 +"""
164 +tex = tex.replace("\\begin{document}", shims + "\\begin{document}", 1)
165 +tex = re.sub(r'(\\newcommand\{\\WPauthor\}\{[^}]*)\}', lambda m: m.group(1).replace('\\quad', '~~~~') + '}', tex)
166 +tex = tex.replace("\\newcommand{\\textperiodcentered}{·}", "\\providecommand{\\textperiodcentered}{·}")
167 +OUT.write_text(tex)
168 +print("wrote", OUT, len(tex), "chars")
added data/raw_russell3000/spid_firm_year_signals.csv +19494 −0
Large diff (19,494 lines) is not loaded by default to keep the page fast.Load diff

Diff truncated — file too large.

Menu

Theme