Overall Statistics
Total Orders
5863
Average Win
0.47%
Average Loss
-0.29%
Compounding Annual Return
156.432%
Drawdown
26.000%
Expectancy
0.581
Start Equity
100000
End Equity
11116745.12
Net Profit
11016.745%
Sharpe Ratio
2.879
Sortino Ratio
3.746
Probabilistic Sharpe Ratio
99.852%
Loss Rate
40%
Win Rate
60%
Profit-Loss Ratio
1.62
Alpha
0.915
Beta
1.135
Annual Standard Deviation
0.346
Annual Variance
0.12
Information Ratio
3.015
Tracking Error
0.307
Treynor Ratio
0.877
Total Fees
$89091.89
Estimated Strategy Capacity
$22000000.00
Lowest Capacity Asset
TYP U8JOSZGR4OKL
Portfolio Turnover
17.28%
Drawdown Recovery
103
"""
Deploy Book 75/25 — Crash-Guarded LETF Rotation + Sector-Neutral Momentum
==========================================================================

Single-algorithm deployment of a 2-sleeve long-only US equity book:

  * 75% capital: crash-guarded 4-way leveraged-ETF rotation ensemble
                 (T11 + T10 + S3 + S2, each 25% of the 75% bucket,
                  + a QQQ velocity crash guard that de-levers to 50%
                  gross when QQQ trailing 10-day return drops below -10%)
                 — this is the "Multi-Model Tactical ETF Rotation 504
                 with Velocity Crash Guard" engine

  * 25% capital: sector-neutral large-cap trend momentum
                 (monthly rebalance, top-10 by multi-horizon momentum,
                  EMA-189 trend filter, ADX-35 cap, Fibonacci-band
                  ceiling sizing, breadth-based risk-off with 180-day
                  hard timeout)
                 — this is the "Sector-Neutral Large-Cap Trend Momentum
                 (Fixed)" engine with FIX 1-6 applied

Combined weights are aggregated daily and executed via MarketOnOpenOrder
for clean next-open fills (no look-ahead). Same code as the two standalone
publications; this file is the unified deploy artifact.

Account type: cash (IBKR), T+1 settlement modelled correctly.
Resolution: Daily.

Verified deploy numbers (friction-realistic, IBKR cash, daily-rebalanced
weighted blend of per-sleeve equity curves):
  IS 2010-2020 : 29% CAGR / 34% DD / Sharpe 1.00
  OOS 2021-2026: 70% CAGR / 20% DD / Sharpe 1.66 (Sharpe 2.30 on margin)
  Walk-forward : 7/8 two-year windows positive, median 49% CAGR
"""

from AlgorithmImports import *
from collections import defaultdict, deque
import numpy as np


# ============================================================================
# 476 universe selector (sector-neutral top-by-market-cap large caps)
# ============================================================================
class SectorTopUniverse(FundamentalUniverseSelectionModel):
    def __init__(self, algo, blacklist=None):
        self.algo = algo
        self.blacklist = set(blacklist or [])
        super().__init__(self._select)

    def _select(self, fundamentals):
        buckets = defaultdict(list)
        for f in fundamentals:
            if not f.has_fundamental_data:
                continue
            if f.symbol.Value in self.blacklist:
                continue
            if f.company_reference.primary_exchange_id not in ("NYS", "NAS", "ASE"):
                continue
            if f.price is None or f.price <= 5:
                continue
            if f.market_cap is None or f.market_cap < 5_000_000_000:
                continue
            sector = f.asset_classification.morningstar_sector_code
            if sector is None:
                continue
            buckets[sector].append(f)

        symbols = []
        for _, stocks in buckets.items():
            stocks.sort(key=lambda x: x.market_cap, reverse=True)
            symbols.extend(s.symbol for s in stocks[:100])
        return symbols


# ============================================================================
# Unified deploy book
# ============================================================================
class DeployBook_75_25(QCAlgorithm):

    # -------- Capital split between sleeves --------
    W_504CG = 0.75
    W_476   = 0.25

    # -------- 504cg constants (QuadEnsemble) --------
    QUARTER    = 0.25            # each sub-sleeve gets 25% of the 504cg bucket
    SVIX_LIVE  = datetime(2022, 3, 30)
    UVIX_LIVE  = datetime(2022, 3, 30)
    _T10_LATE  = {"KMLM", "LABU", "QQQE", "VOOG", "VOOV"}
    _T11_LATE  = {"KMLM"}
    CRASH_ENTRY    = -0.10       # QQQ 10-day return -> de-lever
    CRASH_EXIT     = -0.04       # recover above -> resume
    CRASH_LOOKBACK = 10
    CRASH_GROSS    = 0.50        # 50% deployed when in crash (rest cash)

    # -------- 476 constants --------
    LOOKBACKS = [21, 63, 126, 189, 252]
    STOCK_COUNT = 10
    MAX_WEIGHT_476 = 0.20
    BAND_LEN = 189
    HIST_LEN = 126
    ADX_LIMIT = 35
    ADX_PERIOD = 14
    BOTTOM_LEVELS = {0, 1, 2, 3, 4}
    RISK_OFF_THRESHOLD = 0.45

    def Initialize(self):
        self.SetStartDate(2021, 1, 1)
        self.SetEndDate(2026, 1, 1)
        self.SetCash(100_000)
        self.SetBrokerageModel(BrokerageName.INTERACTIVE_BROKERS_BROKERAGE, AccountType.MARGIN)
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0.0
        self.SetBenchmark("SPY")

        res = Resolution.Daily

        # -------- 504cg manual universe --------
        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.AddEquity(t, res).Symbol for t in unique}
        self._504cg_universe = set(self._syms.values())

        # -------- 504cg 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",
        ]}
        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)
        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")
        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")

        # -------- 504cg state --------
        self._qqq_window = RollingWindow[float](self.CRASH_LOOKBACK + 1)
        self._in_crash = False
        self._504cg_targets = {}    # current 504cg target weight dict (within 504cg's 75% bucket)
        self._504cg_last_label = ""

        # -------- 476 universe + state --------
        self.UniverseSettings.Resolution = Resolution.Daily
        self.UniverseSettings.DataNormalizationMode = DataNormalizationMode.TOTAL_RETURN
        self.SetUniverseSelection(SectorTopUniverse(self, blacklist={"GME", "AMC"}))
        self._476_symbols = set()
        self._476_targets = {}      # current 476 target weight dict (within 476's 25% bucket)

        # 476 per-symbol state
        self.ma = {}
        self.adx = {}
        self.close_win = {}
        self.stretch_ema = {}
        self.band_hist = {}
        self.stretch_win = {}

        # 476 breadth state
        self.current_band_idx = {}
        self.allow_universe = True
        self.max_stress_level = 0.0
        self.was_risk_off = False
        self.risk_off_date = None

        # Combined execution
        self._pending_weights = None

        self.SetWarmUp(300)

        # 476 monthly rebalance
        self.Schedule.On(
            self.DateRules.MonthEnd("SPY"),
            self.TimeRules.BeforeMarketClose("SPY", 5),
            self.Rebalance_476
        )

    # ========================================================================
    # OnSecuritiesChanged — route 504cg ETFs vs 476 stocks
    # ========================================================================
    def OnSecuritiesChanged(self, changes):
        for sec in changes.AddedSecurities:
            sec.SetFeeModel(InteractiveBrokersFeeModel())
            sec.SetSlippageModel(ConstantSlippageModel(0.001))
            s = sec.Symbol

            # 504cg ETFs are pre-added in Initialize — already handled
            if s in self._504cg_universe:
                continue

            # New 476 universe stock — set up its indicators
            self._476_symbols.add(s)
            self.ma[s]          = self.EMA(s, self.BAND_LEN, Resolution.Daily)
            self.adx[s]         = self.ADX(s, self.ADX_PERIOD, Resolution.Daily)
            self.stretch_ema[s] = self.EMA(s, self.BAND_LEN, Resolution.Daily)
            self.close_win[s]   = RollingWindow[float](self.BAND_LEN)
            self.band_hist[s]   = RollingWindow[int](self.HIST_LEN)
            self.stretch_win[s] = RollingWindow[float](self.HIST_LEN)

        for sec in changes.RemovedSecurities:
            s = sec.Symbol
            if s in self._504cg_universe:
                continue
            self._476_symbols.discard(s)
            self.ma.pop(s, None)
            self.adx.pop(s, None)
            self.stretch_ema.pop(s, None)
            self.close_win.pop(s, None)
            self.band_hist.pop(s, None)
            self.current_band_idx.pop(s, None)
            self.stretch_win.pop(s, None)

    # ========================================================================
    # 504cg sleeve readiness checks
    # ========================================================================
    @property
    def _t10_ready(self):
        core = [v for k, v in self._t10_rsi.items() if k not in self._T10_LATE]
        return not self.IsWarmingUp and all(r.IsReady for r in core)

    @property
    def _t11_ready(self):
        core10 = [v for k, v in self._t11_rsi10.items() if k not in self._T11_LATE]
        return (not self.IsWarmingUp
                and self._t11_spy_sma200.IsReady
                and self._t11_tqqq_sma20.IsReady
                and all(r.IsReady for r in core10)
                and all(r.IsReady for r in self._t11_rsi20.values())
                and self._t11_rsi60_sh.IsReady)

    @property
    def _s2_ready(self):
        return (not self.IsWarmingUp
                and self._s2_tqqq_sma200.IsReady
                and self._s2_tqqq_sma20.IsReady
                and self._s2_tqqq_rsi10.IsReady
                and self._s2_soxl_rsi10.IsReady
                and self._s2_sqqq_rsi10.IsReady
                and self._s2_bsv_rsi10.IsReady)

    @property
    def _s3_ready(self):
        return (not self.IsWarmingUp
                and self._s3_spy_sma202.IsReady
                and self._s3_qqq_sma202.IsReady
                and self._s3_smh_sma202.IsReady
                and self._s3_soxl_sma202.IsReady
                and self._s3_rsi_qqq8.IsReady
                and self._s3_rsi_smh8.IsReady
                and self._s3_rsi_spy15.IsReady
                and self._s3_rsi_qqq15.IsReady
                and self._s3_rsi_smh15.IsReady
                and self._s3_rsi_soxl15.IsReady)

    # ========================================================================
    # 504cg T11 helpers
    # ========================================================================
    def _t11_bond_baller(self, r10, r20, tqqq_px, tqqq_sma):
        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):
        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"

    # ========================================================================
    # 504cg sleeve weight methods (each returns dict summing to QUARTER=0.25
    # of the 504cg bucket)
    # ========================================================================
    def _t10_weights(self):
        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].IsReady}
        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"].IsReady 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"].IsReady
        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):
        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].IsReady}
        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"].IsReady and self._t11_kmlm_sma20.IsReady
            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 = {}
            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):
        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):
        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"]].HasData
                           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 {}

    # ========================================================================
    # 504cg daily rebalance (called from OnData)
    # ========================================================================
    def _504cg_rebalance(self):
        # Crash guard state machine
        if self._qqq_window.IsReady:
            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 10d={qqq_ret*100:.1f}% de-lever to {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 10d={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}"

        if new_label == self._504cg_last_label:
            return False  # no change

        combined = {}
        for w in [w10, w11, w2, w3]:
            for sym, wt in w.items():
                combined[sym] = combined.get(sym, 0.0) + wt * crash_gross
        self._504cg_targets = combined
        self._504cg_last_label = new_label
        return True  # changed

    # ========================================================================
    # 476 monthly rebalance (scheduled)
    # ========================================================================
    def Rebalance_476(self):
        if self.IsWarmingUp:
            return

        idxs = list(self.current_band_idx.values())
        if len(idxs) < 50:
            return

        bottom_frac = sum(i in self.BOTTOM_LEVELS for i in idxs) / len(idxs)
        self.max_stress_level = max(self.max_stress_level, bottom_frac)

        if bottom_frac >= self.RISK_OFF_THRESHOLD:
            if not self.was_risk_off:
                self.risk_off_date = self.Time
            self.allow_universe = False
            self.was_risk_off = True
        elif self.was_risk_off:
            denominator = max(self.max_stress_level, 0.10)
            improvement = (self.max_stress_level - bottom_frac) / denominator
            days_risk_off = (self.Time - self.risk_off_date).days if self.risk_off_date else 0
            if improvement >= 0.60 or bottom_frac < 0.15 or days_risk_off > 180:
                for s in self._476_symbols:
                    if s in self.band_hist:
                        self.band_hist[s] = RollingWindow[int](self.HIST_LEN)
                self.allow_universe = True
                self.was_risk_off = False
                self.max_stress_level = 0.0
                self.risk_off_date = None
        else:
            self.allow_universe = True

        if not self.allow_universe:
            self._476_targets = {}
            self._update_combined()
            return

        hist = self.History(list(self._476_symbols), max(self.LOOKBACKS) + 1, Resolution.Daily)
        if hist.empty:
            return

        closes = hist["close"].unstack(0)
        momentum = {}

        for s in self._476_symbols:
            if s not in closes:
                continue
            px = closes[s]
            if len(px) < max(self.LOOKBACKS) + 1:
                continue
            if not self.adx[s].IsReady or self.adx[s].Current.Value > self.ADX_LIMIT:
                continue
            mom = np.mean([px.iloc[-1] / px.iloc[-lb - 1] - 1 for lb in self.LOOKBACKS])
            if not self.ma[s].IsReady:
                continue
            price = self.Securities[s].Price
            ema = self.ma[s].Current.Value
            if price <= ema:
                continue
            fundamentals = self.Securities[s].Fundamentals
            if fundamentals is None or fundamentals.MarketCap < 5_000_000_000:
                continue
            if mom > 0:
                momentum[s] = mom

        if not momentum:
            self._476_targets = {}
            self._update_combined()
            return

        top = sorted(momentum, key=momentum.get, reverse=True)[:self.STOCK_COUNT]

        scaled = {}
        for s in top:
            if not self.ma[s].IsReady or not self.stretch_ema[s].IsReady:
                continue
            dev = np.std(list(self.close_win[s]))
            if dev <= 0:
                continue
            mid = self.ma[s].Current.Value
            lm  = self.stretch_ema[s].Current.Value
            lm2 = lm / 2.0
            lm3 = lm2 * 0.38196601
            lm4 = lm * 1.38196601
            lm5 = lm * 1.61803399
            lm6 = (lm + lm2) / 2.0
            bands = [
                mid - dev * lm5, mid - dev * lm4, mid - dev * lm,
                mid - dev * lm6, mid - dev * lm2, mid - dev * lm3,
                mid,
                mid + dev * lm3, mid + dev * lm2, mid + dev * lm6,
                mid + dev * lm, mid + dev * lm4, mid + dev * lm5
            ]
            price = self.Securities[s].Price
            idx = self._band_index(price, bands)
            self.band_hist[s].Add(idx)
            hist_idx = list(self.band_hist[s])
            historical_high = max(hist_idx) if hist_idx else idx
            if historical_high <= 0:
                scale = 1.0
            elif idx >= historical_high:
                scale = 0.0
            else:
                scale = max(0.2, 1.0 - idx / historical_high)
            if self.stretch_win[s].IsReady:
                stretch_list = list(self.stretch_win[s])
                current_stretch = stretch_list[0]
                peak_stretch = max(stretch_list)
                if idx >= 10 and peak_stretch > 0:
                    if current_stretch < (peak_stretch * 0.80):
                        scale = min(scale, 0.2)
            scaled[s] = momentum[s] * scale

        if not scaled:
            self._476_targets = {}
            self._update_combined()
            return

        total_scaled = sum(scaled.values())
        raw_weights = {s: v / total_scaled for s, v in scaled.items()}
        capped_weights = {s: min(self.MAX_WEIGHT_476, w) for s, w in raw_weights.items()}
        current_sum = sum(capped_weights.values())
        if current_sum > 0:
            self._476_targets = {s: w / current_sum for s, w in capped_weights.items()}
        else:
            self._476_targets = {}
        self._update_combined()

    def _band_index(self, price, bands):
        for i in range(len(bands) - 1):
            if bands[i] <= price < bands[i + 1]:
                return i
        return len(bands) - 2

    # ========================================================================
    # Combined weights aggregator + execution
    # ========================================================================
    def _update_combined(self):
        # 504cg targets already include crash_gross scaling and sum to ~1.0
        # 476 targets sum to 1.0 (or 0.0 if risk-off)
        all_syms = set(self._504cg_targets) | set(self._476_targets)
        combined = {}
        for s in all_syms:
            w = (self.W_504CG * self._504cg_targets.get(s, 0.0)
                 + self.W_476   * self._476_targets.get(s, 0.0))
            if w > 0:
                combined[s] = w
        self._pending_weights = combined

    def OnData(self, data):
        # Feed 504cg crash-guard QQQ window (also during warmup)
        qqq_sym = self._syms["QQQ"]
        if data.Bars.ContainsKey(qqq_sym):
            self._qqq_window.Add(data.Bars[qqq_sym].Close)

        if self.IsWarmingUp:
            return

        # 504cg daily signal update
        if self._504cg_rebalance():
            self._update_combined()

        # 476 breadth-band tracking (per-day per-stock band index)
        for s in list(self._476_symbols):
            if not data.ContainsKey(s):
                continue
            bar = data[s]
            if bar is None:
                continue
            close = bar.Close
            self.close_win[s].Add(close)
            if not self.close_win[s].IsReady or not self.ma[s].IsReady:
                continue
            dev = np.std(list(self.close_win[s]))
            if dev <= 0:
                continue
            mid = self.ma[s].Current.Value
            stretch = abs(close - mid) / dev
            self.stretch_ema[s].Update(self.Time, stretch)
            self.stretch_win[s].Add(stretch)
            bands = [
                mid - dev * 1.618, mid - dev * 1.382, mid - dev,
                mid - dev * 0.809, mid - dev * 0.5, mid - dev * 0.382,
                mid,
                mid + dev * 0.382, mid + dev * 0.5, mid + dev * 0.809,
                mid + dev, mid + dev * 1.382, mid + dev * 1.618
            ]
            self.current_band_idx[s] = self._band_index(close, bands)

        # Drain pending combined weights via MarketOnOpenOrder
        if self._pending_weights is not None:
            targets = self._pending_weights
            self._pending_weights = None
            equity = self.Portfolio.TotalPortfolioValue
            # Liquidate anything not in target set
            for pos in list(self.Portfolio.Values):
                if pos.Invested and pos.Symbol not in targets:
                    if pos.Quantity != 0:
                        self.MarketOnOpenOrder(pos.Symbol, -pos.Quantity)
            # Set target positions
            for sym, w in targets.items():
                if w <= 0 or not self.Securities.ContainsKey(sym):
                    continue
                price = self.Securities[sym].Price
                if price <= 0:
                    continue
                target_qty = int(equity * w / price)
                cur = self.Portfolio[sym].Quantity if self.Portfolio.ContainsKey(sym) else 0
                delta = target_qty - cur
                if delta != 0:
                    self.MarketOnOpenOrder(sym, delta)