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>
94 lines
3.1 KiB
Python
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()
|