Overall Statistics
Total Orders
995
Average Win
1.07%
Average Loss
-0.86%
Compounding Annual Return
35.461%
Drawdown
19.900%
Expectancy
0.390
Start Equity
100000
End Equity
521006.60
Net Profit
421.007%
Sharpe Ratio
1.033
Sortino Ratio
1.293
Probabilistic Sharpe Ratio
58.321%
Loss Rate
38%
Win Rate
62%
Profit-Loss Ratio
1.25
Alpha
0.157
Beta
0.698
Annual Standard Deviation
0.217
Annual Variance
0.047
Information Ratio
0.7
Tracking Error
0.183
Treynor Ratio
0.321
Total Fees
$5243.61
Estimated Strategy Capacity
$120000000.00
Lowest Capacity Asset
BSV TRO5ZARLX6JP
Portfolio Turnover
17.74%
Drawdown Recovery
283
from AlgorithmImports import *
import numpy as np
import pandas as pd

# =============================================================================
# kinfo ENSEMBLE — 0379 (defense) + 0273 (offense), one daily algorithm
# Applies kinfo's #1 lesson (uncorrelated legs beat singles) to the two engines
# that VERIFIED on QC:
#   * 0379 symphony : DD-controlled multi-asset rotation (TLT/GLD/SHV + crash gate)
#                     -> OOS 32.6%/21.8%DD/Sh0.94, full 20.4/29.3/0.77
#   * 0273          : aggressive leveraged-Nasdaq regime rotator (QQQ/TQQQ/SQQQ,
#                     5-cond bear gate) -> kinfo full 33.6%/42.5%/Sh1.05
# Each sleeve is gross<=100%; blended at W_0379 / W_0273 (sum=1) so the combined
# book never borrows -> cash-faithful. QC-default margin (faithful proxy; the
# cash-mode MOO rotation artifact is avoided, see champ_lev_regime notes).
#
# IS = 2010-2020 | OOS = 2021-2025 | default full. Tune W_0379 / W_0273 by results.
# =============================================================================

class KinfoEnsemble(QCAlgorithm):

    # blend weights (sum to 1.0) — 70/30 chosen: best DD (25.2% full) + Sharpe 1.32/1.63, monotone in-sample
    W_0379 = 0.70
    W_0273 = 0.30

    USE_CASH = False   # False = margin proxy (gross<=100%, no borrowing). True = IBKR cash + 10bps + sells-before-buys
    REBAL_TOL = 0.08   # rebalance only when held names change or a weight moves >= this (cuts turnover/fees)

    T_0379 = ["QQQ","SPY","TLT","BSV","GLD","QLD","PSQ","SHV","IEF","QID","SMH","USD"]
    T_0273 = ["TQQQ","SQQQ"]              # QQQ shared with 0379 set; ONEQ + VIX added below
    HISTORY_BARS = 1500                   # 0273 needs ~6y for bear-streak; 0379 uses last 300

    # 0273 params
    STREAK_MIN = 40
    DIST_THRESHOLD = -0.08
    BOUNCE_ENTRY = 0.045
    MAX_SQQQ = 0.90

    def initialize(self):
        self.set_start_date(2021, 1, 1)
        self.set_end_date(2026, 6, 9)
        self.set_cash(100000)
        if self.USE_CASH:
            self.set_brokerage_model(BrokerageName.INTERACTIVE_BROKERS_BROKERAGE, AccountType.CASH)
            self.settings.minimum_order_margin_portfolio_percentage = 0.0

        self.syms = {}
        for t in self.T_0379 + self.T_0273:
            if t in self.syms:
                continue
            eq = self.add_equity(t, Resolution.DAILY)
            if self.USE_CASH:
                eq.set_fee_model(InteractiveBrokersFeeModel())
                eq.set_slippage_model(ConstantSlippageModel(0.001))
                eq.set_settlement_model(ImmediateSettlementModel())
            self.syms[t] = eq.symbol
        self.ixic = self.add_equity("ONEQ", Resolution.DAILY).symbol   # 0273 IXIC proxy
        try:
            self.vix = self.add_data(CBOE, "VIX", Resolution.DAILY).symbol
            self._has_vix = True
        except Exception:
            self.vix = None
            self._has_vix = False

        self.set_benchmark("QQQ")
        self.set_warm_up(self.HISTORY_BARS, Resolution.DAILY)
        self._last_key = None
        self._pending = None   # FILL-FIX: queued target weights, drained at next open
        self.schedule.on(self.date_rules.every_day("QQQ"),
                         self.time_rules.before_market_close("QQQ", 10), self.rebalance)

    # ---------- shared helpers ----------
    @staticmethod
    def _rsi_wilder(px, window):
        delta = px.diff()
        gain = delta.clip(lower=0.0)
        loss = (-delta).clip(lower=0.0)
        ag = gain.ewm(alpha=1.0/window, adjust=False).mean()
        al = loss.ewm(alpha=1.0/window, adjust=False).mean()
        rs = ag / al.replace(0.0, np.nan)
        return (100.0 - 100.0/(1.0+rs)).fillna(50.0)

    @staticmethod
    def _rsi_sma_seed(series, period):
        delta = series.diff()
        gain = delta.clip(lower=0.0).to_numpy()
        loss = (-delta.clip(upper=0.0)).to_numpy()
        n = len(series); ag = np.full(n, np.nan); al = np.full(n, np.nan)
        if n > period:
            ag[period] = np.nanmean(gain[1:period+1]); al[period] = np.nanmean(loss[1:period+1])
            ip = 1.0/period
            for i in range(period+1, n):
                ag[i] = ag[i-1]*(1-ip) + gain[i]*ip
                al[i] = al[i-1]*(1-ip) + loss[i]*ip
        rs = np.where(al > 0, ag/np.where(al == 0, np.nan, al), np.nan)
        return pd.Series(100.0 - 100.0/(1.0+rs), index=series.index).fillna(50.0)

    @staticmethod
    def _top(tickers, rsi10):
        best = tickers[0]; bv = rsi10[best]
        for t in tickers[1:]:
            if rsi10[t] > bv: best, bv = t, rsi10[t]
        return best

    # ---------- 0379 sleeve ----------
    def _decide_0379(self, close):
        rsi_assets = ["QQQ","SPY","TLT","BSV","GLD","PSQ","IEF","QLD","USD","SMH"]
        rsi10 = {t: float(self._rsi_sma_seed(close[t], 10).iloc[-1]) for t in rsi_assets}
        rsi60_spy = float(self._rsi_sma_seed(close["SPY"], 60).iloc[-1])
        spy = close["SPY"]; qqq = close["QQQ"]
        spy_ma200 = float(spy.rolling(200).mean().iloc[-1])
        spy_ma20  = float(spy.rolling(20).mean().iloc[-1])
        qqq_ma20  = float(qqq.rolling(20).mean().iloc[-1])
        spy_l = float(spy.iloc[-1]); qqq_l = float(qqq.iloc[-1])
        cr10  = float((qqq.iloc[-1]/qqq.iloc[-11]-1.0)*100.0)
        cr60  = float((qqq.iloc[-1]/qqq.iloc[-61]-1.0)*100.0)
        cr252 = float((qqq.iloc[-1]/qqq.iloc[-253]-1.0)*100.0)
        w = {}
        def add(t, x): w[t] = w.get(t, 0.0) + x
        if spy_l > spy_ma200:
            if rsi10["QQQ"] > 80.0: add("TLT", 1.0)
            elif rsi10["SPY"] > 80.0: add("TLT", 1.0)
            elif rsi60_spy > 60.0: add(self._top(["TLT","BSV","GLD"], rsi10), 1.0)
            else: add("QQQ", 1.0)
        else:
            if rsi10["QQQ"] < 30.0:
                add("QLD", 1.0)
            else:
                tlt_gt_psq = rsi10["TLT"] > rsi10["PSQ"]
                qqq_below = qqq_l < qqq_ma20
                if qqq_l > qqq_ma20:
                    if rsi10["PSQ"] < 30.0: add("PSQ", 0.50)
                    elif cr10 > 5.5: add("SHV", 0.50)
                    else: add("QQQ", 0.50)
                else:
                    add(self._top(["IEF","PSQ"], rsi10), 0.50)
                if cr252 < -20.0:
                    if qqq_below:
                        if cr60 <= -12.0:
                            sib1 = "SPY" if spy_l > spy_ma20 else ("QQQ" if tlt_gt_psq else "PSQ")
                            sib2 = "QQQ" if tlt_gt_psq else "PSQ"
                            add(sib1, 0.25); add(sib2, 0.25)
                        else:
                            add("QLD" if tlt_gt_psq else "QID", 0.50)
                    else:
                        if rsi10["PSQ"] < 31.0: add("PSQ", 0.50)
                        elif cr10 > 5.5: add("PSQ", 0.50)
                        else: add(self._top(["QQQ","SMH"], rsi10), 0.50)
                else:
                    if qqq_below:
                        add("QLD" if tlt_gt_psq else "QID", 0.50)
                    else:
                        if rsi10["PSQ"] < 31.0: add("QID", 0.50)
                        elif cr10 > 5.5: add("QID", 0.50)
                        else: add(self._top(["QLD","USD"], rsi10), 0.50)
        return w

    # ---------- 0273 sleeve ----------
    def _decide_0273(self, df, vix_series):
        df = df.copy()
        df["qqq_sma_50"] = df["QQQ"].rolling(50).mean()
        df["qqq_sma_200"] = df["QQQ"].rolling(200).mean()
        df["qqq_dist_sma_50"] = (df["QQQ"] - df["qqq_sma_50"]) / df["qqq_sma_50"]
        df["qqq_dist_sma_200"] = (df["QQQ"] - df["qqq_sma_200"]) / df["qqq_sma_200"]
        df["qqq_sma_200_slope_20"] = (df["qqq_sma_200"] - df["qqq_sma_200"].shift(20)) / df["qqq_sma_200"].shift(20)
        df["qqq_roc_21"] = df["QQQ"].pct_change(21)
        df["ixic_sma_50"] = df["IXIC"].rolling(50).mean()
        df["ixic_sma_100"] = df["IXIC"].rolling(100).mean()
        df["ixic_sma_175"] = df["IXIC"].rolling(175).mean()
        df["ixic_sma_200"] = df["IXIC"].rolling(200).mean()
        df["ixic_sma_200_dist"] = (df["IXIC"] - df["ixic_sma_200"]) / df["ixic_sma_200"]
        df["ixic_above_sma_50"] = (df["IXIC"] > df["ixic_sma_50"]).astype(float)
        df["ixic_above_sma_100"] = (df["IXIC"] > df["ixic_sma_100"]).astype(float)
        df["ixic_above_sma_175"] = (df["IXIC"] > df["ixic_sma_175"]).astype(float)
        df["qqq_rsi_14"] = self._rsi_wilder(df["QQQ"], 14)
        df["tqqq_vol20"] = df["TQQQ"].pct_change().rolling(20).std() * np.sqrt(252)
        if vix_series is not None and not vix_series.empty:
            v = vix_series.reindex(df.index).ffill()
            df["vix_ratio_20"] = (v / v.rolling(20).mean()).fillna(1.0)
        else:
            df["vix_ratio_20"] = 1.0

        feat = df.shift(1).iloc[-1]
        req = ["qqq_dist_sma_50","qqq_dist_sma_200","qqq_sma_200_slope_20","qqq_roc_21",
               "tqqq_vol20","qqq_rsi_14","ixic_sma_200_dist","ixic_above_sma_50",
               "ixic_above_sma_100","ixic_above_sma_175","vix_ratio_20"]
        if feat[req].isna().any():
            return {}

        above175 = bool(feat["ixic_above_sma_175"] > 0.5)
        above100 = bool(feat["ixic_above_sma_100"] > 0.5)
        above50 = bool(feat["ixic_above_sma_50"] > 0.5)
        rsi14 = float(feat["qqq_rsi_14"]); roc21 = float(feat["qqq_roc_21"])
        dist_200_ixic = float(feat["ixic_sma_200_dist"]); vix_ratio = float(feat["vix_ratio_20"])
        strong_bull = above175 and above50 and (rsi14 > 55.0) and (roc21 > 0.0)
        full_momentum = (rsi14 > 60.0) and (roc21 > 0.0)
        regime = 4
        if above100: regime = 3
        if above175 and (not strong_bull): regime = 2
        if above175 and strong_bull and (not full_momentum): regime = 5
        if above175 and strong_bull and full_momentum: regime = 1
        vix_scale = 1.0
        if vix_ratio >= 2.2: vix_scale = 0.38
        elif vix_ratio >= 1.5: vix_scale = 0.62
        elif vix_ratio >= 1.1: vix_scale = 0.88
        w_qqq, w_tqqq, w_sqqq = 0.0, 0.0, 0.0
        very_extended = (regime == 1) and (dist_200_ixic > 0.15)
        normal_bull = (regime == 1) and (not very_extended)
        if very_extended: w_tqqq = float(np.clip(0.84*vix_scale, 0.35, 0.84))
        elif normal_bull: w_tqqq = float(np.clip(0.90*vix_scale, 0.35, 0.90))
        elif regime == 5: w_tqqq = float(np.clip(0.60*vix_scale, 0.28, 0.60))
        elif regime == 2: w_tqqq = float(np.clip(0.70*vix_scale, 0.28, 0.70))
        elif regime == 4: w_qqq = 0.45
        elif regime == 3:
            w_tqqq = 0.62 if rsi14 > 55 else (0.48 if rsi14 > 45 else 0.34)
        t_vol = float(feat["tqqq_vol20"])
        if above175 and (t_vol < 0.60): w_tqqq = min(0.95, w_tqqq*1.05)
        if above175 and (t_vol >= 0.80): w_tqqq = min(0.95, w_tqqq*0.75)
        if very_extended: w_tqqq = min(0.95, w_tqqq*0.92)

        now = df.iloc[-1]
        dist50 = float(now["qqq_dist_sma_50"]); dist200 = float(now["qqq_dist_sma_200"])
        slope200 = float(now["qqq_sma_200_slope_20"])
        if any(np.isnan([dist50, dist200, slope200])):
            return {}
        below_200 = (df["qqq_dist_sma_200"] < 0).astype(int).values
        streak = 0
        for b in below_200[:-1][::-1]:
            if b == 1: streak += 1
            else: break
        bear_gate = (streak >= self.STREAK_MIN) and (dist200 < dist50) and (dist200 < self.DIST_THRESHOLD) and (slope200 < 0.0)
        roc3 = float(df["QQQ"].pct_change(3).shift(1).iloc[-1])
        if bear_gate and (roc3 >= self.BOUNCE_ENTRY):
            w_qqq, w_tqqq, w_sqqq = 0.0, 0.0, self.MAX_SQQQ
        gross = w_qqq + w_tqqq + w_sqqq
        if gross > 1.0:
            w_qqq /= gross; w_tqqq /= gross; w_sqqq /= gross
        return {"QQQ": w_qqq, "TQQQ": w_tqqq, "SQQQ": w_sqqq}

    # ---------- combined rebalance ----------
    def rebalance(self):
        if self.is_warming_up:
            return
        all_syms = [self.syms[t] for t in self.syms] + [self.ixic]
        hist = self.history(all_syms, self.HISTORY_BARS, Resolution.DAILY)
        if hist is None or hist.empty or "close" not in hist.columns:
            return
        try:
            wide = hist["close"].unstack(level=0)
        except Exception:
            return
        col = {}
        for t in self.syms:
            s = self.syms[t]
            if s in wide.columns: col[t] = wide[s]
        if self.ixic in wide.columns: col["IXIC"] = wide[self.ixic]
        if any(t not in col for t in self.T_0379) or "IXIC" not in col:
            return
        close = pd.DataFrame(col).dropna()
        if len(close) < 260:
            return

        # vix series
        vix_series = None
        if self._has_vix and self.vix is not None:
            try:
                vh = self.history(self.vix, self.HISTORY_BARS, Resolution.DAILY)
                if vh is not None and not vh.empty:
                    s = vh["value"] if "value" in vh.columns else (vh["close"] if "close" in vh.columns else None)
                    if s is not None:
                        if isinstance(s.index, pd.MultiIndex):
                            s.index = s.index.get_level_values(-1)
                        s.index = pd.to_datetime(s.index).tz_localize(None)
                        vix_series = s[~pd.Index(s.index).duplicated(keep="last")].sort_index().astype(float)
            except Exception:
                vix_series = None

        w379 = self._decide_0379(close[self.T_0379])
        df0273 = close[["QQQ","TQQQ","SQQQ","IXIC"]]
        if vix_series is not None:
            df0273.index = close.index
        w273 = self._decide_0273(df0273, vix_series)

        target = {}
        for t, x in w379.items():
            target[t] = target.get(t, 0.0) + self.W_0379 * x
        for t, x in w273.items():
            if x > 0:
                target[t] = target.get(t, 0.0) + self.W_0273 * x

        target = {t: round(w, 3) for t, w in target.items() if w > 0.005}
        # rebalance-on-change with tolerance band (cuts turnover/fees on cash):
        # only trade if the held names change OR some weight moves >= REBAL_TOL.
        if self._last_key is not None:
            same_names = set(target) == set(self._last_key)
            moves = [abs(target.get(t, 0.0) - self._last_key.get(t, 0.0))
                     for t in set(target) | set(self._last_key)]
            if same_names and (max(moves) if moves else 0.0) < self.REBAL_TOL:
                return
        self._last_key = target

        # FILL-FIX (ports the 476/504cg discipline): do NOT execute intraday on Daily
        # resolution. An intraday market order / set_holdings on Daily fills at the
        # PRIOR bar's close (yesterday) -> inflates the backtest vs live. Instead QUEUE
        # the target weights; on_data drains them via MarketOnOpenOrder so fills land at
        # the NEXT session open — a price that only exists AFTER the decision.
        self._pending = target

    def on_data(self, data):
        # FILL-FIX drain: execute the queued rebalance at the next open via MOO.
        if self.is_warming_up or self._pending is None:
            return
        target = self._pending
        self._pending = None
        keep = set(self.syms[t] for t in target)
        # close names no longer targeted
        for kvp in list(self.portfolio):
            sym, h = kvp.key, kvp.value
            if h.invested and sym not in keep and sym != self.ixic and h.quantity != 0:
                self.market_on_open_order(sym, -h.quantity)
        # size off current equity & last price; submit reductions before increases
        equity = self.portfolio.total_portfolio_value
        def cur_w(t):
            h = self.portfolio[self.syms[t]]
            return (float(h.holdings_value) / equity) if equity > 0 else 0.0
        for t, w in sorted(target.items(), key=lambda kv: kv[1] - cur_w(kv[0])):
            sym = self.syms[t]
            price = float(self.securities[sym].price)
            if price <= 0:
                continue
            target_qty = int(equity * w / price)
            cur = self.portfolio[sym].quantity if self.portfolio.contains_key(sym) else 0
            delta = target_qty - cur
            if delta != 0:
                self.market_on_open_order(sym, delta)