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>
This commit is contained in:
2026-09-29 13:40:15 +08:00
parent 2c19e5efe5
commit ad569f1cea
24 changed files with 4086 additions and 4 deletions

View File

@@ -10,8 +10,9 @@ import pandas as pd
import trader
from strategies.permanent import ETF_UNIVERSE
from strategies.robust_momentum import RobustMomentumStrategy
from strategies.qqq_trend import QQQVolTrend
from strategies.sleeve_portfolio import (SleevePortfolioStrategy, decisions, drifting_shares,
us_three_sleeve)
us_momentum_qqq_trend, us_three_sleeve)
def _bars(n_days: int = 520, n_stocks: int = 30, seed: int = 1) -> dict[str, pd.DataFrame]:
@@ -89,5 +90,51 @@ class SleevePortfolioTests(unittest.TestCase):
self.assertEqual(trader.strategy_fields("us_three_sleeve"), ("close", "open"))
def _qqq_bars(n_days: int = 520, drift: float = 0.0005, seed: int = 2) -> dict[str, pd.DataFrame]:
"""Stocks plus the combo's ETFs only (its live universe: S&P 500 + QQQ, TQQQ, SHY)."""
other = [c for c in ETF_UNIVERSE if c not in ("TQQQ", "SHY")]
bars = {f: b.drop(columns=other) for f, b in _bars(n_days, seed=seed).items()}
rng = np.random.default_rng(seed + 1)
idx = bars["close"].index
q = pd.Series(100 * np.exp(np.cumsum(rng.normal(drift, 0.012, n_days))), index=idx)
for f in bars:
bars[f]["QQQ"] = q if f == "close" else q.shift(1).bfill()
return bars
class QQQTrendTests(unittest.TestCase):
def test_weights_on_a_day_ignore_that_day_and_later_closes(self):
close = _qqq_bars()["close"]
base = QQQVolTrend().generate_signals(close)
shocked = close.copy()
cut = close.index[400]
shocked.loc[cut:, "QQQ"] *= 0.5
pd.testing.assert_frame_equal(base.loc[:cut], QQQVolTrend().generate_signals(shocked).loc[:cut])
def test_out_below_the_average_and_capped_above_it(self):
up = _qqq_bars(drift=0.002)["close"]
down = _qqq_bars(drift=-0.002)["close"]
w_up = QQQVolTrend(cap=1.5).generate_signals(up).iloc[260:]
w_down = QQQVolTrend().generate_signals(down).iloc[260:]
self.assertLessEqual(w_up["TQQQ"].max(), 0.5 + 1e-12)
self.assertGreater(w_up["TQQQ"].mean(), 0.3)
np.testing.assert_allclose(w_up[["TQQQ", "SHY"]].sum(axis=1), 1.0)
self.assertLess(w_down["TQQQ"].mean(), 0.05)
def test_momentum_qqq_trend_is_a_us_mixed_strategy(self):
self.assertIsInstance(trader.STRATEGY_REGISTRY["us_momentum_qqq_trend"](), SleevePortfolioStrategy)
self.assertEqual(set(trader.strategy_extra_tickers("us_momentum_qqq_trend")), {"QQQ", "SHY", "TQQQ"})
self.assertFalse(trader.strategy_supports_market("us_momentum_qqq_trend", "cn"))
bars = _qqq_bars()
strat = us_momentum_qqq_trend()
w = strat.generate_signals(bars)
self.assertLessEqual(w.sum(axis=1).max(), 1.0 + 1e-9)
stocks = [c for c in w.columns if c[1:].isdigit()]
self.assertLessEqual(int((w[stocks].iloc[-1] > 0).sum()), 10)
self.assertGreater(w[["TQQQ", "SHY"]].iloc[-1].sum(), 0.3)
days = bars["close"].index[-2:]
pd.testing.assert_frame_equal(trader.bar_session_weights(strat, bars, days), w.loc[days])
if __name__ == "__main__":
unittest.main()