"""Extract the SUBJECT company from SC 13D filing headers. The EDGAR full index lists the FILER (the activist) in the CIK/company columns; the target company appears only inside the document. For Schedule 13D filings the SGML header's first COMPANY block is the subject company (verified on samples: FTE Networks, 30DC, Helius Medical, Genco). This fetcher reads data/research/edgar_13d.csv, keeps SC 13D originals, and for each one fetches the .txt header, extracting the first company name + CIK. Writes data/research/edgar_13d_targets.csv (resume-safe: paths already extracted are skipped). PYTHONPATH=. uv run python -m research.lab.fetch_13d_targets [--limit N] """ from __future__ import annotations import os import re import sys import time from concurrent.futures import ThreadPoolExecutor, as_completed import pandas as pd import requests from research.lab.fetch_us import _proxy_env IN = "data/research/edgar_13d.csv" OUT = "data/research/edgar_13d_targets.csv" UA = {"User-Agent": "quant-research/0.1 gahow@example.com", "Accept-Encoding": "gzip"} NAME_RE = re.compile(r"COMPANY CONFORMED NAME:\s*(.+)") CIK_RE = re.compile(r"CENTRAL INDEX KEY:\s*(\d+)") WORKERS = 5 def fetch_target(path: str) -> tuple[str, str, int] | None: url = "https://www.sec.gov/Archives/" + path for attempt in range(3): try: r = requests.get(url, headers=UA, timeout=60) if r.status_code == 404: return path, "", 0 r.raise_for_status() head = r.text[:20000] name = NAME_RE.search(head) cik = CIK_RE.search(head) return path, (name.group(1).strip() if name else ""), int(cik.group(1)) if cik else 0 except Exception: # noqa: BLE001 time.sleep(1.0 * (attempt + 1)) print(f" FAILED {path}") return None def main() -> None: _proxy_env() limit = None if "--limit" in sys.argv: limit = int(sys.argv[sys.argv.index("--limit") + 1]) ev = pd.read_csv(IN) ev = ev[ev["form"] == "SC 13D"].copy() done: set[str] = set() frames = [] if os.path.exists(OUT): prev = pd.read_csv(OUT) frames.append(prev) done = set(prev["path"]) todo = ev[~ev["path"].isin(done)] if limit: todo = todo.head(limit) print(f"13D originals: {len(ev)}, done: {len(done)}, fetching: {len(todo)}") rows = [] with ThreadPoolExecutor(max_workers=WORKERS) as ex: futs = {ex.submit(fetch_target, p): p for p in todo["path"]} n = 0 for fut in as_completed(futs): res = fut.result() n += 1 if res is not None: path, name, cik = res rows.append({"path": path, "target_name": name, "target_cik": cik}) if n % 500 == 0: print(f" {n}/{len(todo)}") df = pd.DataFrame(rows) if len(df): meta = todo.set_index("path")[["filed", "cik", "company"]] df = df.join(meta, on="path") allrows = pd.concat(frames + [df]) if frames or len(df) else pd.DataFrame() allrows.to_csv(OUT, index=False) print(f"stored {len(allrows)} rows -> {OUT}") if __name__ == "__main__": main()