Overall Statistics
Total Orders
175
Average Win
6.12%
Average Loss
-3.58%
Compounding Annual Return
18.206%
Drawdown
28.400%
Expectancy
0.870
Start Equity
10000
End Equity
123115.28
Net Profit
1131.153%
Sharpe Ratio
0.755
Sortino Ratio
0.794
Probabilistic Sharpe Ratio
19.026%
Loss Rate
31%
Win Rate
69%
Profit-Loss Ratio
1.71
Alpha
0.007
Beta
0.911
Annual Standard Deviation
0.158
Annual Variance
0.025
Information Ratio
-0.102
Tracking Error
0.038
Treynor Ratio
0.131
Total Fees
$202.08
Estimated Strategy Capacity
$0
Lowest Capacity Asset
QQQ RIWIV7K5Z9LX
Portfolio Turnover
3.18%
from AlgorithmImports import *

class QQQSPYMomentumRotationSystem(QCAlgorithm):
    def initialize(self):
        # Backtest period
        self.set_start_date(2010, 1, 1)
        self.set_end_date(2025, 1, 1)
        self.set_cash(10000)
        
        # Add securities
        self.qqq = self.add_equity("QQQ", Resolution.DAILY).symbol
        self.spy = self.add_equity("SPY", Resolution.DAILY).symbol
        
        # Set QQQ as primary benchmark for QuantConnect comparison
        self.set_benchmark(self.qqq)
        
        # === BENCHMARK TRACKING FOR MANUAL COMPARISON ===
        self.initial_cash = 10000
        self.qqq_shares = 0
        self.spy_shares = 0
        self.qqq_benchmark_value = 10000
        self.spy_benchmark_value = 10000
        self.benchmark_initialized = False
        
        # === STRATEGY PARAMETERS ===
        self.momentum_period = 20        # 20-day momentum lookback
        self.qqq_bias = 0.01            # 1% bias toward QQQ
        self.rebalance_frequency = 5     # Rebalance every 5 days
        self.min_momentum_diff = 0.005   # 0.5% minimum difference to switch
        
        # === TRACKING VARIABLES ===
        self.current_holding = None      # Current position ("QQQ" or "SPY")
        self.last_target = None          # Last target symbol for persistence filter
        self.qqq_periods = 0            # Count of periods holding QQQ
        self.spy_periods = 0            # Count of periods holding SPY
        self.total_switches = 0         # Total number of switches
        self.last_rebalance_date = None # Track last rebalance date
        
        # === PERFORMANCE TRACKING ===
        self.trade_log = []             # Store trade history
        
        # Schedule rebalancing
        self.schedule.on(
            self.date_rules.every(DayOfWeek.MONDAY),
            self.time_rules.after_market_open(self.qqq, 30),
            self.rebalance
        )
        
        # Daily performance logging - changed to monthly for cleaner output
        self.schedule.on(
            self.date_rules.month_start(),
            self.time_rules.after_market_open(self.qqq, 30),
            self.log_performance
        )
        
        self.log("=== QQQ/SPY MOMENTUM ROTATION SYSTEM INITIALIZED ===")
        self.log(f"Momentum Period: {self.momentum_period} days")
        self.log(f"QQQ Bias: {self.qqq_bias:.1%}")
        self.log(f"Min Momentum Diff: {self.min_momentum_diff:.1%}")

    def rebalance(self):
        """Main rebalancing logic with comprehensive error handling"""
        if self.is_warming_up:
            return
            
        # Skip if we just rebalanced recently
        if (self.last_rebalance_date is not None and 
            (self.time.date() - self.last_rebalance_date).days < self.rebalance_frequency):
            return
        
        try:
            self.log(f"=== QQQ-PRIMARY REBALANCE: {self.time.strftime('%Y-%m-%d')} ===")
            
            # === STEP 1: GET MOMENTUM DATA WITH ERROR HANDLING ===
            qqq_momentum, spy_momentum = self.get_momentum_data()
            if qqq_momentum is None or spy_momentum is None:
                self.log("ERROR: Could not calculate momentum - skipping rebalance")
                return
            
            # === STEP 2: CALCULATE SCORES ===
            qqq_score = qqq_momentum + self.qqq_bias
            spy_score = spy_momentum
            score_diff = qqq_score - spy_score
            
            self.log(f"QQQ Momentum: {qqq_momentum:.2%}, SPY Momentum: {spy_momentum:.2%}")
            self.log(f"QQQ Score: {qqq_score:.2%}, SPY Score: {spy_score:.2%}")
            self.log(f"Score Difference: {score_diff:.2%}")
            
            # === STEP 3: DETERMINE TARGET ===
            if score_diff > self.min_momentum_diff:
                target_symbol = self.qqq
                target_name = "QQQ"
                allocation = 1.0
            elif score_diff < -self.min_momentum_diff:
                target_symbol = self.spy
                target_name = "SPY"
                allocation = 1.0
            else:
                # Scores too close - maintain current position or default to QQQ
                if self.current_holding:
                    self.log(f"MOMENTUM INDECISIVE: Maintaining {self.current_holding}")
                    return
                else:
                    target_symbol = self.qqq
                    target_name = "QQQ"
                    allocation = 1.0
                    self.log("MOMENTUM INDECISIVE: Defaulting to QQQ")
            
            # === STEP 4: PERSISTENCE FILTER ===
            if self.last_target == target_symbol:
                self.log(f"PERSISTENCE FILTER: Holding {target_name} - consistent momentum")
                return
            
            # Update last target after persistence check
            self.last_target = target_symbol
            
            # === STEP 5: CHECK IF ALREADY IN POSITION ===
            if self.current_holding == target_name:
                self.log(f"ALREADY HOLDING: {target_name} at {allocation:.0%} allocation")
                return
            
            # === STEP 6: EXECUTE TRADE ===
            success = self.execute_rotation(target_symbol, target_name, allocation)
            
            if success:
                # Only increment counters after successful trade
                if target_name == "QQQ":
                    self.qqq_periods += 1
                else:
                    self.spy_periods += 1
                
                self.last_rebalance_date = self.time.date()
                
        except Exception as e:
            self.log(f"REBALANCE ERROR: {str(e)}")
            self.debug(f"Rebalance exception: {e}")

    def get_momentum_data(self):
        """Safely retrieve and calculate momentum data"""
        try:
            # Get QQQ history
            try:
                qqq_hist = self.history(self.qqq, self.momentum_period + 1, Resolution.DAILY)
                if qqq_hist.empty or len(qqq_hist) < self.momentum_period + 1:
                    self.log("ERROR: Insufficient QQQ history")
                    return None, None
                qqq_closes = qqq_hist['close']
            except Exception as e:
                self.log(f"ERROR: QQQ history retrieval failed: {str(e)}")
                return None, None
            
            # Get SPY history
            try:
                spy_hist = self.history(self.spy, self.momentum_period + 1, Resolution.DAILY)
                if spy_hist.empty or len(spy_hist) < self.momentum_period + 1:
                    self.log("ERROR: Insufficient SPY history")
                    return None, None
                spy_closes = spy_hist['close']
            except Exception as e:
                self.log(f"ERROR: SPY history retrieval failed: {str(e)}")
                return None, None
            
            # Calculate momentum safely
            try:
                qqq_momentum = (qqq_closes.iloc[-1] - qqq_closes.iloc[0]) / qqq_closes.iloc[0]
                spy_momentum = (spy_closes.iloc[-1] - spy_closes.iloc[0]) / spy_closes.iloc[0]
                return qqq_momentum, spy_momentum
            except Exception as e:
                self.log(f"ERROR: Momentum calculation failed: {str(e)}")
                return None, None
                
        except Exception as e:
            self.log(f"ERROR: get_momentum_data failed: {str(e)}")
            return None, None

    def execute_rotation(self, target_symbol, target_name, allocation):
        """Execute the rotation trade with error handling"""
        try:
            # Liquidate current positions
            self.liquidate()
            
            # Calculate position size
            quantity = self.calculate_order_quantity(target_symbol, allocation)
            
            if quantity == 0:
                self.log(f"ERROR: Cannot calculate valid quantity for {target_name}")
                return False
            
            # Execute trade
            order_ticket = self.market_order(target_symbol, quantity)
            
            if order_ticket:
                # Update tracking
                self.current_holding = target_name
                self.total_switches += 1
                
                # Log trade
                self.log("=== ROTATION EXECUTED ===")
                self.log(f"NEW POSITION: {target_name} | Allocation: {allocation:.0%} | Shares: {quantity}")
                self.log(f"Total Switches: {self.total_switches}")
                
                # Store trade record
                self.trade_log.append({
                    'date': self.time,
                    'action': 'BUY',
                    'symbol': target_name,
                    'quantity': quantity,
                    'price': self.securities[target_symbol].price,
                    'allocation': allocation
                })
                
                return True
            else:
                self.log(f"ERROR: Order failed for {target_name}")
                return False
                
        except Exception as e:
            self.log(f"ERROR: execute_rotation failed: {str(e)}")
            return False

    def log_performance(self):
        """Log monthly performance summary with benchmark comparison"""
        try:
            # Initialize benchmarks on first run
            if not self.benchmark_initialized and self.securities[self.qqq].price > 0:
                qqq_price = self.securities[self.qqq].price
                spy_price = self.securities[self.spy].price
                
                self.qqq_shares = self.initial_cash / qqq_price
                self.spy_shares = self.initial_cash / spy_price
                self.benchmark_initialized = True
                self.log(f"BENCHMARKS INITIALIZED: QQQ shares: {self.qqq_shares:.2f}, SPY shares: {self.spy_shares:.2f}")
            
            # Calculate current values
            portfolio_value = self.portfolio.total_portfolio_value
            cash = self.portfolio.cash
            invested = portfolio_value - cash
            
            # Calculate benchmark values
            if self.benchmark_initialized:
                qqq_price = self.securities[self.qqq].price
                spy_price = self.securities[self.spy].price
                
                self.qqq_benchmark_value = self.qqq_shares * qqq_price
                self.spy_benchmark_value = self.spy_shares * spy_price
                
                # Calculate returns
                strategy_return = (portfolio_value - self.initial_cash) / self.initial_cash
                qqq_return = (self.qqq_benchmark_value - self.initial_cash) / self.initial_cash
                spy_return = (self.spy_benchmark_value - self.initial_cash) / self.initial_cash
                
                self.log(f"=== MONTHLY PERFORMANCE: {self.time.strftime('%Y-%m-%d')} ===")
                self.log(f"STRATEGY VALUE: ${portfolio_value:,.2f} | Return: {strategy_return:.2%}")
                self.log(f"QQQ B&H VALUE: ${self.qqq_benchmark_value:,.2f} | Return: {qqq_return:.2%}")
                self.log(f"SPY B&H VALUE: ${self.spy_benchmark_value:,.2f} | Return: {spy_return:.2%}")
                
                # Performance vs benchmarks
                vs_qqq = strategy_return - qqq_return
                vs_spy = strategy_return - spy_return
                self.log(f"OUTPERFORMANCE: vs QQQ: {vs_qqq:.2%} | vs SPY: {vs_spy:.2%}")
                
                # Plot benchmark comparison for charts
                self.plot("Benchmark Comparison", "Strategy", portfolio_value)
                self.plot("Benchmark Comparison", "QQQ B&H", self.qqq_benchmark_value)
                self.plot("Benchmark Comparison", "SPY B&H", self.spy_benchmark_value)
                
            else:
                self.log(f"=== MONTHLY PERFORMANCE: {self.time.strftime('%Y-%m-%d')} ===")
                self.log(f"Portfolio Value: ${portfolio_value:,.2f}")
            
            if self.current_holding:
                self.log(f"Current Position: {self.current_holding}")
            
            self.log(f"QQQ Periods: {self.qqq_periods} | SPY Periods: {self.spy_periods}")
            self.log(f"Total Switches: {self.total_switches}")
            
        except Exception as e:
            self.log(f"ERROR: log_performance failed: {str(e)}")

    def on_order_event(self, order_event):
        """Track order execution"""
        if order_event.status == OrderStatus.FILLED:
            self.log(f"ORDER FILLED: {order_event.symbol} - {order_event.fill_quantity} shares @ ${order_event.fill_price:.2f}")

    def on_end_of_algorithm(self):
        """Final performance summary with detailed benchmark comparison"""
        try:
            final_value = self.portfolio.total_portfolio_value
            initial_value = self.initial_cash
            
            # Calculate final benchmark values
            if self.benchmark_initialized:
                qqq_final_price = self.securities[self.qqq].price
                spy_final_price = self.securities[self.spy].price
                
                qqq_final_value = self.qqq_shares * qqq_final_price
                spy_final_value = self.spy_shares * spy_final_price
                
                # Calculate total returns
                strategy_total_return = (final_value - initial_value) / initial_value
                qqq_total_return = (qqq_final_value - initial_value) / initial_value
                spy_total_return = (spy_final_value - initial_value) / initial_value
                
                # Calculate annualized returns (15 years: 2010-2025)
                years = 15
                strategy_annual = (final_value / initial_value) ** (1/years) - 1
                qqq_annual = (qqq_final_value / initial_value) ** (1/years) - 1
                spy_annual = (spy_final_value / initial_value) ** (1/years) - 1
                
                self.log("=" * 60)
                self.log("=== FINAL PERFORMANCE COMPARISON (2010-2025) ===")
                self.log("=" * 60)
                
                self.log(f"INITIAL INVESTMENT: ${initial_value:,.2f}")
                self.log("")
                
                self.log("FINAL VALUES:")
                self.log(f"  Strategy (Rotation): ${final_value:,.2f}")
                self.log(f"  QQQ Buy & Hold:     ${qqq_final_value:,.2f}")
                self.log(f"  SPY Buy & Hold:     ${spy_final_value:,.2f}")
                self.log("")
                
                self.log("TOTAL RETURNS:")
                self.log(f"  Strategy: {strategy_total_return:.2%}")
                self.log(f"  QQQ B&H:  {qqq_total_return:.2%}")
                self.log(f"  SPY B&H:  {spy_total_return:.2%}")
                self.log("")
                
                self.log("ANNUALIZED RETURNS:")
                self.log(f"  Strategy: {strategy_annual:.2%}")
                self.log(f"  QQQ B&H:  {qqq_annual:.2%}")
                self.log(f"  SPY B&H:  {spy_annual:.2%}")
                self.log("")
                
                self.log("OUTPERFORMANCE:")
                self.log(f"  vs QQQ: {strategy_total_return - qqq_total_return:+.2%} total | {strategy_annual - qqq_annual:+.2%} annual")
                self.log(f"  vs SPY: {strategy_total_return - spy_total_return:+.2%} total | {strategy_annual - spy_annual:+.2%} annual")
                self.log("")
                
                # Strategy statistics
                self.log("STRATEGY STATISTICS:")
                self.log(f"  Total Switches: {self.total_switches}")
                self.log(f"  QQQ Periods: {self.qqq_periods}")
                self.log(f"  SPY Periods: {self.spy_periods}")
                
                if self.total_switches > 0:
                    avg_hold_period = (self.qqq_periods + self.spy_periods) / self.total_switches
                    self.log(f"  Avg Hold Period: {avg_hold_period:.1f} rebalances")
                
                if self.current_holding:
                    self.log(f"  Final Position: {self.current_holding}")
                
                self.log("=" * 60)
                
                # Determine winner
                if strategy_total_return > qqq_total_return and strategy_total_return > spy_total_return:
                    self.log("*** STRATEGY WINS! Rotation outperformed both benchmarks! ***")
                elif strategy_total_return > max(qqq_total_return, spy_total_return):
                    best_benchmark = "QQQ" if qqq_total_return > spy_total_return else "SPY"
                    self.log(f"Strategy beat {best_benchmark} but not the better benchmark")
                else:
                    self.log("Strategy underperformed both benchmarks")
                
            else:
                # Fallback if benchmarks weren't initialized
                strategy_total_return = (final_value - initial_value) / initial_value
                strategy_annual = (final_value / initial_value) ** (1/15) - 1
                
                self.log("=== FINAL ALGORITHM SUMMARY ===")
                self.log(f"Initial Value: ${initial_value:,.2f}")
                self.log(f"Final Value: ${final_value:,.2f}")
                self.log(f"Total Return: {strategy_total_return:.2%}")
                self.log(f"Annualized Return: {strategy_annual:.2%}")
                self.log(f"Total Switches: {self.total_switches}")
                
        except Exception as e:
            self.log(f"ERROR: on_end_of_algorithm failed: {str(e)}")