Files
quant-trading/research/lab/qqq_scan.py
Gahow Wang ad569f1cea feat: us_momentum_qqq_trend; alpha-vs-QQQ research across new data sources
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>
2026-09-29 13:40:15 +08:00

144 lines
6.0 KiB
Python

"""
Which strategies and factors carry alpha against QQQ, before and after 2017?
PYTHONPATH=. uv run python -m research.lab.fetch_long_history # once (QQQ)
PYTHONPATH=. uv run --with numba python -m research.lab.qqq_scan [--part sleeves|factors|mega]
sleeves the current sleeves (us_final, short_horizon) and TrendRider
factors every price factor (ranks_us) and every EDGAR fundamental, long-only
top 20 among PIT S&P 500 members, every 21 days, buffer 2
mega the same idea inside a QQQ-like universe: the 100 members with the
highest 126-day dollar volume (market caps are missing for many
delisted names, dollar volume is not), top 10 / 20
Alpha is the intercept of daily excess returns on QQQ's excess return, with
a Newey-West t-stat (research/lab/alpha.py). Periods: design 2006-09..2016,
holdout 2017..2026-09. A factor whose alpha is positive in both, with t > 2
over the whole span, is a candidate; everything else is noise or beta.
Writes data/research/results/qqq_scan_{part}.pkl (daily returns) and .csv.
"""
from __future__ import annotations
import argparse
import dataclasses
import os
import pickle
import warnings
import pandas as pd
from research.lab import fundamentals as fu
from research.lab.alpha import alpha_stats, bench_returns, fmt_alpha
from research.lab.factor_scan import RESULTS, load
from research.lab.factors import _dollar_volume
from research.lab.rotation import factor_ranks
from research.lab.us_sweep import _blend, run, sector_rank
warnings.filterwarnings("ignore")
LONG = ("2006-09-27", "2026-09-24")
DESIGN = ("2006-09-27", "2016-12-31")
HOLDOUT = ("2017-01-01", "2026-09-24")
def report(name: str, r: pd.Series, rows: list, bench: pd.Series, spy: pd.Series) -> None:
r = r.dropna()
full = alpha_stats(r, bench.reindex(r.index))
d = alpha_stats(r.loc[DESIGN[0]:DESIGN[1]], bench)
h = alpha_stats(r.loc[HOLDOUT[0]:HOLDOUT[1]], bench)
s = alpha_stats(r, spy.reindex(r.index))
rows.append({"name": name, **{f"full_{k}": v for k, v in full.items()},
**{f"design_{k}": v for k, v in d.items()}, **{f"holdout_{k}": v for k, v in h.items()},
"spy_alpha": s["alpha"], "spy_t": s["t"]})
print(f"{fmt_alpha(name, full)} | design a {d['alpha']*100:5.1f}% t {d['t']:5.2f} | "
f"holdout a {h['alpha']*100:5.1f}% t {h['t']:5.2f} cagr {h['cagr']*100:5.1f}% vs {h['bench_cagr']*100:5.1f}% "
f"| vs SPY a {s['alpha']*100:5.1f}% t {s['t']:5.2f}", flush=True)
def part_sleeves(bench, spy, rows) -> dict:
R = pd.read_pickle(os.path.join(RESULTS, "us_final_sleeves.pkl"))
extra = os.path.join(RESULTS, "short_horizon_sleeves.pkl")
if os.path.exists(extra):
R = R.join(pd.read_pickle(extra))
with open(os.path.join("data", "research", "letf_history.pkl"), "rb") as fh:
tr = pickle.load(fh)["equity"]
R["trend_rider_v7_vt24 (synthetic)"] = tr["trend_rider_v7_vt24 [lag]"].pct_change().reindex(R.index)
for c in R.columns:
report(c, R[c], rows, bench, spy)
return {}
def part_factors(p, bench, spy, rows) -> dict:
pr = factor_ranks(p, "us")
F = fu.load(p)
scores = {k: v for k, v in pr.items()}
for k, v in F.items():
if k == "mcap":
continue
scores[f"f:{k}"] = v.where(p.member).rank(axis=1, pct=True)
scores[f"f:{k} (sector)"] = sector_rank(v, p)
out = {}
for name, sc in scores.items():
st = run(p, sc.where(p.member), [LONG], top_n=20, buffer=2.0, every=21)
out[name] = st["_rets"]
report(name, st["_rets"], rows, bench, spy)
return out
def mega_panel(p, n: int = 100):
dv = _dollar_volume(p).rolling(126, min_periods=63).mean().where(p.member)
top = dv.rank(axis=1, ascending=False) <= n
return dataclasses.replace(p, member=p.member & top)
def part_mega(p, bench, spy, rows) -> dict:
pm = mega_panel(p)
pr = factor_ranks(p, "us")
F = fu.load(p)
ew = pm.close.notna().astype(float).where(pm.member)
scores = {
"mega EW (100)": (ew, 10000),
"mega overnight+rec": (_blend(pr["overnight_mom"], pr["rec_126"]), None),
"mega mom_12_1": (pr["mom_12_1"], None), "mega resid_mom": (pr["resid_mom"], None),
"mega high_52w": (pr["high_52w"], None), "mega sharpe_mom": (pr["sharpe_mom"], None),
"mega overnight_mom": (pr["overnight_mom"], None), "mega rec_126": (pr["rec_126"], None),
"mega mom_6_1": (pr["mom_6_1"], None), "mega trend_hzz": (pr["trend_hzz"], None),
"mega low_beta": (pr["low_beta"], None), "mega ivol": (pr["ivol"], None),
"mega gp_a": (F["gp_a"].rank(axis=1, pct=True), None), "mega roe": (F["roe"].rank(axis=1, pct=True), None),
"mega mom_12_1+gp_a": (_blend(pr["mom_12_1"], F["gp_a"].rank(axis=1, pct=True)), None),
}
out = {}
for name, (sc, n_all) in scores.items():
for n in ([n_all] if n_all else (10, 20)):
st = run(pm, sc.where(pm.member), [LONG], top_n=n, buffer=2.0, every=21)
key = name if n_all else f"{name} top{n}"
out[key] = st["_rets"]
report(key, st["_rets"], rows, bench, spy)
return out
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--part", nargs="*", default=["sleeves", "mega", "factors"])
args = ap.parse_args()
p, _ = load("us")
bench = bench_returns(p.dates)
spy = bench_returns(p.dates, "SPY")
report("QQQ itself", bench.loc[LONG[0]:], [], bench, spy)
for part in args.part:
print(f"\n=== {part}: alpha vs QQQ (full 2006-2026) | design 2006-16 | holdout 2017-26 | vs SPY", flush=True)
rows = []
if part == "sleeves":
out = part_sleeves(bench, spy, rows)
elif part == "factors":
out = part_factors(p, bench, spy, rows)
else:
out = part_mega(p, bench, spy, rows)
pd.DataFrame(rows).to_csv(os.path.join(RESULTS, f"qqq_scan_{part}.csv"), index=False)
if out:
pd.DataFrame(out).to_pickle(os.path.join(RESULTS, f"qqq_scan_{part}.pkl"))
if __name__ == "__main__":
main()