Overall Statistics
Total Orders
483
Average Win
0.22%
Average Loss
0%
Compounding Annual Return
9.464%
Drawdown
25.900%
Expectancy
0
Start Equity
100000
End Equity
143542.91
Net Profit
43.543%
Sharpe Ratio
0.391
Sortino Ratio
0.383
Probabilistic Sharpe Ratio
12.451%
Loss Rate
0%
Win Rate
100%
Profit-Loss Ratio
0
Alpha
-0.002
Beta
0.716
Annual Standard Deviation
0.142
Annual Variance
0.02
Information Ratio
-0.362
Tracking Error
0.07
Treynor Ratio
0.078
Total Fees
$483.00
Estimated Strategy Capacity
$620000000.00
Lowest Capacity Asset
CNHI VKCU8EVSVH45
Portfolio Turnover
0.06%
# region imports
from AlgorithmImports import *
# endregion

class SimpleEqualWeightSP500Replicator(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2020, 1, 1)
        self.SetEndDate(2023, 12, 31)
        self.SetCash(100000)
        
        # Set rebalancing flag and date trackers
        self.rebalance_day = -1
        self.last_month = -1
        
        # Subscribe to SPY for benchmarking
        self.AddEquity("SPY", Resolution.Daily)
        self.SetBenchmark("SPY")
        
        # Set universe settings for better performance
        self.UniverseSettings.Resolution = Resolution.Daily
        self.UniverseSettings.ExtendedMarketHours = False
        
        # Add universe selection
        self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
        
        # Set trading warm-up period
        self.SetWarmUp(timedelta(days=30))

    def CoarseSelectionFunction(self, coarse):
        # Only run once per month
        if self.Time.month == self.last_month:
            return Universe.Unchanged
        
        self.last_month = self.Time.month
        self.rebalance_day = self.Time.day
        
        # Filter for stocks with fundamental data, price > $5, and good liquidity
        filtered = [x for x in coarse if x.HasFundamentalData 
                   and x.Price > 5 
                   and x.DollarVolume > 10000000]
        
        # Sort by dollar volume (liquid stocks)
        sorted_by_volume = sorted(filtered, key=lambda x: x.DollarVolume, reverse=True)
        
        # Take top 1000 for fine selection
        return [x.Symbol for x in sorted_by_volume[:1000]]
    
    def FineSelectionFunction(self, fine):
        # Filter out stocks with negative earnings or extreme valuations
        filtered_fine = [x for x in fine if x.MarketCap > 3e9]
        
        # Sort by market cap in DESCENDING order (largest first)
        sorted_by_market_cap = sorted(filtered_fine, key=lambda x: x.MarketCap, reverse=True)
        
        # Take top 500 by market cap (or fewer if not enough stocks meet criteria)
        count = min(500, len(sorted_by_market_cap))
        selected = sorted_by_market_cap[:count]
        
        self.Log(f"Selected {len(selected)} stocks based on market cap")
        
        return [x.Symbol for x in selected]
    
    def OnData(self, data):
        # Skip if we're in the warm-up period
        if self.IsWarmingUp: 
            return
        
        # Only rebalance on the day we selected our universe
        if self.Time.day != self.rebalance_day: 
            return
        
        # Liquidate positions that are no longer in our universe
        for holding in self.Portfolio.Values:
            if holding.Invested and holding.Symbol not in self.ActiveSecurities:
                self.Liquidate(holding.Symbol)
        
        # Calculate equal weight for each position
        active_securities = [sec for sec in self.ActiveSecurities.Values 
                           if sec.Symbol.Value != "SPY" and sec.HasData]
        
        if len(active_securities) > 0:
            weight = 1.0 / len(active_securities)
            
            # Set holdings to equal weight only if security has data
            for security in active_securities:
                # Check if the security has valid price data
                if security.Price > 0 and security.IsTradable:
                    self.SetHoldings(security.Symbol, weight)
            
            self.Log(f"Rebalanced portfolio: {len(active_securities)} positions at {weight:.4f} weight each")