| Overall Statistics |
|
Total Orders 3672 Average Win 1.12% Average Loss -0.69% Compounding Annual Return 224.021% Drawdown 53.700% Expectancy 0.617 Start Equity 10000 End Equity 18678469.74 Net Profit 186684.697% Sharpe Ratio 3.313 Sortino Ratio 3.983 Probabilistic Sharpe Ratio 99.962% Loss Rate 39% Win Rate 61% Profit-Loss Ratio 1.63 Alpha 1.326 Beta 1.267 Annual Standard Deviation 0.434 Annual Variance 0.189 Information Ratio 3.547 Tracking Error 0.381 Treynor Ratio 1.136 Total Fees $182788.80 Estimated Strategy Capacity $17000000.00 Lowest Capacity Asset BTAL UZWBUH9JN52D Portfolio Turnover 19.98% Drawdown Recovery 133 |
# =============================================================================
# 504 + Velocity Crash Guard — long-only leveraged-ETF tactical rotation
# =============================================================================
#
# WHAT THIS IS
# ------------
# This is the QuantConnect community / corpus strategy commonly referred to as
# "504 — Multi-Model Tactical ETF Rotation": a 4-way EQUAL-WEIGHT ENSEMBLE of
# four independent sub-strategies (T11 + T10 + S3 + S2), each running its full
# logic on 25% of capital, with overlapping positions weight-summed before
# execution. The four sleeves rotate among leveraged ETFs (TQQQ / SOXL / TECL /
# SPXL), inverse / vol instruments (SQQQ / UVXY / UVIX / SVIX) and defensives
# (TLT / BIL / BSV / BTAL / bonds) using SMA regime gates + RSI cascades.
#
# THE ONE ADDITION vs the original 504
# ------------------------------------
# A single VELOCITY CRASH GUARD overlay (the only change; the four sleeves are
# otherwise untouched):
#
# if QQQ trailing 10-day return < -10% -> de-lever the whole book to 50%
# (the other 50% sits in cash)
# hold de-levered until QQQ 10-day return recovers above -4% (hysteresis)
#
# WHY: the four sleeves all use SLOW signals (200/202-day SMA, RSI). In a
# velocity crash (e.g. COVID-2020: TQQQ -68% in 29 days) the SMAs lag by weeks,
# so the book is still fully leveraged when the crash hits. The 10-day return
# trigger reacts in days, not weeks. De-levering to 50% (instead of fully flat)
# keeps half the exposure so the strategy still rides the V-shaped recovery.
#
# RISK PROFILE (our QC backtests, $10k start, IBKR margin model + slippage)
# ------------------------------------------------------------------------
# Original 504 504 + Crash Guard
# In-sample 2010-2020: CAGR 57.7% CAGR 61.9%
# MaxDD 70.1% --> MaxDD 54.3% (-15.8 pp)
# Sharpe 1.16 Sharpe 1.27
# Out-sample 2021-2025: CAGR 225.4% CAGR 211.0% (insurance cost)
# MaxDD 32.9% --> MaxDD 30.6%
# Sharpe 3.30 Sharpe 3.25
#
# The guard cuts drawdown on BOTH windows while keeping CAGR ~intact (it raises
# in-sample CAGR and gives up ~6% out-of-sample as a small insurance premium).
#
# HONEST CAVEATS (read before risking real money)
# -----------------------------------------------
# * 3x LEVERAGED ETFs. TQQQ/SOXL/TECL/SPXL can lose 50-80%+ in a severe bear.
# Even WITH the guard, in-sample MaxDD is ~54%. This is a high-octane sleeve,
# not a standalone all-weather portfolio.
# * The guard's proven benefit is the VELOCITY-CRASH class (COVID-type). The
# big in-sample drawdown reduction leans heavily on a single event (2020).
# * Parameter-robust: a +/- sweep of the threshold (-9/-11%), lookback (8/12d)
# and de-lever level (40/60%) all land in the same place (MaxDD ~51-58% IS,
# CAGR preserved) -> not a knife-edge curve fit.
# * NOT a universal overlay: the same guard bolted onto a DIFFERENT leveraged
# strategy HURT it. The benefit is specific to this ensemble's structure
# (its sleeves go fully-leveraged INTO velocity crashes).
# * Out-of-sample (2021-2025) was used during parameter selection, so it is no
# longer a fully clean hold-out. Forward / paper-trade before live capital.
#
# EXECUTION / FILL MODEL
# ----------------------
# Daily resolution. Decisions are taken on the prior session's close (on_data
# fires post-close) and orders are MarketOnOpenOrder -> filled at the NEXT
# session's OPEN. No look-ahead. Verified: 100% of orders are type-4 MOO and
# 1,056 sampled fills across 2010-2025 reconcile to the next-day open.
#
# Original strategy credit: "504 — Multi-Model Tactical ETF Rotation"
# (QuantConnect community / corpus). Crash-guard idea adapted from the public
# "LRS Sentinel" TQQQ/SOXL regime-rotation write-up.
# =============================================================================
from AlgorithmImports import *
class QuadEnsemble(QCAlgorithm):
"""
4-Way Equal-Weight Ensemble: T11 + T10 + S3 + S2 (25% each)
+ a velocity crash guard (QQQ 10-day return < -10% -> de-lever to 50%).
T11 — FeaverFrontrunner (XLK/KMLM switcher + 50/50 bear split)
T10 — SimonsKMLM (RSI cascade + XLK/KMLM switcher)
S3 — DailyRegimeRotation (3-of-4 SMA vote + RSI triggers)
S2 — HolyGrail (TQQQ 200-SMA gate + BSV defensive)
"""
QUARTER = 0.25
SVIX_LIVE = datetime(2022, 3, 30)
UVIX_LIVE = datetime(2022, 3, 30)
_T10_LATE = {"KMLM", "LABU", "QQQE", "VOOG", "VOOV"}
_T11_LATE = {"KMLM"}
# Crash guard parameters (robust across a +/- sweep; see header)
CRASH_ENTRY = -0.10 # QQQ 10-day return below this -> de-lever
CRASH_EXIT = -0.04 # recover above this -> resume full exposure
CRASH_LOOKBACK = 10 # trading-day window for the return
CRASH_GROSS = 0.50 # gross exposure while in crash (rest cash)
def initialize(self) -> None:
self.set_start_date(2020, 1, 1)
self.set_end_date(2026, 12, 31) # fixed end date avoids clock/tz non-reproducibility
self.set_cash(10_000)
self.set_brokerage_model(
BrokerageName.INTERACTIVE_BROKERS_BROKERAGE,
AccountType.MARGIN,
)
self.set_benchmark("SPY")
self.settings.minimum_order_margin_portfolio_percentage = 0.0
res = Resolution.DAILY
# ── Universe (deduplicated) ─────────────────────────────────────────────
all_tickers = [
"TQQQ", "TECL", "SOXL", "SQQQ", "UVXY", "SVIX", "SVXY",
"XLK", "KMLM", "TLT",
"QQQE", "VTV", "VOX", "VOOG", "VOOV", "XLP", "XLY", "FAS",
"SPXL", "LABU",
"SPY", "IOO", "VTV", "XLF",
"TECS", "SOXS",
"QQQ", "PSQ", "QLD", "BTAL", "BIL", "AGG", "SH", "BND", "IEF",
"BSV",
"SMH", "UVIX",
]
seen = set()
unique = [t for t in all_tickers if not (t in seen or seen.add(t))]
self._syms = {t: self.add_equity(t, res).symbol for t in unique}
# ── T10 indicators ────────────────────────────────────────────
def rsi10(t): return self.rsi(self._syms[t], 10, MovingAverageType.WILDERS, res)
self._t10_rsi = {t: rsi10(t) for t in [
"QQQE", "VTV", "VOX", "TECL", "VOOG", "VOOV", "XLP",
"TQQQ", "XLY", "FAS", "SPY",
"SOXL", "SPXL", "LABU", "XLK", "KMLM",
]}
# ── T11 indicators ────────────────────────────────────────────
self._t11_rsi10 = {t: rsi10(t) for t in [
"SPY", "IOO", "TQQQ", "VTV", "XLF",
"XLK", "KMLM", "PSQ", "BND", "QQQ", "IEF",
]}
def rsi20(t): return self.rsi(self._syms[t], 20, MovingAverageType.WILDERS, res)
self._t11_rsi20 = {t: rsi20(t) for t in ["TLT", "PSQ", "AGG"]}
self._t11_rsi60_sh = self.rsi(self._syms["SH"], 60, MovingAverageType.WILDERS, res)
self._t11_spy_sma200 = self.sma(self._syms["SPY"], 200, res)
self._t11_tqqq_sma20 = self.sma(self._syms["TQQQ"], 20, res)
self._t11_kmlm_sma20 = self.sma(self._syms["KMLM"], 20, res)
# ── S2 indicators ──────────────────────────────────────────────
self._s2_tqqq_sma200 = self.sma(self._syms["TQQQ"], 200, res)
self._s2_tqqq_sma20 = self.sma(self._syms["TQQQ"], 20, res)
self._s2_tqqq_rsi10 = rsi10("TQQQ")
self._s2_soxl_rsi10 = rsi10("SOXL")
self._s2_sqqq_rsi10 = rsi10("SQQQ")
self._s2_bsv_rsi10 = rsi10("BSV")
# ── S3 indicators ─────────────────────────────────────────────
self._s3_spy_sma202 = self.sma(self._syms["SPY"], 202, res)
self._s3_qqq_sma202 = self.sma(self._syms["QQQ"], 202, res)
self._s3_smh_sma202 = self.sma(self._syms["SMH"], 202, res)
self._s3_soxl_sma202 = self.sma(self._syms["SOXL"], 202, res)
def rsi8(t): return self.rsi(self._syms[t], 8, MovingAverageType.WILDERS, res)
def rsi15(t): return self.rsi(self._syms[t], 15, MovingAverageType.WILDERS, res)
self._s3_rsi_qqq8 = rsi8("QQQ")
self._s3_rsi_smh8 = rsi8("SMH")
self._s3_rsi_spy15 = rsi15("SPY")
self._s3_rsi_qqq15 = rsi15("QQQ")
self._s3_rsi_smh15 = rsi15("SMH")
self._s3_rsi_soxl15 = rsi15("SOXL")
self.set_warm_up(215, res)
self._trade_count = 0
self._last_label = ""
# ── Crash guard state ─────────────────────────────────────────
self._qqq_window = RollingWindow[float](self.CRASH_LOOKBACK + 1)
self._in_crash = False
# ── Readiness ────────────────────────────────────────────────────
@property
def _t10_ready(self) -> bool:
core = [v for k, v in self._t10_rsi.items() if k not in self._T10_LATE]
return not self.is_warming_up and all(r.is_ready for r in core)
@property
def _t11_ready(self) -> bool:
core10 = [v for k, v in self._t11_rsi10.items() if k not in self._T11_LATE]
return (not self.is_warming_up
and self._t11_spy_sma200.is_ready
and self._t11_tqqq_sma20.is_ready
and all(r.is_ready for r in core10)
and all(r.is_ready for r in self._t11_rsi20.values())
and self._t11_rsi60_sh.is_ready)
@property
def _s2_ready(self) -> bool:
return (not self.is_warming_up
and self._s2_tqqq_sma200.is_ready
and self._s2_tqqq_sma20.is_ready
and self._s2_tqqq_rsi10.is_ready
and self._s2_soxl_rsi10.is_ready
and self._s2_sqqq_rsi10.is_ready
and self._s2_bsv_rsi10.is_ready)
@property
def _s3_ready(self) -> bool:
return (not self.is_warming_up
and self._s3_spy_sma202.is_ready
and self._s3_qqq_sma202.is_ready
and self._s3_smh_sma202.is_ready
and self._s3_soxl_sma202.is_ready
and self._s3_rsi_qqq8.is_ready
and self._s3_rsi_smh8.is_ready
and self._s3_rsi_spy15.is_ready
and self._s3_rsi_qqq15.is_ready
and self._s3_rsi_smh15.is_ready
and self._s3_rsi_soxl15.is_ready)
# ── T11 helpers ────────────────────────────────────────────────
def _t11_bond_baller(self, r10, r20, tqqq_px, tqqq_sma) -> str:
if r20["TLT"] > r20["PSQ"]: return "QQQ"
if tqqq_px > tqqq_sma:
if r10["PSQ"] < 35: return "PSQ"
if r20["AGG"] > self._t11_rsi60_sh.current.value: return "TQQQ"
return "PSQ"
else:
if r10["IEF"] > r20["PSQ"]: return "PSQ"
return "SQQQ"
def _t11_feaver_bear(self, r10, r20, tqqq_px, tqqq_sma) -> str:
hist = self.history(self._syms["QQQ"], 61, Resolution.DAILY)
qqq_60d = 0.0
if not hist.empty and len(hist) >= 61:
c = hist["close"].values
qqq_60d = (c[-1] / c[0] - 1) * 100
if qqq_60d < -12:
if r10["BND"] > r10["QQQ"]: return "QLD"
return "BTAL"
if tqqq_px > tqqq_sma:
if r10["PSQ"] < 35: return "PSQ"
if r20["AGG"] > self._t11_rsi60_sh.current.value: return "TQQQ"
return "PSQ"
else:
if r10["IEF"] > r20["PSQ"]: return "PSQ"
return "SQQQ"
# ── Strategy signals → weight dicts ────────────────────────────────
def _t10_weights(self) -> dict:
if not self._t10_ready: return {}
r = {t: self._t10_rsi[t].current.value for t in self._t10_rsi
if self._t10_rsi[t].is_ready}
if (r.get("QQQE",0)>79 or r.get("VTV",0)>79 or r.get("VOX",0)>79
or r.get("TECL",0)>79 or r.get("VOOG",0)>79 or r.get("VOOV",0)>79
or r.get("XLP",0)>75 or r.get("TQQQ",0)>79 or r.get("XLY",0)>80
or r.get("FAS",0)>80 or r.get("SPY",0)>80):
return {self._syms["UVXY"]: self.QUARTER}
if r.get("TQQQ",50) < 30: return {self._syms["TECL"]: self.QUARTER}
if r.get("SOXL",50) < 30: return {self._syms["SOXL"]: self.QUARTER}
if r.get("SPXL",50) < 30: return {self._syms["SPXL"]: self.QUARTER}
if self._t10_rsi["LABU"].is_ready and r["LABU"] < 25:
return {self._syms["LABU"]: self.QUARTER}
vs = "SVIX" if self.time >= self.SVIX_LIVE else "SVXY"
kmlm_ready = self._t10_rsi["KMLM"].is_ready
xlk_wins = (not kmlm_ready) or (r["XLK"] > r["KMLM"])
if xlk_wins:
return {self._syms["TECL"]: self.QUARTER/3,
self._syms["SOXL"]: self.QUARTER/3,
self._syms[vs]: self.QUARTER/3}
return {self._syms["SQQQ"]: self.QUARTER*0.5,
self._syms["TLT"]: self.QUARTER*0.5}
def _t11_weights(self) -> dict:
if not self._t11_ready: return {}
r10 = {t: self._t11_rsi10[t].current.value for t in self._t11_rsi10
if self._t11_rsi10[t].is_ready}
r20 = {t: self._t11_rsi20[t].current.value for t in self._t11_rsi20}
spy_px = self.securities[self._syms["SPY"]].close
tqqq_px = self.securities[self._syms["TQQQ"]].close
kmlm_px = self.securities[self._syms["KMLM"]].close
spy_sma = self._t11_spy_sma200.current.value
tqqq_sma = self._t11_tqqq_sma20.current.value
kmlm_sma = self._t11_kmlm_sma20.current.value
ob79 = (r10["SPY"]>79 or r10["IOO"]>79 or r10["TQQQ"]>79
or r10["VTV"]>79 or r10["XLF"]>79)
if ob79:
ob81 = (r10["SPY"]>81 or r10["IOO"]>81 or r10["TQQQ"]>81
or r10["VTV"]>81 or r10["XLF"]>81)
if ob81:
return {self._syms["UVXY"]: self.QUARTER}
return {self._syms["UVXY"]: self.QUARTER/3,
self._syms["BIL"]: self.QUARTER/3,
self._syms["BTAL"]: self.QUARTER/3}
if r10["TQQQ"] < 30: return {self._syms["TQQQ"]: self.QUARTER}
if r10["SPY"] < 30: return {self._syms["SPXL"]: self.QUARTER}
if spy_px > spy_sma:
kmlm_ready = self._t11_rsi10["KMLM"].is_ready and self._t11_kmlm_sma20.is_ready
if not kmlm_ready or r10["XLK"] > r10["KMLM"]:
return {self._syms["TECL"]: self.QUARTER/3,
self._syms["SOXL"]: self.QUARTER/3,
self._syms["TQQQ"]: self.QUARTER/3}
if kmlm_px < kmlm_sma:
return {self._syms["TECL"]: self.QUARTER/3,
self._syms["SOXL"]: self.QUARTER/3,
self._syms["TQQQ"]: self.QUARTER/3}
return {self._syms["TECS"]: self.QUARTER/3,
self._syms["SOXS"]: self.QUARTER/3,
self._syms["SQQQ"]: self.QUARTER/3}
else:
bb = self._t11_bond_baller(r10, r20, tqqq_px, tqqq_sma)
fb = self._t11_feaver_bear(r10, r20, tqqq_px, tqqq_sma)
w: dict = {}
w[self._syms[bb]] = w.get(self._syms[bb], 0) + self.QUARTER * 0.5
w[self._syms[fb]] = w.get(self._syms[fb], 0) + self.QUARTER * 0.5
return w
def _s2_weights(self) -> dict:
if not self._s2_ready: return {}
tqqq_price = self.securities[self._syms["TQQQ"]].close
tqqq_rsi = self._s2_tqqq_rsi10.current.value
soxl_rsi = self._s2_soxl_rsi10.current.value
sqqq_rsi = self._s2_sqqq_rsi10.current.value
bsv_rsi = self._s2_bsv_rsi10.current.value
sma200 = self._s2_tqqq_sma200.current.value
sma20 = self._s2_tqqq_sma20.current.value
if tqqq_price > sma200:
sym = self._syms["UVXY"] if tqqq_rsi > 79 else self._syms["TQQQ"]
return {sym: self.QUARTER}
if tqqq_rsi < 31: return {self._syms["TECL"]: self.QUARTER}
if soxl_rsi < 30: return {self._syms["SOXL"]: self.QUARTER}
if tqqq_price < sma20:
sym = self._syms["SQQQ"] if sqqq_rsi > bsv_rsi else self._syms["BSV"]
return {sym: self.QUARTER}
return {self._syms["TQQQ"]: self.QUARTER}
def _s3_weights(self) -> dict:
if not self._s3_ready: return {}
spy_bull = self.securities[self._syms["SPY"]].price > self._s3_spy_sma202.current.value
qqq_bull = self.securities[self._syms["QQQ"]].price > self._s3_qqq_sma202.current.value
smh_bull = self.securities[self._syms["SMH"]].price > self._s3_smh_sma202.current.value
soxl_bull = self.securities[self._syms["SOXL"]].price > self._s3_soxl_sma202.current.value
bull = (int(spy_bull)+int(qqq_bull)+int(smh_bull)+int(soxl_bull)) >= 3
overbought = (self._s3_rsi_spy15.current.value > 72 or
self._s3_rsi_qqq15.current.value > 72 or
self._s3_rsi_smh15.current.value > 72 or
self._s3_rsi_soxl15.current.value > 72)
if bull:
if overbought:
vol = (self._syms["UVIX"]
if (self.time >= self.UVIX_LIVE
and self.securities[self._syms["UVIX"]].has_data
and self.securities[self._syms["UVIX"]].price > 0)
else self._syms["UVXY"])
return {vol: self.QUARTER}
return {self._syms["TQQQ"]: self.QUARTER*0.5,
self._syms["SOXL"]: self.QUARTER*0.5}
if self._s3_rsi_qqq8.current.value < 29 or self._s3_rsi_smh8.current.value < 31:
return {self._syms["SOXL"]: self.QUARTER}
return {} # cash
# ── Merge and apply ─────────────────────────────────────────────
def _rebalance(self) -> None:
# ── Crash guard: QQQ 10-day return < -10% -> de-lever to CRASH_GROSS until
# the 10-day return recovers above -4% (asymmetric hysteresis).
if self._qqq_window.is_ready:
qqq_ret = self._qqq_window[0] / self._qqq_window[self.CRASH_LOOKBACK] - 1.0
if not self._in_crash and qqq_ret < self.CRASH_ENTRY:
self._in_crash = True
self.log(f"[CRASH] {self.time.date()} | QQQ {self.CRASH_LOOKBACK}d ret = {qqq_ret*100:.1f}% -> de-lever {self.CRASH_GROSS:.0%}")
elif self._in_crash and qqq_ret > self.CRASH_EXIT:
self._in_crash = False
self.log(f"[RECOVER] {self.time.date()} | QQQ {self.CRASH_LOOKBACK}d ret = {qqq_ret*100:.1f}% -> resume full")
crash_gross = self.CRASH_GROSS if self._in_crash else 1.0
w10 = self._t10_weights()
w11 = self._t11_weights()
w2 = self._s2_weights()
w3 = self._s3_weights()
def lbl(w): return "+".join(f"{round(wt/self.QUARTER*100):.0f}%{s.value}"
for s, wt in w.items()) if w else "CASH"
new_label = f"T10={lbl(w10)}|T11={lbl(w11)}|S2={lbl(w2)}|S3={lbl(w3)}|cg={crash_gross}"
# Only trade when the target set changes
if new_label == self._last_label:
return
combined: dict = {}
for w in [w10, w11, w2, w3]:
for sym, wt in w.items():
combined[sym] = combined.get(sym, 0.0) + wt * crash_gross
# Liquidate anything no longer targeted — fill at tomorrow's open
targets = set(combined)
for h in list(self.portfolio.values()):
if h.invested and h.symbol not in targets:
self.market_on_open_order(h.symbol, -h.quantity)
# Set target weights — fill at tomorrow's open (MarketOnOpen, no look-ahead)
pv = self.portfolio.total_portfolio_value
for sym, wt in combined.items():
price = self.securities[sym].price
if price <= 0:
continue
target_qty = int(pv * wt / price)
delta = target_qty - int(self.portfolio[sym].quantity)
if delta != 0:
self.market_on_open_order(sym, delta)
self._trade_count += 1
net = "+".join(f"{round(w*100):.0f}%{s.value}"
for s, w in sorted(combined.items(), key=lambda x: -x[1]))
self.log(f"[{self._trade_count:04d}] {self.time.date()} | net={net}")
self._last_label = new_label
def on_data(self, data: Slice) -> None:
# Feed the QQQ rolling window (also during warmup, so the guard is armed at day 0)
qqq_sym = self._syms["QQQ"]
if data.bars.contains_key(qqq_sym):
self._qqq_window.add(data.bars[qqq_sym].close)
if self.is_warming_up:
return
self._rebalance()
def on_end_of_algorithm(self) -> None:
self.log(f"\n Final NAV: ${self.portfolio.total_portfolio_value:>15,.2f} | State changes: {self._trade_count}")