Overall Statistics
Total Orders
2130
Average Win
0.03%
Average Loss
-0.02%
Compounding Annual Return
19.505%
Drawdown
12.100%
Expectancy
0.382
Start Equity
1000000
End Equity
1193880.51
Net Profit
19.388%
Sharpe Ratio
0.733
Sortino Ratio
1.04
Probabilistic Sharpe Ratio
54.527%
Loss Rate
42%
Win Rate
58%
Profit-Loss Ratio
1.40
Alpha
-0.045
Beta
1.01
Annual Standard Deviation
0.119
Annual Variance
0.014
Information Ratio
-0.94
Tracking Error
0.046
Treynor Ratio
0.086
Total Fees
$2174.25
Estimated Strategy Capacity
$25000000.00
Lowest Capacity Asset
BYA R735QTJ8XC9X
Portfolio Turnover
2.26%
Drawdown Recovery
139
from AlgorithmImports import *

class SP500FundamentalsGrowthStrategy(QCAlgorithm):
    """
    S&P 500 Growth Strategy - Fundamentals Only Version
    
    Growth Score (Higher = Better):
    - YoY Revenue Growth: 70% weight  
    - ROE: 30% weight (quality growth proxy)
    
    Portfolio: Top 200 Growth stocks with tier weighting
    """
    
    def initialize(self):
        self.set_start_date(2023, 1, 1)
        self.set_end_date(2023, 12, 31)
        self.set_cash(1000000)
        self.set_benchmark("SPY")
        
        # S&P 500 universe via SPY ETF constituents
        self.spy_symbol = self.add_equity("SPY", Resolution.DAILY).symbol
        self.add_universe(self.universe.etf(self.spy_symbol, self.universe_settings))
        
        # Strategy parameters
        self.rebalance_frequency = timedelta(days=7)  # Weekly rebalancing (more practical)
        
        # Data tracking
        self.universe_symbols = []
        self.last_rebalance = datetime.min
        
        # Schedule weekly rebalancing
        self.schedule.on(
            self.date_rules.week_start("SPY"),
            self.time_rules.at(10, 0),
            self.rebalance_strategy
        )
        
        self.debug("S&P 500 Fundamentals Growth-Only strategy initialized")
    
    def on_securities_changed(self, changes):
        """Handle universe changes - no indicators to create"""
        # Remove old securities
        for removed in changes.removed_securities:
            symbol = removed.symbol
            if symbol in self.universe_symbols:
                self.universe_symbols.remove(symbol)
        
        # Add new securities
        for added in changes.added_securities:
            symbol = added.symbol
            if symbol != self.spy_symbol:
                self.universe_symbols.append(symbol)
        
        self.debug(f"Universe updated: {len(self.universe_symbols)} stocks")
    
    def rebalance_strategy(self):
        """Main rebalancing logic using only fundamentals"""
        
        if (self.time - self.last_rebalance) < self.rebalance_frequency:
            return
            
        self.debug(f"Rebalancing at {self.time}")
        
        # Calculate scores for all stocks
        growth_scores = {}
        processed_count = 0
        
        for symbol in self.universe_symbols:
            try:
                fund = self.securities[symbol].fundamentals
                
                if fund.has_fundamental_data:
                    processed_count += 1
                    
                    # Calculate growth score
                    growth_score = self.calculate_growth_score_fundamental(fund)
                    if growth_score is not None:
                        growth_scores[symbol] = growth_score
                        
            except Exception:
                continue
        
        self.debug(f"Processed {processed_count} stocks with fundamentals")
        self.debug(f"Valid growth scores: {len(growth_scores)}")
        
        if len(growth_scores) < 50:
            self.debug("Insufficient data for rebalancing")
            return
        
        # Rank stocks (higher score = better)
        growth_rankings = self.rank_stocks(growth_scores, ascending=False)
        
        # Select top 200 growth stocks
        top_200_growth = growth_rankings[:200]
        
        # Calculate position weights with tier structure (100% of portfolio)
        growth_weights = self.calculate_tier_weights(top_200_growth, 1.0)
        
        # Execute rebalancing
        successful_trades = self.rebalance_to_targets(growth_weights)
        
        # Enhanced logging
        self.debug(f"Rebalanced: {len(growth_weights)} growth positions")
        self.debug(f"Successfully executed {successful_trades}/{len(growth_weights)} trades")
        
        # Log top picks with scores for verification
        if len(growth_rankings) >= 10:
            self.debug("=== TOP 5 GROWTH PICKS ===")
            for i, symbol in enumerate(growth_rankings[:5]):
                ticker = self.get_ticker(symbol)
                score = growth_scores[symbol]
                weight = growth_weights.get(symbol, 0)
                self.debug(f"{i+1}. {ticker}: Score={score:.1f}, Weight={weight:.2%}")
        
        self.last_rebalance = self.time
    
    def calculate_growth_score_fundamental(self, fund):
        """
        Calculate growth score using only fundamentals (higher = better)
        70% YoY Revenue Growth + 30% ROE (quality growth proxy)
        """
        try:
            # Revenue growth component (70% weight)
            revenue_growth = fund.operation_ratios.revenue_growth.one_year
            if revenue_growth <= 0:
                growth_component = 0
            else:
                growth_component = min(revenue_growth * 100, 50)  # Cap at 50% for scoring
            
            # ROE component (30% weight) - higher ROE = better quality growth
            roe = fund.operation_ratios.roe.value
            if roe <= 0:
                roe_component = 0
            else:
                roe_component = min(roe * 100, 50)  # Cap at 50% ROE for scoring
            
            # Calculate weighted growth score
            growth_score = (growth_component * 0.70) + (roe_component * 0.30)
            
            return growth_score
            
        except Exception:
            return None
    
    def rank_stocks(self, scores_dict, ascending=True):
        """Rank stocks by scores"""
        sorted_items = sorted(scores_dict.items(), key=lambda x: x[1], reverse=not ascending)
        return [symbol for symbol, score in sorted_items]
    
    def calculate_tier_weights(self, ranked_symbols, total_weight):
        """
        Calculate tier-based weights
        Ranks 1-50: 50% of portfolio weight (1% each)
        Ranks 51-100: 25% of portfolio weight (0.5% each)
        Ranks 101-200: 25% of portfolio weight (0.25% each)
        """
        weights = {}
        
        # Tier 1: Ranks 1-50 get 50% of total weight
        tier1_weight = (total_weight * 0.50) / 50  # 1% each
        for i in range(min(50, len(ranked_symbols))):
            weights[ranked_symbols[i]] = tier1_weight
        
        # Tier 2: Ranks 51-100 get 25% of total weight
        tier2_weight = (total_weight * 0.25) / 50  # 0.5% each
        for i in range(50, min(100, len(ranked_symbols))):
            weights[ranked_symbols[i]] = tier2_weight
        
        # Tier 3: Ranks 101-200 get 25% of total weight
        tier3_weight = (total_weight * 0.25) / 100  # 0.25% each
        for i in range(100, min(200, len(ranked_symbols))):
            weights[ranked_symbols[i]] = tier3_weight
        
        return weights
    
    def rebalance_to_targets(self, target_weights):
        """Execute rebalancing to target weights"""
        
        # Liquidate positions not in target
        current_holdings = {kvp.key for kvp in self.portfolio if kvp.value.invested}
        for symbol in current_holdings:
            if symbol not in target_weights:
                self.liquidate(symbol)
        
        # Set target positions
        successful_trades = 0
        failed_trades = 0
        
        for symbol, target_weight in target_weights.items():
            if target_weight > 0.0001:  # Minimum position size
                try:
                    # Check if security has valid price data
                    if symbol in self.securities and self.securities[symbol].price > 0:
                        self.set_holdings(symbol, target_weight)
                        successful_trades += 1
                    else:
                        failed_trades += 1
                except Exception:
                    failed_trades += 1
                    continue
        
        if failed_trades > 0:
            self.debug(f"Failed to place {failed_trades} orders (likely due to missing price data)")
        
        return successful_trades
    
    def get_ticker(self, symbol):
        """Extract ticker from symbol"""
        return str(symbol).split(' ')[0]
    
    def on_data(self, data):
        """Handle incoming data"""
        pass