| Overall Statistics |
|
Total Orders 898 Average Win 2.96% Average Loss -2.06% Compounding Annual Return 86.114% Drawdown 31.600% Expectancy 0.473 Start Equity 100000 End Equity 4146630.08 Net Profit 4046.630% Sharpe Ratio 1.785 Sortino Ratio 1.894 Probabilistic Sharpe Ratio 91.388% Loss Rate 40% Win Rate 60% Profit-Loss Ratio 1.44 Alpha 0.497 Beta 0.887 Annual Standard Deviation 0.327 Annual Variance 0.107 Information Ratio 1.607 Tracking Error 0.302 Treynor Ratio 0.658 Total Fees $28549.66 Estimated Strategy Capacity $180000000.00 Lowest Capacity Asset GLD T3SKPOF94JFP Portfolio Turnover 24.23% Drawdown Recovery 440 |
from AlgorithmImports import *
from collections import deque
class TQQQKofNStrategy(QCAlgorithm):
"""TQQQ 6-of-8 K-of-N voting with SQQQ inverse defensive sleeve."""
def Initialize(self):
self.SetStartDate(2020, 6, 1)
self.SetEndDate(2026, 5, 29)
self.SetCash(100000)
self.tqqq = self.AddEquity("TQQQ", Resolution.Daily).Symbol
self.sqqq = self.AddEquity("SQQQ", 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.qqq_hist = deque(maxlen=210)
self.spy_hist = deque(maxlen=210)
self.smh_hist = deque(maxlen=70)
self.vix_hist = deque(maxlen=55)
self.qqq_spy_ratio_hist = deque(maxlen=55)
self.K_THRESHOLD = 6 # Need 6 of 8 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):
# Equities come through data.Bars
if data.Bars.ContainsKey(self.qqq):
self.qqq_hist.append(float(data.Bars[self.qqq].Close))
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 _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 _qqq_beat_spy_42d(self):
"""QQQ 42-day return > SPY 42-day return."""
if len(self.qqq_hist) < 43 or len(self.spy_hist) < 43:
return False
qqq_ret = self.qqq_hist[-1] / self.qqq_hist[-43] - 1
spy_ret = self.spy_hist[-1] / self.spy_hist[-43] - 1
return qqq_ret > spy_ret
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_sma200_slope10(self):
"""QQQ 200-day SMA today > QQQ 200-day SMA 10 days ago."""
if len(self.qqq_hist) < 210:
return False
hist = list(self.qqq_hist)
sma_today = sum(hist[-200:]) / 200.0
sma_10ago = sum(hist[-210:-10]) / 200.0
return sma_today > sma_10ago
def _qqq_rsi5(self):
"""Compute QQQ 5-period RSI."""
if len(self.qqq_hist) < 7:
return 50.0
hist = list(self.qqq_hist)[-6:]
gains = []
losses = []
for i in range(1, len(hist)):
change = hist[i] - hist[i-1]
gains.append(max(change, 0))
losses.append(max(-change, 0))
avg_gain = sum(gains) / 5.0
avg_loss = sum(losses) / 5.0
if avg_loss == 0:
return 100.0
return 100.0 - 100.0 / (1.0 + avg_gain / avg_loss)
def _vix_below_sma50(self):
"""VIX < 50-day SMA of VIX."""
if len(self.vix_hist) < 50:
return False
vals = list(self.vix_hist)[-50:]
return vals[-1] < sum(vals) / 50.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 Rebalance(self):
if self.IsWarmingUp:
return
# Compute all 8 atoms
rsi5 = self._qqq_rsi5()
score = 0
score += int(self._qqq_leading_spy()) # Atom 1
score += int(self._qqq_beat_spy_42d()) # Atom 2
score += int(self._smh_mom63d_pos()) # Atom 3
score += int(self._qqq_sma200_slope10()) # Atom 4
score += int(rsi5 < 75) # Atom 5: QQQ_RSI5<75
score += int(self._vix_below_sma50()) # Atom 6
score += int(self._vix_falling()) # Atom 7
score += int(rsi5 < 70) # Atom 8: QQQ_RSI5<70
in_market = score >= self.K_THRESHOLD
# TIERED REGIME:
# score >= 6: BULLISH → 100% TQQQ
# score 4-5: MILD OUT → 100% GLD (no inverse — avoids whipsaw)
# score <= 3: STRONG BEAR → 20% SQQQ + 80% GLD (confirmed downturn)
if in_market:
# BULLISH: 100% TQQQ
self.SetHoldings(self.tqqq, 1.0)
if self.Portfolio[self.sqqq].Invested:
self.Liquidate(self.sqqq)
if self.Portfolio[self.gld].Invested:
self.Liquidate(self.gld)
self.days_in += 1
elif score <= 3:
# STRONG BEAR: only deploy SQQQ when firmly bearish
if self.Portfolio[self.tqqq].Invested:
self.Liquidate(self.tqqq)
self.SetHoldings(self.sqqq, 0.20)
self.SetHoldings(self.gld, 0.80)
self.days_out += 1
else:
# MILD OUT (score 4-5): safe haven only, no inverse
if self.Portfolio[self.tqqq].Invested:
self.Liquidate(self.tqqq)
if self.Portfolio[self.sqqq].Invested:
self.Liquidate(self.sqqq)
self.SetHoldings(self.gld, 1.0)
self.days_out += 1
if in_market != self.in_market:
self.Debug(f"{self.Time.date()} TQQQ {'IN' if in_market else 'OUT'} score={score}/8")
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"TQQQ Final: ${self.Portfolio.TotalPortfolioValue:,.2f} | "
f"TIM={tim:.1f}% ({self.days_in}/{total})")