research: S&P 600 replication of mid-cap quality sleeves (all REJECT)
Preregistered replication vs IJR, 2017-01..2026-09, 20bp decides: quality composite +2.3%/yr (t 0.78; halves +8.0/-3.5), quality-filtered momentum +2.2% (t 0.73); SUE and lottery-filtered momentum also REJECT, all weaker at 40bp. Equal-weight members beat IJR (t 3.2) and every sleeve. Decision: drop quality stock selection in mid/small caps. Adds --universe small to fetch_us_fundamentals (IJR, name-checked CIKs), a `which` argument to fundamentals.load_mid, and --universe small to midcap_sleeve. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,8 +5,9 @@ history (needed to turn split-adjusted prices back into market caps).
|
||||
|
||||
uv run python -m research.lab.fetch_us_fundamentals
|
||||
uv run python -m research.lab.fetch_us_fundamentals --universe mid # S&P 400 (IJH) ever-members
|
||||
uv run python -m research.lab.fetch_us_fundamentals --universe small # S&P 600 (IJR) ever-members
|
||||
|
||||
Writes data/research/us_fund_raw.pkl (us_mid_fund_raw.pkl for --universe mid):
|
||||
Writes data/research/us_fund_raw.pkl (us_{mid,small}_fund_raw.pkl for --universe):
|
||||
{"facts": DataFrame[ticker, tag, start, end, val, filed, form],
|
||||
"splits": DataFrame (dates x tickers, split ratio on split days)}
|
||||
|
||||
@@ -27,7 +28,7 @@ import yfinance as yf
|
||||
from research.lab.fetch_us import _proxy_env
|
||||
|
||||
OUT = os.path.join("data", "research", "us_fund_raw.pkl")
|
||||
MID_OUT = os.path.join("data", "research", "us_mid_fund_raw.pkl")
|
||||
UNIVERSE_FUND = {"mid": "IJH", "small": "IJR"} # --universe -> iShares fund
|
||||
UA = {"User-Agent": "quant-research/0.1 gahow@example.com", "Accept-Encoding": "gzip"}
|
||||
FORMS = {"10-K", "10-Q", "10-K/A", "10-Q/A", "10-KT", "20-F", "40-F"}
|
||||
|
||||
@@ -148,8 +149,8 @@ def _name_tokens(name: str) -> set[str]:
|
||||
return {w for w in words if w not in _SUFFIXES and len(w) > 1}
|
||||
|
||||
|
||||
def mid_ciks(tickers: list[str]) -> tuple[dict[str, int], pd.DataFrame]:
|
||||
"""CIKs for S&P 400 tickers. Candidates come from sec_sic.csv and
|
||||
def mid_ciks(tickers: list[str], fund: str = "IJH") -> tuple[dict[str, int], pd.DataFrame]:
|
||||
"""CIKs for S&P 400 (IJH) or S&P 600 (IJR) tickers. Candidates come from sec_sic.csv and
|
||||
mid_sec_sic.csv (earlier runs), then today's company_tickers.json; both
|
||||
map a ticker to whoever holds it NOW, so a candidate counts only if one of
|
||||
its SEC names (current or former) shares a word with an iShares holding
|
||||
@@ -177,7 +178,7 @@ def mid_ciks(tickers: list[str]) -> tuple[dict[str, int], pd.DataFrame]:
|
||||
js = r.json()
|
||||
names[str(c)] = [js.get("name", "")] + [x.get("name", "") for x in js.get("formerNames", [])]
|
||||
hold = load_holdings()
|
||||
hnames = hold[hold["fund"] == "IJH"].groupby("ticker")["name"].apply(lambda s: set(s.dropna()))
|
||||
hnames = hold[hold["fund"] == fund].groupby("ticker")["name"].apply(lambda s: set(s.dropna()))
|
||||
rows, ok = [], {}
|
||||
for t in tickers:
|
||||
c = cand.get(t, 0)
|
||||
@@ -201,13 +202,14 @@ def mid_ciks(tickers: list[str]) -> tuple[dict[str, int], pd.DataFrame]:
|
||||
def main() -> None:
|
||||
import sys
|
||||
global OUT
|
||||
if "--universe" in sys.argv and sys.argv[sys.argv.index("--universe") + 1] == "mid":
|
||||
uni = sys.argv[sys.argv.index("--universe") + 1] if "--universe" in sys.argv else None
|
||||
if uni in UNIVERSE_FUND:
|
||||
from research.lab.panel_mid import load_us_mid
|
||||
OUT = MID_OUT
|
||||
tickers = list(load_us_mid("mid").close.columns)
|
||||
ciks, audit = mid_ciks(tickers)
|
||||
audit.to_csv(os.path.join("data", "research", "us_mid_fund_cik_audit.csv"), index=False)
|
||||
print(f"{len(ciks)}/{len(tickers)} S&P 400 tickers have a name-checked CIK", flush=True)
|
||||
OUT = os.path.join("data", "research", f"us_{uni}_fund_raw.pkl")
|
||||
tickers = list(load_us_mid(uni).close.columns)
|
||||
ciks, audit = mid_ciks(tickers, UNIVERSE_FUND[uni])
|
||||
audit.to_csv(os.path.join("data", "research", f"us_{uni}_fund_cik_audit.csv"), index=False)
|
||||
print(f"{len(ciks)}/{len(tickers)} {UNIVERSE_FUND[uni]} tickers have a name-checked CIK", flush=True)
|
||||
else:
|
||||
with open(os.path.join("data", "research", "us_ohlcv.pkl"), "rb") as fh:
|
||||
tickers = [t for t in pickle.load(fh)["close"].columns]
|
||||
|
||||
Reference in New Issue
Block a user