Overall Statistics
Total Orders
163
Average Win
33.76%
Average Loss
-5.45%
Compounding Annual Return
52.515%
Drawdown
54.800%
Expectancy
1.042
Start Equity
100000
End Equity
1259551.94
Net Profit
1159.552%
Sharpe Ratio
1.008
Sortino Ratio
1.11
Probabilistic Sharpe Ratio
36.686%
Loss Rate
72%
Win Rate
28%
Profit-Loss Ratio
6.19
Alpha
0.348
Beta
1.025
Annual Standard Deviation
0.434
Annual Variance
0.189
Information Ratio
0.883
Tracking Error
0.397
Treynor Ratio
0.427
Total Fees
$10345.57
Estimated Strategy Capacity
$0
Lowest Capacity Asset
AMD R735QTJ8XC9X
Portfolio Turnover
7.45%
Drawdown Recovery
413
#region imports
from AlgorithmImports import *
#endregion

class BubbleSurfer(QCAlgorithm):

    def Initialize(self):
        # 1. Timeline: Start of the AI Mania (2020)
        self.SetStartDate(2020, 1, 1)  
        self.SetEndDate(2026, 1, 28)    
        self.SetCash(100000) 
        self.SetName("AI Bubble Surfer (Chandelier Exit)")

        # 2. Universe: The "AI Bubble" Basket
        self.tickers = [
            "NVDA", "SMCI", "AMD", "TSM", 
            "AVGO", "META", "MSFT", "PLTR", "GOOGL"
        ]
        
        self.data = {}
        
        for ticker in self.tickers:
            symbol = self.AddEquity(ticker, Resolution.Daily).Symbol
            
            # 3. Indicators for the Chandelier Exit
            # ATR (Average True Range) measures the "noise" or "heat" of the stock
            atr = self.ATR(symbol, 14, MovingAverageType.Wilders, Resolution.Daily)
            
            # MAX indicator to track the "Highest High" over the lookback period
            highest_high = self.MAX(symbol, 22, Resolution.Daily, Field.High)
            
            self.data[symbol] = {
                'atr': atr,
                'max': highest_high,
                'stop_price': 0
            }

        # 4. Parameters
        self.atr_mult = 3.0       # 3x ATR is the standard "Trend Following" distance
        self.lookback = 63        # 3 Month Momentum ranking
        
        self.current_winner = None # Track our single holding

        # Check regularly
        self.Schedule.On(
            self.DateRules.EveryDay("NVDA"),
            self.TimeRules.AfterMarketOpen("NVDA", 30),
            self.ManagePositions
        )
        
        self.SetBenchmark("SPY")
        self.SetSecurityInitializer(lambda x: x.SetFeeModel(ConstantFeeModel(1.0)))
        self.SetWarmUp(100)

    def ManagePositions(self):
        if self.IsWarmingUp: return

        # 1. Update Stop Prices (The Chandelier Logic)
        for symbol, indicators in self.data.items():
            if not indicators['atr'].IsReady or not indicators['max'].IsReady:
                continue
            
            # Chandelier Exit Formula:
            # Stop = Highest High of last 22 days - (3 * Current ATR)
            # This "hangs" the stop loss from the highest point reached.
            hh = indicators['max'].Current.Value
            volatility = indicators['atr'].Current.Value
            
            new_stop = hh - (volatility * self.atr_mult)
            
            # Trailing Logic: Stop can ONLY move UP, never down (ratchet)
            # Unless we are not invested, then it resets.
            if self.Portfolio[symbol].Invested:
                if new_stop > indicators['stop_price']:
                    indicators['stop_price'] = new_stop
            else:
                indicators['stop_price'] = new_stop

        # 2. CHECK EXITS (Risk Management First)
        # If our current winner hit the floor, SELL immediately.
        if self.current_winner and self.Portfolio[self.current_winner].Invested:
            current_price = self.Securities[self.current_winner].Price
            stop_price = self.data[self.current_winner]['stop_price']
            
            if current_price < stop_price:
                self.Liquidate(self.current_winner, "Chandelier Exit Hit")
                self.current_winner = None # We are now Cash

        # 3. CHECK ENTRIES (FOMO Logic)
        # Only look for a new trade if we are 100% Cash (Winner takes all)
        if not self.Portfolio.Invested:
            self.EnterTopStock()

    def EnterTopStock(self):
        # Rank universe by 3-Month Momentum
        history = self.History(list(self.data.keys()), self.lookback, Resolution.Daily)
        if history.empty: return

        scores = {}
        for symbol in self.data.keys():
            if symbol not in history.index: continue
            closes = history.loc[symbol]['close']
            if len(closes) < self.lookback: continue
            
            # Momentum
            mom = (closes.iloc[-1] / closes.iloc[0]) - 1
            scores[symbol] = mom

        # Sort and Pick #1
        if not scores: return
        best_symbol = sorted(scores.items(), key=lambda x: x[1], reverse=True)[0][0]
        
        # FINAL SAFETY CHECK:
        # Don't buy if it's currently crashing (Price < Calculated Stop)
        # This prevents buying a "falling knife" even if it has high past momentum.
        price = self.Securities[best_symbol].Price
        stop = self.data[best_symbol]['stop_price']
        
        if price > stop:
            self.SetHoldings(best_symbol, 1.0)
            self.current_winner = best_symbol
            # Initialize the stop strictly
            self.data[best_symbol]['stop_price'] = self.data[best_symbol]['max'].Current.Value - (self.data[best_symbol]['atr'].Current.Value * self.atr_mult)