Overall Statistics
Total Orders
1602
Average Win
0.76%
Average Loss
-0.71%
Compounding Annual Return
76.833%
Drawdown
24.100%
Expectancy
0.560
Start Equity
100000
End Equity
1894985.66
Net Profit
1794.986%
Sharpe Ratio
2.055
Sortino Ratio
2.551
Probabilistic Sharpe Ratio
99.178%
Loss Rate
25%
Win Rate
75%
Profit-Loss Ratio
1.08
Alpha
0.453
Beta
0.488
Annual Standard Deviation
0.237
Annual Variance
0.056
Information Ratio
1.756
Tracking Error
0.238
Treynor Ratio
0.996
Total Fees
$16857.24
Estimated Strategy Capacity
$50000000.00
Lowest Capacity Asset
BIL TT1EBZ21QWKL
Portfolio Turnover
12.87%
Drawdown Recovery
220
# region imports
from AlgorithmImports import *
# endregion


TICKERS = [
    "BIL", "UVXY", "TQQQ", "TECL", "UPRO", "SQQQ", "NVDL",
    "SPY", "SPXL", "XLK", "KMLM", "TLT", "NVDA",
]

BLOCKS = [
    (0.9, 0.1), (0.8, 0.2), (0.7, 0.3), (0.6, 0.4), (0.5, 0.5),
    (0.4, 0.6), (0.3, 0.7), (0.2, 0.8), (0.1, 0.9),
]




class TQQQFTLTQC(QCAlgorithm):
    """TQQQ FTLT Yolo - top 14 RSI Sort."""

    def initialize(self):
        self.set_start_date(2021, 1, 1)
        self.set_cash(100000)
        self.symbols = {}
        self._rsi = {}
        self._ma20 = {}
        self._ma200 = {}
        self._nvda_prices = RollingWindow[float](6)  # Need 6 prices for 5 daily returns
        
        for t in TICKERS:
            sym = self.add_equity(t, Resolution.DAILY).symbol
            self.symbols[t] = sym
            self._rsi[t] = {
                2: RelativeStrengthIndex(2, MovingAverageType.WILDERS),
                5: RelativeStrengthIndex(5, MovingAverageType.WILDERS),
                10: RelativeStrengthIndex(10, MovingAverageType.WILDERS),
                14: RelativeStrengthIndex(14, MovingAverageType.WILDERS),
            }
            self._ma20[t] = SimpleMovingAverage(20)
            self._ma200[t] = SimpleMovingAverage(200)
        
        self.set_warm_up(200, Resolution.DAILY)



    def _get_ti(self, ticker: str) -> dict:
        """Get current TI values. Returns dict with rsi_2, rsi_5, rsi_10, rsi_14, ma_20, ma_200, close, stdev5."""
        out = {}
        sym = self.symbols[ticker]
        close = self.securities[sym].price
        out["close"] = close
        out["rsi_2"] = self._rsi[ticker][2].current.value
        out["rsi_5"] = self._rsi[ticker][5].current.value
        out["rsi_10"] = self._rsi[ticker][10].current.value
        out["rsi_14"] = self._rsi[ticker][14].current.value
        out["ma_20"] = self._ma20[ticker].current.value
        out["ma_200"] = self._ma200[ticker].current.value
        if ticker == "NVDA":
            if self._nvda_prices.is_ready:
                # Calculate 5 most recent daily returns (close-to-close)
                returns = []
                for i in range(5):
                    if i + 1 < self._nvda_prices.count:
                        if self._nvda_prices[i] > 0 and self._nvda_prices[i+1] > 0:
                            ret = (self._nvda_prices[i] - self._nvda_prices[i+1]) / self._nvda_prices[i+1]
                            returns.append(ret)
                # Standard deviation of daily returns as percentage
                out["stdev5"] = np.std(returns) * 100 if len(returns) == 5 else 0.0
            else:
                out["stdev5"] = 0.0
        else:
            out["stdev5"] = 0.0
        return out

    def _bil_replace(self) -> str:
        ti = self._get_ti("NVDA")
        if not self._nvda_prices.is_ready:
            return "BIL"
        return "NVDL" if ti["stdev5"] > ti["rsi_2"] else "BIL"

    def _tqqq_ftlt_reddit(self) -> str:
        spy = self._get_ti("SPY")
        if not self._ma200["SPY"].is_ready:
            return "BIL"
        spy_above_ma200 = spy["close"] > spy["ma_200"]
        if spy_above_ma200:
            tqqq = self._get_ti("TQQQ")
            if tqqq["rsi_10"] > 79:
                return "UVXY"
            spxl = self._get_ti("SPXL")
            if spxl["rsi_10"] > 80:
                return "UVXY"
            xlk = self._get_ti("XLK")
            kmlm = self._get_ti("KMLM")
            if xlk["rsi_10"] > kmlm["rsi_10"]:
                return "TQQQ"
            return "BIL"
        tqqq = self._get_ti("TQQQ")
        if tqqq["rsi_10"] < 31:
            return "TECL"
        if spy["rsi_10"] < 30:
            return "UPRO"
        if self._ma20["TQQQ"].is_ready and tqqq["close"] < tqqq["ma_20"]:
            sqqq = self._get_ti("SQQQ")
            tlt = self._get_ti("TLT")
            xlk = self._get_ti("XLK")
            kmlm = self._get_ti("KMLM")
            if sqqq["rsi_10"] > tlt["rsi_10"]:
                if xlk["rsi_10"] > kmlm["rsi_10"]:
                    return "BIL"
                return "SQQQ"
            return "BIL"
        sqqq = self._get_ti("SQQQ")
        if sqqq["rsi_10"] < 31:
            return "SQQQ"
        xlk = self._get_ti("XLK")
        kmlm = self._get_ti("KMLM")
        if xlk["rsi_10"] > kmlm["rsi_10"]:
            if kmlm["close"] < kmlm["ma_20"]:
                return "TQQQ"
            return "BIL"
        return "BIL"

    def _decide_weights(self) -> dict:
        merged = {}
        for w_bil, w_ftlt in BLOCKS:
            t_bil = self._bil_replace()
            t_ftlt = self._tqqq_ftlt_reddit()
            bw = 1.0 / len(BLOCKS)
            merged[t_bil] = merged.get(t_bil, 0.0) + bw * w_bil
            merged[t_ftlt] = merged.get(t_ftlt, 0.0) + bw * w_ftlt
        candidates = [(t, merged[t]) for t in merged if merged[t] > 0]
        if not candidates:
            return {"BIL": 1.0}
        top2 = sorted(candidates, key=lambda x: -self._get_ti(x[0])["rsi_14"])[:2]
        out = {t: 0.0 for t in TICKERS}
        for t, _ in top2:
            out[t] = 0.5
        return out

    def on_data(self, slice):
        # Update indicators with daily bar close (during warmup and live)
        for t in TICKERS:
            sym = self.symbols[t]
            if sym not in slice.bars:
                continue
            bar = slice.bars[sym]
            close = bar.close
            self._rsi[t][2].update(bar.end_time, close)
            self._rsi[t][5].update(bar.end_time, close)
            self._rsi[t][10].update(bar.end_time, close)
            self._rsi[t][14].update(bar.end_time, close)
            self._ma20[t].update(bar.end_time, close)
            self._ma200[t].update(bar.end_time, close)
            if t == "NVDA":
                self._nvda_prices.add(close)
        
        if self.is_warming_up:
            return
        
        if not self._rsi["SPY"][14].is_ready or not self._ma200["SPY"].is_ready:
            return

        weights = self._decide_weights()
        
        total = sum(weights.values())
        if total <= 0:
            return
        
        for t in TICKERS:
            sym = self.symbols[t]
            w = weights.get(t, 0) / total if total > 0 else 0.0
            self.set_holdings(sym, float(w))