Files
quant-trading/research/lab/fetch_13d_targets.py
Gahow Wang 04e2c5c3be research: commit the 2026-10-07/08 studies left in the working tree, overnight mid/small (STOP)
Studies run by earlier sessions and documented in CLAUDE.md, but never
committed: FINRA short volume (SVR) and the research-only svr_momentum veto,
SEC fails-to-deliver, 13D activists and the 13D event sleeve, 13F
institutional flow, GDELT news volume, S&P 400/600 Form 4 insiders,
union_shield, shield_lab; plus the mid/small insider and Tiingo fetcher
changes and midsmall_rebuild.

overnight_midsmall: the overnight-only momentum book on S&P 400/600 earns
0.93 net Sharpe at 3 bp a side (2019-26) with corr 0.83 to the S&P 500 book
(STOP).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 22:20:43 -07:00

94 lines
3.1 KiB
Python

"""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()