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 # Claude Code local settings 40 # Claude Code local settings
41 .claude/ 41 .claude/
42 **/.claude/ 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 <!-- Authors: Manel Kammoun, Djerrycharli Tandjambianda, Simon-Pierre Boucher — simon-pierre.boucher@uqo.ca --> 1 <!-- Authors: Manel Kammoun, Djerrycharli Tandjambianda, Simon-Pierre Boucher — simon-pierre.boucher@uqo.ca -->
2 # 🧠 Does the Language of Artificial Intelligence Move Markets? 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) 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.
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.
26 7
27 --- 8 ---
28 9
@@ -30,177 +11,47 @@
30 11
31 | Item | Value | 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) | 14+| Version | **2.0 (October 28, 2026)** — Russell 3000 extension; à valider par les co-auteurs |
34−| SSRN | not yet submitted | 15+| Funding | FIRC (UQO) |
35−| Journal | — (draft) | 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` |
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`) | 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 |
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.
60 18
61−## 🏆 Headline results 19+## 🏆 Headline results (Russell 3000 unless stated)
62 20
63 | Object | Estimate | 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) | 23+| Panel | 27,468 firm-years, 2,379 firms, formation years 2010–2024 (6,793 firm-years of S&P 500 members) |
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 | 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) |
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 | 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) |
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 | 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) |
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) | 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 |
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** | 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) |
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 | 29+| Placebo break dates | 2022 ranks 8th of 10 candidate first-post-cohorts (best: 2016) |
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 | 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) |
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 | 31+| Sector | post-launch sector×year FE slope +0.10 (t 0.11); full-sample +0.88 (t 2.48); β_TECH 0.38 |
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) | 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 |
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) | 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 ## 🗂 Repository layout 36 ## 🗂 Repository layout
78 37
79 ``` 38 ```
80 ai-disclosure-stock-returns/ 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) 40+├── data/raw_russell3000/ # factor panels, filing_scores, filings, companies, universe.json, spid_firm_year_signals.csv (from the EDGAR DB)
82−├── scripts/ # the pipeline, in order 41+├── data/processed_r3k/ # prices (Yahoo), SEC share history, FF factors, merged/analysis panels, LS series, event CARs
83−│ ├── 01_download_prices.py # monthly prices (500 tickers), SPY, market caps [needs network; outputs committed] 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)
84−│ ├── 02_build_panel.py # look-ahead-free forward returns (July formation) + controls [needs the EDGAR DB panel] 43+├── results/r3k/ # results.json, results2.json, results_event.json, CSVs
85−│ ├── 03_analysis.py # sorts, FF6 alphas, two-way FE, Fama–MacBeth, post-ChatGPT break 44+├── figures/r3k/ # 14 figures (pdf + png)
86−│ ├── 04_figures.py # figures 1–5 45+├── paper/ # main.tex, sections/, appendix/, tables/ (generated, incl. macros.tex), figures_r3k/ → main.pdf
87−│ ├── 05_make_tables.py # LaTeX tables (core) — the committed fragments were hand-edited afterwards (see CHANGELOG) 46+├── code/tex2docx/ # LaTeX → Word conversion (prep_tex.py, post_docx.py)
88−│ ├── 06_extended.py # placebo, sector-neutral / double sorts, Mag-7 exclusion, PCA, DiD, event-time (firm-clustered SE since v1.1) 47+└── docs/ # v1.1 archive, review report, changelog
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
99 ``` 48 ```
100 49
101 ## 🚀 Quick start 50 ## 🚀 Quick start
102 51
103 ```bash 52 ```bash
104−pip install -r requirements.txt 53+python3 -m pip install -r requirements.txt
105−# Re-analyses (no network, < 1 min each) from the committed parquet files: 54+python3 scripts/r3k_00_factors.py && python3 scripts/r3k_01_download_prices.py && python3 scripts/r3k_01c_sec_shares.py
106−python scripts/03_analysis.py && python scripts/06_extended.py && python scripts/07_event_study.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
107−python scripts/11_review_reanalysis.py && python scripts/12_make_tables_review.py 56+cd paper && latexmk # → main.pdf (every number in the text is a macro from tables/macros.tex)
108−python scripts/04_figures.py && python scripts/08_figures_ext.py && cp figures/*.png paper/figures/
109−cd paper && make # latexmk → main.pdf
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