Files
quant-trading/research/lab/levered_book.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

173 lines
7.5 KiB
Python

"""
How much return can risk-managed leverage add to the US sleeves, and at what
drawdown? A small account can borrow (Reg-T margin, at most 2x) where a fund
with a mandate cannot; the question is whether scaling exposure by recent
volatility lets that leverage be taken without the tail that static leverage
brings.
PYTHONPATH=. uv run python -m research.lab.levered_book
Input: daily sleeve returns from research/lab/us_final.py (and, when present,
short_horizon.py). Leverage is applied to the sleeve's return stream:
exposure_t = clip(target / vol_hat_{t-1}, 0, cap) (vol_hat from returns through t-1)
r_t = exposure_t * sleeve_t - max(exposure_t - 1, 0) * (T-bill + spread)
- |exposure_t - exposure_{t-1}| * cost
Exposure only moves when it is more than ``band`` away from the current one
(fewer orders). Stock sleeves may go to 2x (Reg-T); the trend sleeve already
holds 3x ETFs, which brokers margin at 75-100%, so it is capped at 1x.
Vol estimators: realized over the last 126 days (Barroso & Santa-Clara 2015)
and EWMA with a 20-day half-life. Targets are round numbers, not fitted.
"""
from __future__ import annotations
import os
import warnings
import numpy as np
import pandas as pd
from research.lab.factor_scan import RESULTS
from research.lab.us_final import monthly_blend
warnings.filterwarnings("ignore")
SPAN = ("2011-01-03", "2026-09-24")
LONG = ("2006-09-27", "2026-09-24")
SPREAD = 0.015 # IBKR benchmark + 1.5% under $100k, +1% to $1M
COST = 0.001 # one-way, per unit of exposure changed
RF_PATH = os.path.join("data", "research", "ext", "fred_dtb3.csv")
def tbill(index: pd.DatetimeIndex) -> pd.Series:
"""Daily 3-month T-bill rate (annual, decimal) from FRED DTB3."""
rf = pd.read_csv(RF_PATH, index_col=0, parse_dates=True).iloc[:, 0]
rf = pd.to_numeric(rf, errors="coerce").ffill() / 100
return rf.reindex(index.union(rf.index)).ffill().reindex(index)
def vol_hat(r: pd.Series, kind: str) -> pd.Series:
if kind == "rv126":
v = np.sqrt((r ** 2).rolling(126, min_periods=63).mean() * 252)
else:
v = np.sqrt((r ** 2).ewm(halflife=20, min_periods=63).mean() * 252)
return v.shift(1)
def banded(target: pd.Series, band: float) -> pd.Series:
out = np.empty(len(target))
cur = np.nan
for i, t in enumerate(target.to_numpy()):
if np.isnan(t):
out[i] = cur if not np.isnan(cur) else 1.0
continue
if np.isnan(cur) or abs(t - cur) > band * max(cur, 0.25):
cur = t
out[i] = cur
return pd.Series(out, index=target.index)
def lever(r: pd.Series, rf: pd.Series, exposure: pd.Series) -> pd.Series:
borrow = (exposure - 1).clip(lower=0) * (rf.reindex(r.index) + SPREAD) / 252
trade = exposure.diff().abs().fillna(0.0) * COST
return exposure * r - borrow - trade
def managed(r: pd.Series, rf: pd.Series, target: float, cap: float, kind: str = "rv126",
band: float = 0.1) -> tuple[pd.Series, pd.Series]:
exp = banded((target / vol_hat(r, kind)).clip(upper=cap), band)
return lever(r, rf, exp), exp
def stats(r: pd.Series, rf: pd.Series) -> dict:
r = r.dropna()
e = (1 + r).cumprod()
yrs = len(r) / 252
yr = (1 + r).groupby(r.index.year).prod() - 1
roll3 = e.pct_change(756).dropna()
return {"cagr": e.iloc[-1] ** (1 / yrs) - 1,
"sharpe": (r - rf.reindex(r.index) / 252).mean() / r.std() * np.sqrt(252),
"vol": r.std() * np.sqrt(252), "mdd": (e / e.cummax() - 1).min(),
"worst_yr": yr.min(), "yrs_below_20": int((yr.iloc[1:-1] < 0.20).sum()),
"n_yrs": len(yr) - 2, "worst_3y": (1 + roll3.min()) ** (1 / 3) - 1}
def fmt(name: str, s: dict, exp: pd.Series | None = None) -> str:
x = f" avg exp {exp.mean():.2f}" if exp is not None else ""
return (f"{name:46s} cagr {s['cagr']*100:5.1f}% xsh {s['sharpe']:.2f} vol {s['vol']*100:4.1f}% "
f"mdd {s['mdd']*100:6.1f}% worst yr {s['worst_yr']*100:6.1f}% worst 3y/yr {s['worst_3y']*100:5.1f}% "
f"yrs<20% {s['yrs_below_20']}/{s['n_yrs']}{x}")
def main() -> None:
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), how="left")
rf = tbill(R.index)
print("=== single sleeves, static vs vol-managed (2006- for stock sleeves, 2010- trend)")
for c in ["momentum", "lowrisk", "trend", "spy"]:
r = R[c].dropna()
cap = 1.0 if c == "trend" else 2.0
print(fmt(f"{c} as is", stats(r, rf)))
for target in (0.15, 0.25):
for kind in ("rv126", "ewma20"):
m, e = managed(r, rf, target, cap, kind)
print(fmt(f" {c} vt{int(target*100)} {kind} cap {cap:.0f}x", stats(m, rf), e))
span = R.loc[SPAN[0]:SPAN[1]].fillna(0.0)
three = {"momentum": 1 / 3, "lowrisk": 1 / 3, "trend": 1 / 3}
book = monthly_blend(span, three)
print("\n=== 3-sleeve book 2011-2026: static leverage vs managed")
for L in (1.0, 1.5, 2.0, 2.5):
e = pd.Series(L, index=book.index)
print(fmt(f"static {L:.1f}x", stats(lever(book, rf, e), rf), e))
for target in (0.20, 0.25, 0.30, 0.35, 0.40):
for kind in ("rv126", "ewma20"):
m, e = managed(book, rf, target, 2.5, kind)
print(fmt(f"book vt{int(target*100)} {kind} cap 2.5x", stats(m, rf), e))
print("\n=== sleeve-level management, then equal capital (stock sleeves cap 2x, trend 1x)")
for target in (0.15, 0.20, 0.25, 0.30):
parts = {}
for c in ("momentum", "lowrisk", "trend"):
cap = 1.0 if c == "trend" else 2.0
parts[c], _ = managed(R[c].dropna(), rf, target, cap)
P = pd.DataFrame(parts).loc[SPAN[0]:SPAN[1]].fillna(0.0)
print(fmt(f"sleeves vt{int(target*100)} rv126, 1/3 each", stats(monthly_blend(P, three), rf)))
print("\n=== the same leverage through 2008 (momentum + lowrisk 50/50, 2006-; no trend sleeve before 2010)")
two = monthly_blend(R.loc[LONG[0]:LONG[1]].fillna(0.0), {"momentum": .5, "lowrisk": .5})
for L in (1.0, 2.0):
e = pd.Series(L, index=two.index)
print(fmt(f"static {L:.1f}x", stats(lever(two, rf, e), rf), e))
for target in (0.25, 0.35):
m, e = managed(two, rf, target, 2.5, "ewma20")
print(fmt(f"vt{int(target*100)} ewma20 cap 2.5x", stats(m, rf), e))
print("\n=== alpha left after hedging market beta (rolling 126d beta, futures hedge), 2011-")
spy = R["spy"]
hedged = {}
for c in ["momentum", "lowrisk", "trend"] + [c for c in R.columns if c.endswith("e21")]:
r = R[c]
cov = (r * spy).rolling(126, min_periods=63).mean() - r.rolling(126, min_periods=63).mean() * spy.rolling(126, min_periods=63).mean()
beta = (cov / spy.rolling(126, min_periods=63).var()).shift(1).clip(0, 2)
hedged[c] = r - beta * (spy - rf / 252)
print(fmt(f"hedged {c} (avg beta {beta.loc[SPAN[0]:].mean():.2f})", stats(hedged[c].loc[SPAN[0]:].dropna(), rf)))
print("\n=== yearly, book vt30 ewma20 cap 2.5x vs 3-sleeve 1x vs SPY")
m, e = managed(book, rf, 0.30, 2.5, "ewma20")
yr = pd.DataFrame({"vt30": (1 + m).groupby(m.index.year).prod() - 1,
"1x": (1 + book).groupby(book.index.year).prod() - 1,
"spy": (1 + span["spy"]).groupby(span.index.year).prod() - 1,
"avg_exp": e.groupby(e.index.year).mean()})
print((yr * [100, 100, 100, 1]).round(1).to_string())
if __name__ == "__main__":
main()