| Overall Statistics |
|
Total Orders 1025 Average Win 3.32% Average Loss -1.95% Compounding Annual Return 129.039% Drawdown 31.900% Expectancy 0.598 Start Equity 100000 End Equity 14393116.93 Net Profit 14293.117% Sharpe Ratio 2.008 Sortino Ratio 2.261 Probabilistic Sharpe Ratio 91.367% Loss Rate 41% Win Rate 59% Profit-Loss Ratio 1.70 Alpha 0.875 Beta 0.656 Annual Standard Deviation 0.468 Annual Variance 0.219 Information Ratio 1.826 Tracking Error 0.461 Treynor Ratio 1.433 Total Fees $36420.66 Estimated Strategy Capacity $280000000.00 Lowest Capacity Asset GLD T3SKPOF94JFP Portfolio Turnover 25.27% Drawdown Recovery 525 |
# ==============================================================================
# QuantConnect — K-of-N Regime Rotation (TQQQ + SOXL) with Inverse Defensive
# ==============================================================================
# CRITICAL FIXES:
# 1. VIX accessed via AddIndex (not AddEquity — VIX is a CBOE index, not equity)
# 2. VIX data accessed via data[symbol] (not data.Bars — indices aren't in Bars)
# 3. Defensive sleeve uses SQQQ/SOXS inverse ETFs for CAGR boost
#
# TQQQ: 6/8 K-of-N voting | SQQQ defensive | Target: CAGR>100%, DD<30%
# Atoms: QQQ_leading_SPY(50), QQQ_beat_SPY_42d, SMH_mom63d>0,
# QQQ_SMA200_slope10, QQQ_RSI5<75, VIX<SMA50, VIX_falling(20), QQQ_RSI5<70
#
# SOXL: 5/6 K-of-N voting | SOXS defensive | Target: CAGR>100%, DD<30%
# Atoms: QQQ_leading_SPY(50), T>SMA150, VIX_falling(20), stoch_K<80,
# SMH_mom63d>0, QQQ_SMA150_slope10
#
# ALL indicators are PRICE-BASED. No volume/OBV dependency.
# Inverse ETFs profit during bear markets when K-of-N correctly signals OUT.
# ==============================================================================
from AlgorithmImports import *
from collections import deque
class SOXLKofNStrategy(QCAlgorithm):
"""SOXL 5-of-6 K-of-N voting with SOXS inverse defensive sleeve."""
def Initialize(self):
self.SetStartDate(2020, 6, 1)
self.SetEndDate(2026, 5, 29)
self.SetCash(100000)
self.soxl = self.AddEquity("SOXL", Resolution.Daily).Symbol
self.soxs = self.AddEquity("SOXS", Resolution.Daily).Symbol
self.qqq = self.AddEquity("QQQ", Resolution.Daily).Symbol
self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol
self.smh = self.AddEquity("SMH", Resolution.Daily).Symbol
self.gld = self.AddEquity("GLD", Resolution.Daily).Symbol
# VIX is a CBOE INDEX, not an equity — must use AddIndex
self.vix = self.AddIndex("VIX", Resolution.Daily).Symbol
# History buffers
self.soxl_hist = deque(maxlen=160)
self.qqq_hist = deque(maxlen=160)
self.spy_hist = deque(maxlen=55)
self.smh_hist = deque(maxlen=70)
self.vix_hist = deque(maxlen=25)
self.qqq_spy_ratio_hist = deque(maxlen=55)
self.qqq_sma150_hist = deque(maxlen=15)
self.K_THRESHOLD = 5 # Need 5 of 6 atoms true
self.in_market = True
self.days_in = 0
self.days_out = 0
self.Schedule.On(
self.DateRules.EveryDay("SPY"),
self.TimeRules.BeforeMarketClose("SPY", 1),
self.Rebalance
)
self.SetWarmUp(250, Resolution.Daily)
def OnData(self, data):
if data.Bars.ContainsKey(self.soxl):
self.soxl_hist.append(float(data.Bars[self.soxl].Close))
if data.Bars.ContainsKey(self.qqq):
self.qqq_hist.append(float(data.Bars[self.qqq].Close))
self._update_sma150()
if data.Bars.ContainsKey(self.spy):
self.spy_hist.append(float(data.Bars[self.spy].Close))
if data.Bars.ContainsKey(self.smh):
self.smh_hist.append(float(data.Bars[self.smh].Close))
# VIX is an INDEX — access via data[symbol], NOT data.Bars
if data.ContainsKey(self.vix) and data[self.vix] is not None:
self.vix_hist.append(float(data[self.vix].Close))
if len(self.qqq_hist) > 0 and len(self.spy_hist) > 0:
self.qqq_spy_ratio_hist.append(self.qqq_hist[-1] / self.spy_hist[-1])
def _update_sma150(self):
if len(self.qqq_hist) >= 150:
sma = sum(list(self.qqq_hist)[-150:]) / 150.0
self.qqq_sma150_hist.append(sma)
def _qqq_leading_spy(self):
"""QQQ/SPY ratio > 50-day SMA of ratio."""
if len(self.qqq_spy_ratio_hist) < 50:
return False
vals = list(self.qqq_spy_ratio_hist)[-50:]
return vals[-1] > sum(vals) / 50.0
def _soxl_above_sma150(self):
"""SOXL close > 150-day SMA of SOXL close."""
if len(self.soxl_hist) < 150:
return False
vals = list(self.soxl_hist)[-150:]
return vals[-1] > sum(vals) / 150.0
def _vix_falling(self):
"""VIX < 20-day SMA of VIX."""
if len(self.vix_hist) < 20:
return False
vals = list(self.vix_hist)[-20:]
return vals[-1] < sum(vals) / 20.0
def _stoch_k_below_80(self):
"""QQQ 14-day stochastic %K < 80."""
if len(self.qqq_hist) < 14:
return True
vals = list(self.qqq_hist)[-14:]
low14 = min(vals)
high14 = max(vals)
if high14 == low14:
return True
stoch_k = 100.0 * (vals[-1] - low14) / (high14 - low14)
return stoch_k < 80.0
def _smh_mom63d_pos(self):
"""SMH 63-day return > 0."""
if len(self.smh_hist) < 64:
return False
return self.smh_hist[-1] > self.smh_hist[-64]
def _qqq_sma150_slope10(self):
"""QQQ 150-day SMA today > QQQ 150-day SMA 10 days ago."""
if len(self.qqq_sma150_hist) < 11:
return False
return self.qqq_sma150_hist[-1] > self.qqq_sma150_hist[-11]
def Rebalance(self):
if self.IsWarmingUp:
return
# Compute all 6 atoms
score = 0
score += int(self._qqq_leading_spy()) # Atom 1
score += int(self._soxl_above_sma150()) # Atom 2: T>SMA150
score += int(self._vix_falling()) # Atom 3
score += int(self._stoch_k_below_80()) # Atom 4
score += int(self._smh_mom63d_pos()) # Atom 5
score += int(self._qqq_sma150_slope10()) # Atom 6
# TIERED REGIME:
# score >= 5: BULLISH → 100% SOXL
# score 3-4: MILD OUT → 100% GLD (no inverse — avoids whipsaw)
# score <= 2: STRONG BEAR → 20% SOXS + 80% GLD (confirmed downturn)
in_market = score >= self.K_THRESHOLD
if in_market:
# BULLISH: 100% SOXL
self.SetHoldings(self.soxl, 1.0)
if self.Portfolio[self.soxs].Invested:
self.Liquidate(self.soxs)
if self.Portfolio[self.gld].Invested:
self.Liquidate(self.gld)
self.days_in += 1
elif score <= 2:
# STRONG BEAR: only deploy SOXS when firmly bearish
if self.Portfolio[self.soxl].Invested:
self.Liquidate(self.soxl)
self.SetHoldings(self.soxs, 0.20)
self.SetHoldings(self.gld, 0.80)
self.days_out += 1
else:
# MILD OUT (score 3-4): safe haven only, no inverse
if self.Portfolio[self.soxl].Invested:
self.Liquidate(self.soxl)
if self.Portfolio[self.soxs].Invested:
self.Liquidate(self.soxs)
self.SetHoldings(self.gld, 1.0)
self.days_out += 1
if in_market != self.in_market:
self.Debug(f"{self.Time.date()} SOXL {'IN' if in_market else 'OUT'} score={score}/6")
self.in_market = in_market
def OnEndOfAlgorithm(self):
total = self.days_in + self.days_out
tim = self.days_in / total * 100 if total > 0 else 0
self.Debug(f"SOXL Final: ${self.Portfolio.TotalPortfolioValue:,.2f} | "
f"TIM={tim:.1f}% ({self.days_in}/{total})")