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>
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
"""
|
||||
Fetch SEC "Insider Transactions Data Sets" (structured Form 3/4/5 data,
|
||||
quarterly zips from 2006Q1) and keep the open-market trades of officers and
|
||||
directors of S&P 500 ever-members in the research price panel.
|
||||
directors of every requested panel universe's ever-members. The zips are
|
||||
parsed once; the issuer->ticker mapping and price checks run per universe
|
||||
(us = S&P 500 research panel, mid = S&P 400, small = S&P 600).
|
||||
|
||||
uv run python -m research.lab.fetch_insider
|
||||
uv run python -m research.lab.fetch_insider # us (insider_trades.pkl)
|
||||
PYTHONPATH=. uv run python -m research.lab.fetch_insider --universe mid small
|
||||
|
||||
Source: https://www.sec.gov/data-research/sec-markets-data/insider-transactions-data-sets
|
||||
The zip links are read from that page (the 2026Q2 file moved to a new path).
|
||||
@@ -30,7 +33,7 @@ filing's own ISSUERTRADINGSYMBOL, limited to the dates that symbol was used
|
||||
and to dates before the ticker's current owner first filed, and kept only if
|
||||
trade prices match the ticker's unadjusted closes (median error < 25%).
|
||||
|
||||
Writes data/research/insider_trades.pkl:
|
||||
Writes data/research/insider_trades{,_{mid,small}}.pkl:
|
||||
{"trades": DataFrame, one row per kept transaction: ticker, cik,
|
||||
insider_cik, role, title, ten_pct, filed, trans_date, code,
|
||||
shares, price, value, price_ok, plan_10b51, plan_fn, ownership,
|
||||
@@ -52,6 +55,7 @@ Rule 10b5-1 (available in every year).
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import os
|
||||
import pickle
|
||||
@@ -219,13 +223,14 @@ def norm_symbols(sym) -> list[str]:
|
||||
return [re.sub(r"\s+", "", x).replace(".", "-") for x in parts if x.strip()]
|
||||
|
||||
|
||||
def raw_closes(tickers: list[str]) -> pd.DataFrame | None:
|
||||
def raw_closes(tickers: list[str], fund: str = FUND_RAW) -> pd.DataFrame | None:
|
||||
"""Unadjusted closes (split-adjusted close x later split ratios), from the
|
||||
fundamentals fetch; used to check that an issuer's trade prices belong to
|
||||
the ticker, and to value trades whose reported price is off."""
|
||||
if not os.path.exists(FUND_RAW):
|
||||
fundamentals fetch of that universe; used to check that an issuer's trade
|
||||
prices belong to the ticker, and to value trades whose reported price is
|
||||
off."""
|
||||
if not os.path.exists(fund):
|
||||
return None
|
||||
with open(FUND_RAW, "rb") as fh:
|
||||
with open(fund, "rb") as fh:
|
||||
raw = pickle.load(fh)
|
||||
close_sa = raw["close_sa"].reindex(columns=tickers)
|
||||
ratio = raw["splits"].reindex(index=close_sa.index, columns=tickers).fillna(0.0).replace(0.0, 1.0)
|
||||
@@ -317,29 +322,28 @@ def map_tickers(trades: pd.DataFrame, filings: pd.DataFrame, panel_tickers: list
|
||||
return trades, tm.reset_index(drop=True)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
for k in ("http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY", "all_proxy", "ALL_PROXY"):
|
||||
os.environ.pop(k, None) # direct connection for cik_map as well
|
||||
sess = _session()
|
||||
paths = download(sess)
|
||||
parts, fil, t0 = [], [], time.time()
|
||||
for path in paths:
|
||||
tr, f = parse_quarter(path)
|
||||
parts.append(tr)
|
||||
fil.append(f)
|
||||
print(f" {os.path.basename(path)}: {len(tr):,} P/S lines by officers/directors "
|
||||
f"[{time.time() - t0:.0f}s]", flush=True)
|
||||
trades = pd.concat(parts, ignore_index=True)
|
||||
filings = pd.concat(fil, ignore_index=True).drop_duplicates("ACCESSION_NUMBER")
|
||||
trades = trades.drop_duplicates(["accession", "trans_date", "code", "shares", "price", "ownership"])
|
||||
trades["cik"] = trades["cik"].astype("int64")
|
||||
filings["cik"] = filings["cik"].astype("int64")
|
||||
print(f"{len(trades):,} kept lines, {len(filings):,} Form 4 filings", flush=True)
|
||||
UNIVERSE_OUT = {"us": OUT,
|
||||
"mid": os.path.join("data", "research", "insider_trades_mid.pkl"),
|
||||
"small": os.path.join("data", "research", "insider_trades_small.pkl")}
|
||||
|
||||
with open(os.path.join("data", "research", "us_ohlcv.pkl"), "rb") as fh:
|
||||
close = pickle.load(fh)["close"]
|
||||
|
||||
def universe_inputs(u: str) -> tuple[pd.DataFrame, pd.DataFrame | None]:
|
||||
"""(panel closes, unadjusted closes) of one universe's research panel."""
|
||||
if u == "us":
|
||||
with open(os.path.join("data", "research", "us_ohlcv.pkl"), "rb") as fh:
|
||||
close = pickle.load(fh)["close"]
|
||||
fund = FUND_RAW
|
||||
else:
|
||||
from research.lab.panel_mid import load_us_mid
|
||||
close = load_us_mid(u).close
|
||||
fund = os.path.join("data", "research", f"us_{u}_fund_raw.pkl")
|
||||
return close, raw_closes(list(close.columns), fund)
|
||||
|
||||
|
||||
def finish(trades: pd.DataFrame, filings: pd.DataFrame, close: pd.DataFrame,
|
||||
raw: pd.DataFrame | None, out: str, label: str) -> None:
|
||||
"""Map the parsed trades to one panel's tickers, value them, and save."""
|
||||
tickers = list(close.columns)
|
||||
raw = raw_closes(tickers)
|
||||
trades, tm = map_tickers(trades, filings, tickers, raw, close)
|
||||
|
||||
# value at the reported price unless it is > 2x off the issuer's usual
|
||||
@@ -359,13 +363,40 @@ def main() -> None:
|
||||
"shares", "price", "value", "price_ok", "plan_10b51", "plan_fn", "ownership", "accession"]
|
||||
trades = trades[cols].sort_values(["filed", "ticker"]).reset_index(drop=True)
|
||||
unmapped = sorted(set(tickers) - set(tm["ticker"]))
|
||||
print(f"{len(trades):,} trades in {trades['ticker'].nunique()} panel tickers "
|
||||
print(f"[{label}] {len(trades):,} trades in {trades['ticker'].nunique()} panel tickers "
|
||||
f"({(trades['code'] == 'P').sum():,} P / {(trades['code'] == 'S').sum():,} S); "
|
||||
f"map: {(tm['source'] == 'sec_map').sum()} by CIK, {(tm['source'] == 'symbol').sum()} by symbol; "
|
||||
f"unmapped {unmapped}", flush=True)
|
||||
with open(OUT, "wb") as fh:
|
||||
with open(out, "wb") as fh:
|
||||
pickle.dump({"trades": trades, "tickers": tm, "unmapped": unmapped}, fh, protocol=4)
|
||||
print(f"Saved {OUT}")
|
||||
print(f"Saved {out}", flush=True)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--universe", nargs="+", default=["us"], choices=["us", "mid", "small"])
|
||||
args = ap.parse_args()
|
||||
for k in ("http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY", "all_proxy", "ALL_PROXY"):
|
||||
os.environ.pop(k, None) # direct connection for cik_map as well
|
||||
sess = _session()
|
||||
paths = download(sess)
|
||||
parts, fil, t0 = [], [], time.time()
|
||||
for path in paths:
|
||||
tr, f = parse_quarter(path)
|
||||
parts.append(tr)
|
||||
fil.append(f)
|
||||
print(f" {os.path.basename(path)}: {len(tr):,} P/S lines by officers/directors "
|
||||
f"[{time.time() - t0:.0f}s]", flush=True)
|
||||
trades = pd.concat(parts, ignore_index=True)
|
||||
filings = pd.concat(fil, ignore_index=True).drop_duplicates("ACCESSION_NUMBER")
|
||||
trades = trades.drop_duplicates(["accession", "trans_date", "code", "shares", "price", "ownership"])
|
||||
trades["cik"] = trades["cik"].astype("int64")
|
||||
filings["cik"] = filings["cik"].astype("int64")
|
||||
print(f"{len(trades):,} kept lines, {len(filings):,} Form 4 filings", flush=True)
|
||||
|
||||
for u in args.universe:
|
||||
close, raw = universe_inputs(u)
|
||||
finish(trades.copy(), filings, close, raw, UNIVERSE_OUT[u], u)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user