Overall Statistics
Total Orders
280
Average Win
0.05%
Average Loss
-0.06%
Compounding Annual Return
14.938%
Drawdown
21.300%
Expectancy
0.085
Start Equity
1000000
End Equity
1111014.17
Net Profit
11.101%
Sharpe Ratio
0.357
Sortino Ratio
0.413
Probabilistic Sharpe Ratio
34.567%
Loss Rate
43%
Win Rate
57%
Profit-Loss Ratio
0.89
Alpha
0.018
Beta
0.943
Annual Standard Deviation
0.178
Annual Variance
0.032
Information Ratio
0.304
Tracking Error
0.052
Treynor Ratio
0.067
Total Fees
$285.70
Estimated Strategy Capacity
$0
Lowest Capacity Asset
TDG TGZ66QQM06SL
Portfolio Turnover
0.62%
Drawdown Recovery
154
from AlgorithmImports import *

class SP500PEFilteredBetaStrategy(QCAlgorithm):
    """
    S&P 500 P/E Filtered Beta-Weighted Strategy
    
    Strategy:
    - Start with S&P 500 universe
    - Exclude 100 stocks with lowest P/E ratios (value traps)
    - Market-cap weight all remaining stocks (~400 stocks)
    - Double the weight of any stock with beta > 1.0
    - Rebalance weekly
    """
    
    def initialize(self):
        self.set_start_date(2024, 1, 1)
        self.set_end_date(2025, 6, 1)
        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)
        self.exclude_lowest_pe = 100  # Exclude 100 lowest P/E stocks
        self.beta_multiplier_threshold = 1.0  # Double weight if beta > 1.0
        
        # Data tracking
        self.universe_symbols = []
        self.last_rebalance = datetime.min
        self.initial_rebalance_done = False
        
        # 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 P/E Filtered Beta-Weighted Strategy initialized")
        self.debug(f"Excluding: {self.exclude_lowest_pe} lowest P/E stocks")
        self.debug(f"Beta > {self.beta_multiplier_threshold}: 2x weight")
    
    def on_securities_changed(self, changes):
        """Handle universe changes"""
        for removed in changes.removed_securities:
            symbol = removed.symbol
            if symbol in self.universe_symbols:
                self.universe_symbols.remove(symbol)
        
        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"""
        
        # Allow initial rebalance immediately, then enforce weekly frequency
        if not self.initial_rebalance_done:
            self.initial_rebalance_done = True
        elif (self.time - self.last_rebalance) < self.rebalance_frequency:
            return
            
        self.debug(f"=== Rebalancing at {self.time} ===")
        
        # Collect P/E ratios, market caps, and betas for all stocks
        pe_ratios = {}
        market_caps = {}
        betas = {}
        
        for symbol in self.universe_symbols:
            try:
                fund = self.securities[symbol].fundamentals
                
                if fund.has_fundamental_data:
                    pe_ratio = fund.valuation_ratios.pe_ratio
                    market_cap = fund.market_cap
                    beta = fund.asset_classification.morningstar_sector_code  # Placeholder
                    
                    # Get actual beta from security
                    if symbol in self.securities:
                        security = self.securities[symbol]
                        # Calculate beta using 252-day returns vs SPY
                        beta = self.calculate_beta(symbol)
                    
                    # Only include stocks with valid P/E ratios and market cap
                    if pe_ratio > 0 and pe_ratio < 1000 and market_cap > 0:
                        pe_ratios[symbol] = pe_ratio
                        market_caps[symbol] = market_cap
                        if beta is not None:
                            betas[symbol] = beta
                        
            except Exception:
                continue
        
        self.debug(f"Found {len(pe_ratios)} stocks with valid data")
        
        if len(pe_ratios) < 100:
            self.debug("Insufficient stocks with valid P/E ratios")
            return
        
        # Sort stocks by P/E ratio
        sorted_by_pe = sorted(pe_ratios.items(), key=lambda x: x[1])
        
        # Identify stocks to exclude (only lowest P/E)
        lowest_pe_stocks = [symbol for symbol, pe in sorted_by_pe[:self.exclude_lowest_pe]]
        excluded_stocks = set(lowest_pe_stocks)
        
        # Get remaining stocks
        included_stocks = [symbol for symbol in pe_ratios.keys() if symbol not in excluded_stocks]
        
        self.debug(f"Excluded {len(excluded_stocks)} lowest P/E stocks")
        self.debug(f"Included {len(included_stocks)} stocks")
        
        if len(included_stocks) < 50:
            self.debug("Too few stocks remaining after filters")
            return
        
        # Calculate base market-cap weights
        base_weights = {}
        total_market_cap = sum(market_caps[symbol] for symbol in included_stocks)
        
        for symbol in included_stocks:
            base_weight = market_caps[symbol] / total_market_cap
            base_weights[symbol] = base_weight
        
        # Apply beta multiplier
        adjusted_weights = {}
        high_beta_count = 0
        
        for symbol in included_stocks:
            weight = base_weights[symbol]
            
            # Double weight if beta > 1.0
            if symbol in betas and betas[symbol] > self.beta_multiplier_threshold:
                weight *= 2.0
                high_beta_count += 1
            
            adjusted_weights[symbol] = weight
        
        # Normalize weights to sum to 1.0
        total_adjusted_weight = sum(adjusted_weights.values())
        target_weights = {symbol: weight / total_adjusted_weight 
                         for symbol, weight in adjusted_weights.items()}
        
        self.debug(f"High beta stocks (>1.0) with 2x weight: {high_beta_count}")
        
        # Execute rebalancing
        successful_trades = self.rebalance_to_targets(target_weights)
        
        self.debug(f"Successfully executed {successful_trades} trades")
        
        # Log P/E range that was excluded
        self.debug(f"Lowest P/E excluded: {sorted_by_pe[0][1]:.1f} to {sorted_by_pe[self.exclude_lowest_pe-1][1]:.1f}")
        
        # Log top holdings by weight
        top_holdings = sorted(target_weights.items(), key=lambda x: x[1], reverse=True)[:5]
        self.debug(f"Top 5 holdings by weight:")
        for symbol, weight in top_holdings:
            beta_str = f"(beta: {betas[symbol]:.2f})" if symbol in betas else ""
            self.debug(f"  {self.get_ticker(symbol)}: {weight:.2%} {beta_str}")
        
        self.last_rebalance = self.time
    
    def calculate_beta(self, symbol):
        """Calculate beta vs SPY using historical returns"""
        try:
            # Get 252 trading days of history
            history = self.history([symbol, self.spy_symbol], 252, Resolution.DAILY)
            
            if history.empty or len(history) < 100:
                return None
            
            # Calculate returns
            symbol_returns = history.loc[symbol]['close'].pct_change().dropna()
            spy_returns = history.loc[self.spy_symbol]['close'].pct_change().dropna()
            
            # Align the data
            aligned_data = symbol_returns.align(spy_returns, join='inner')
            symbol_returns_aligned = aligned_data[0]
            spy_returns_aligned = aligned_data[1]
            
            if len(symbol_returns_aligned) < 50:
                return None
            
            # Calculate beta using covariance method
            covariance = symbol_returns_aligned.cov(spy_returns_aligned)
            variance = spy_returns_aligned.var()
            
            if variance > 0:
                beta = covariance / variance
                return beta
            
            return None
            
        except Exception:
            return None
    
    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:
                try:
                    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")
        
        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