Overall Statistics
Total Orders
1611
Average Win
1.08%
Average Loss
-0.85%
Compounding Annual Return
152.742%
Drawdown
24.400%
Expectancy
0.917
Start Equity
1000000
End Equity
259775114.67
Net Profit
25877.511%
Sharpe Ratio
3.149
Sortino Ratio
3.25
Probabilistic Sharpe Ratio
99.993%
Loss Rate
16%
Win Rate
84%
Profit-Loss Ratio
1.28
Alpha
0.915
Beta
0.664
Annual Standard Deviation
0.307
Annual Variance
0.094
Information Ratio
3.061
Tracking Error
0.291
Treynor Ratio
1.453
Total Fees
$0.00
Estimated Strategy Capacity
$0
Lowest Capacity Asset
TQQQ UK280CGTCB51
Portfolio Turnover
13.21%
Drawdown Recovery
536
# region imports
from AlgorithmImports import *
import numpy as np
import decimal
# endregion


class CustomVix(PythonData):
    def get_source(self, config, date, is_live):
        url = f"https://cdn.cboe.com/api/global/us_indices/daily_prices/{config.symbol.value}_History.csv"
        return SubscriptionDataSource(url, SubscriptionTransportMedium.REMOTE_FILE)

    def reader(self, config, line, date, is_live):
        try:
            if not line or not line[0].isdigit():
                return None
            data = line.split(',')
            v = CustomVix()
            v.symbol = config.symbol
            v.time = datetime.strptime(data[0], "%m/%d/%Y")
            v.value = decimal.Decimal(data[4])
            return v
        except:
            return None


class QuantumPortfolioConservativeV14(QCAlgorithm):

    def initialize(self):
        self.set_start_date(2018, 1, 1)
        self.set_end_date(2023, 12, 31)
        self.set_cash(1_000_000)
        self.set_warm_up(504)

        self.set_brokerage_model(
            BrokerageName.CHARLES_SCHWAB,
            AccountType.MARGIN
        )

        # Assets
        self.spxl = self.add_equity("SPXL", Resolution.DAILY).symbol
        self.tqqq = self.add_equity("TQQQ", Resolution.DAILY).symbol

        self.vix = self.add_data(CustomVix, "VIX", Resolution.DAILY).symbol
        self.vix3m = self.add_data(CustomVix, "VIX3M", Resolution.DAILY).symbol
        self.vvix = self.add_data(CustomVix, "VVIX", Resolution.DAILY).symbol

        # Indicators
        self.ema200_spxl = self.ema(self.spxl, 200)
        self.ema200_tqqq = self.ema(self.tqqq, 200)
        self.ema20_spxl = self.ema(self.spxl, 20)
        self.ema20_tqqq = self.ema(self.tqqq, 20)

        # Risk state
        self.peak_equity = self.portfolio.total_portfolio_value
        self.profit_peak = self.portfolio.total_portfolio_value
        self.equity_cap = self.portfolio.total_portfolio_value

        self.cooldown_days = 0
        self.ema_break_count = 0

        self.schedule.on(
            self.date_rules.every_day(self.spxl),
            self.time_rules.after_market_open(self.spxl, 30),
            self.rebalance
        )

    def rebalance(self):
        if self.is_warming_up:
            return

        equity = self.portfolio.total_portfolio_value

        # ==================================================
        # ① 组合级硬回撤兜底
        # ==================================================
        self.peak_equity = max(self.peak_equity, equity)
        drawdown = (self.peak_equity - equity) / self.peak_equity

        if drawdown > 0.20:
            self.liquidate()
            self.cooldown_days = 20
            self.peak_equity = equity
            self.profit_peak = equity
            self.equity_cap = equity
            self.log(f"FORCE EXIT | DD={drawdown:.2%}")
            return

        if self.cooldown_days > 0:
            self.cooldown_days -= 1
            return

        # ==================================================
        # ② EMA200 + VVIX → 趋势死亡 → 全清仓
        # ==================================================
        spxl_price = self.securities[self.spxl].price
        tqqq_price = self.securities[self.tqqq].price
        vvix = self.securities[self.vvix].price

        below_ema = (
            spxl_price < self.ema200_spxl.current.value and
            tqqq_price < self.ema200_tqqq.current.value
        )

        if below_ema and vvix > 100:
            self.ema_break_count += 1
        else:
            self.ema_break_count = 0

        if self.ema_break_count >= 3:
            self.liquidate()
            self.cooldown_days = 20
            self.ema_break_count = 0
            self.log("EMA200 + VVIX CONFIRMED → FULL EXIT")
            return

        # ==================================================
        # ③ VVIX → 杠杆软上限
        # ==================================================
        if vvix > 120:
            max_exposure = 0.5
        elif vvix > 100:
            max_exposure = 1.0
        else:
            max_exposure = 2.0

        # ==================================================
        # ④ VIX Panic → 系统性风险
        # ==================================================
        vix = self.securities[self.vix].price
        vix3m = self.securities[self.vix3m].price
        if vix <= 0 or vix3m <= 0:
            return

        if vix3m / vix < 1.05 or vix > 25:
            self.liquidate()
            return

        # ==================================================
        # ⑤ Equity Cap(⭐压 45% 回撤的关键)
        # ==================================================
        self.equity_cap = max(self.equity_cap, equity)
        equity_ratio = equity / self.equity_cap

        if equity_ratio < 0.90:
            risk_multiplier = 0.5
        elif equity_ratio < 0.95:
            risk_multiplier = 0.7
        else:
            risk_multiplier = 1.0

        # ==================================================
        # ⑥ 基础目标仓位
        # ==================================================
        target_spxl = max_exposure * 0.5 * risk_multiplier
        target_tqqq = max_exposure * 0.5 * risk_multiplier

        # ==================================================
        # ⑦ 杠杆 ETF 专用保护
        # ==================================================
        self.profit_peak = max(self.profit_peak, equity)
        profit_dd = (self.profit_peak - equity) / self.profit_peak

        if profit_dd > 0.08:
            target_spxl *= 0.5
            target_tqqq *= 0.5

        def scale_by_extension(price, ema):
            dist = (price - ema) / ema
            if dist > 0.45:
                return 0.5
            elif dist > 0.30:
                return 0.7
            return 1.0

        target_spxl *= scale_by_extension(spxl_price, self.ema200_spxl.current.value)
        target_tqqq *= scale_by_extension(tqqq_price, self.ema200_tqqq.current.value)

        vol_spxl = self.history(self.spxl, 20, Resolution.DAILY)["close"].pct_change().std() * np.sqrt(252)
        vol_tqqq = self.history(self.tqqq, 20, Resolution.DAILY)["close"].pct_change().std() * np.sqrt(252)

        if vol_spxl > 0.60 and spxl_price < self.ema20_spxl.current.value:
            target_spxl = 0

        if vol_tqqq > 0.60 and tqqq_price < self.ema20_tqqq.current.value:
            target_tqqq = 0

        # ==================================================
        # ⑧ 执行
        # ==================================================
        self.set_holdings(self.spxl, target_spxl)
        self.set_holdings(self.tqqq, target_tqqq)

        self.log(
            f"VVIX={vvix:.1f} | DD={drawdown:.2%} | "
            f"EquityRatio={equity_ratio:.2f} | "
            f"SPXL={target_spxl:.2f} | TQQQ={target_tqqq:.2f}"
        )