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})")