# Extension de l'univers : S&P 500 → Russell 3000 (2026-10-27) Base de données EDGAR UQO (Manel Kammoun, Charli Tandja Mbianda, Simon-Pierre Boucher). ## Univers | Source | Fichier | Contenu | |---|---|---| | S&P 500 (Wikipédia, cache du 2026-05-25) | `data/metadata/sp500_constituents.json` | 503 lignes, 500 CIK | | Russell 3000 = titres détenus par l'ETF **iShares IWV** (`latest-holdings.csv`, « Fund Holdings as of Oct 23, 2026 ») | `data/metadata/russell3000_constituents.json` | 2 587 lignes actions → 2 555 appariées à un CIK SEC (`company_tickers.json`) ; 21 non appariées (CVR, escrows, banques FDIC sans CIK…) | | **Univers fusionné** | `data/metadata/universe.json` | **2 564 sociétés** (491 dans les deux indices, 9 S&P seulement, 2 064 Russell seulement) | Reconstruire : `python scripts/build_universe.py [--refresh-sp500] [--shards node:cores,… --master M3U96b]`. Le Russell 3000 n'a pas de liste officielle gratuite ; l'ETF IWV (réplication quasi complète) est le proxy standard. Les ~400 titres manquants par rapport aux 3 000 nominaux sont les micro-caps non détenues par l'ETF, les classes d'actions doublonnées (même CIK) et les sociétés sans dépôt SEC (banques déposant à la FDIC). ## Pipeline (identique à l'original, rendu scalable) 1. `scripts/download_filings.py` — téléchargeur **concurrent** (`src/acquisition/bulk_downloader.py`) : seau à jetons global (`--rate` req/s), 6 fils, reprise, repli 65 s sur HTTP 403/429, en-tête de la société (`SIC`, État, bourse) sauvé dans `data/metadata/submissions/.json`. Formulaires 10-K, 10-Q, 8-K depuis 2010-01-01 (document principal). 2. `scripts/build_database.py` — sociétés (univers + en-têtes SEC, colonnes `in_sp500`, `in_russell3000`, `sic_description`, `state_of_incorporation`, `exchange`, `fiscal_year_end`, `russell_weight_pct`, `shard_node`) + dépôts depuis `download_log.json` et `download_log.*.jsonl` ; `local_path` recalculé depuis le nœud courant ; relançable (UPDATE + INSERT, compatible avec les clés étrangères DuckDB). 3. `scripts/parse_filings.py --max-workers N --batch-size 800 --no-json` — parsing par **lots** (mémoire bornée), insertion par DataFrame ; **les 10-K et 10-Q sont maintenant parsés** (l'ancien corpus n'avait que les 8-K + 3 710 10-Q). `src/parsing/chunker.py` (perdu) a été recréé : fenêtres de 512 mots, chevauchement 64, minimum 50. 4. `scripts/build_embeddings.py` — MiniLM-L6-v2 (384 d, MPS) en flux de 50 000 chunks, un fichier parquet float32 par lot. 5. `scripts/build_factors.py` — scores d'ancrage (8 thèmes) ; agrégation par dépôt réécrite en SQL, centroïdes firme-année vectorisés (fonctionne avec >20 M de chunks). ## Exécution distribuée (cluster MacLustr) `scripts/cluster/orchestrate.py` (sur la passerelle **M1M32**, copie du code dans `~/UQO_RESEARCH_DB-code`, journal `~/edgar-orchestrate/`) pilote 10 Macs + le serveur OVH BHS64 : | Étape | Détail | |---|---| | `sync` | code + métadonnées → `~/UQO_RESEARCH_DB` de chaque nœud (venv Python 3.12 `uv`, torch MPS) | | `downloads` | la SEC limite à 10 req/s **par IP** : les 10 Macs (même IP publique) se partagent 9 req/s (`0,9` chacun) ; BHS64 (2e IP) télécharge l'autre moitié de chaque shard à 9 req/s dans `~/edgar-dl//` | | `ship` | chaque Mac rapatrie sa moitié BHS64 (rsync via l'agent SSH de M1M32) | | `pipeline` | `scripts/cluster/run_shard.sh pipeline` : build_db → parse → embed → export (`data/export/*.parquet`) | | `pull` + `merge` | le maître **M3U96b** (qui détient le corpus S&P 500 d'origine, 88 Go, rafraîchi depuis mai 2026) rapatrie les shards, `scripts/merge_shards.py` fusionne dans `data/duckdb/edgar_research.duckdb` (sauvegarde `*.pre-merge-*.duckdb`), régénère les parquet mono-fichier, puis `build_factors.py` | Shards (`data/metadata/shards.json`, proportionnels aux cœurs) : M3U96a 370, M2U64 318, M5M36 238, M4M64a 212, M4M64c 212, M4BP48 212, M4BP36 185, m4mm 159, m4mn 158 nouvelles sociétés ; M3U96b = les 500 S&P. Les fichiers bruts restent sur le nœud qui les a traités (provenance) ; seuls les parquet/embeddings remontent au maître. Reprise : chaque étape est idempotente (`python3 scripts/cluster/orchestrate.py status | downloads | ship | pipeline | pull | merge`). ## SPID (project-db) - `v_sections` lit maintenant toutes les sections parsées (10-K incluses) ; un 10-K déjà ingéré par `spid ingest` n'est pas relu. - Périmètre par défaut `SPID_SECTION_SCOPE="10-K:business,risk_factors,properties,md_and_a,quantitative_disclosures"` (les 10-Q et 8-K passent entièrement, comme pour le S&P 500) ; ex. restreindre : `…;10-Q:md_and_a,risk_factors`. - Registre `extracted_sections` : `python -m spid backfill-extracted` (une fois, avant la fusion, pour inscrire le run S&P 500), `python -m spid pending` (sections restantes + coût estimé gpt-4o-mini), `run_full_extract.py --workers 24 [--max-sections N]`.