Register us_momentum_qqq_trend: half overnight_recovery_top10, half a QQQ trend sleeve (strategies/qqq_trend.py: 200-day switch times a 30% vol target, held as TQQQ + SHY). Mixed stock+ETF, US only (extra tickers QQQ, SHY, TQQQ). Research (research/lab/): alpha against QQQ with a 2006-16 design / 2017-26 holdout split (alpha.py, qqq_scan.py, beat_qqq.py); leverage and drawdown frontier (levered_book.py, us_target50.py); synthetic 3x ETFs from 1990 and TrendRider/QQQ-trend stress tests (fetch_long_history.py, letf_history.py, vol_trend.py); short-horizon sleeves, diversifiers, industry momentum, earnings-announcement returns; new point-in-time data sources: SEC Form 4 insider trades (fetch_insider.py, insider.py), 10-K/10-Q text similarity, 8-K earnings dates and SIC codes (fetch_lazy_prices.py, lazy_prices.py), and an S&P 400/600 universe from iShares holdings (fetch_us_mid.py, panel_mid.py). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
72 lines
2.7 KiB
Python
72 lines
2.7 KiB
Python
"""
|
|
Alpha against a benchmark (QQQ by default): the yardstick for "does this
|
|
strategy add anything a Nasdaq-100 index fund does not?"
|
|
|
|
from research.lab.alpha import bench_returns, alpha_stats
|
|
b = bench_returns(r.index) # QQQ total return, daily
|
|
s = alpha_stats(r, b) # alpha, beta, t, IR, ...
|
|
|
|
alpha/beta come from the daily regression of excess returns (over the
|
|
T-bill) on the benchmark's excess return; the alpha t-stat uses Newey-West
|
|
standard errors (10 lags). IR is the annualized mean over the standard
|
|
deviation of the active return r - b. QQQ comes from
|
|
data/research/long_history.pkl (research/lab/fetch_long_history.py).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import pickle
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
|
|
from research.lab.levered_book import tbill
|
|
|
|
LONG_HISTORY = os.path.join("data", "research", "long_history.pkl")
|
|
|
|
|
|
def bench_returns(index: pd.DatetimeIndex, ticker: str = "QQQ") -> pd.Series:
|
|
with open(LONG_HISTORY, "rb") as fh:
|
|
px = pickle.load(fh)["close"][ticker].dropna()
|
|
return px.reindex(index.union(px.index)).ffill().pct_change(fill_method=None).reindex(index)
|
|
|
|
|
|
def _nw_se(x: np.ndarray, e: np.ndarray, lags: int = 10) -> np.ndarray:
|
|
"""Newey-West standard errors of OLS coefficients."""
|
|
n = len(e)
|
|
xtx_inv = np.linalg.inv(x.T @ x)
|
|
u = x * e[:, None]
|
|
s = u.T @ u
|
|
for l in range(1, lags + 1):
|
|
w = 1 - l / (lags + 1)
|
|
g = u[l:].T @ u[:-l]
|
|
s += w * (g + g.T)
|
|
return np.sqrt(np.diag(xtx_inv @ s @ xtx_inv))
|
|
|
|
|
|
def alpha_stats(r: pd.Series, b: pd.Series, rf: pd.Series | None = None) -> dict:
|
|
df = pd.DataFrame({"r": r, "b": b}).dropna()
|
|
if rf is None:
|
|
rf = tbill(df.index)
|
|
d = rf.reindex(df.index).fillna(0.0) / 252
|
|
y, xb = (df["r"] - d).to_numpy(), (df["b"] - d).to_numpy()
|
|
X = np.column_stack([np.ones(len(y)), xb])
|
|
coef, *_ = np.linalg.lstsq(X, y, rcond=None)
|
|
se = _nw_se(X, y - X @ coef)
|
|
active = df["r"] - df["b"]
|
|
yrs = len(df) / 252
|
|
cagr = lambda x: (1 + x).prod() ** (1 / yrs) - 1 # noqa: E731
|
|
yr_r = (1 + df["r"]).groupby(df.index.year).prod()
|
|
yr_b = (1 + df["b"]).groupby(df.index.year).prod()
|
|
return {"alpha": coef[0] * 252, "t": coef[0] / se[0], "beta": coef[1],
|
|
"ir": active.mean() / active.std() * np.sqrt(252),
|
|
"cagr": cagr(df["r"]), "bench_cagr": cagr(df["b"]),
|
|
"yrs_beat": float((yr_r > yr_b).mean())}
|
|
|
|
|
|
def fmt_alpha(name: str, s: dict) -> str:
|
|
return (f"{name:44s} alpha {s['alpha']*100:6.1f}%/yr t {s['t']:5.2f} beta {s['beta']:4.2f} "
|
|
f"IR {s['ir']:5.2f} cagr {s['cagr']*100:5.1f}% vs {s['bench_cagr']*100:5.1f}% "
|
|
f"yrs beat {s['yrs_beat']*100:3.0f}%")
|