Overall Statistics |
Total Orders 4556 Average Win 0.62% Average Loss -0.70% Compounding Annual Return 16.266% Drawdown 40.300% Expectancy 0.244 Start Equity 100000 End Equity 4894860.04 Net Profit 4794.860% Sharpe Ratio 0.634 Sortino Ratio 0.727 Probabilistic Sharpe Ratio 4.711% Loss Rate 34% Win Rate 66% Profit-Loss Ratio 0.90 Alpha 0.07 Beta 0.68 Annual Standard Deviation 0.161 Annual Variance 0.026 Information Ratio 0.426 Tracking Error 0.13 Treynor Ratio 0.15 Total Fees $24699.39 Estimated Strategy Capacity $4100000.00 Lowest Capacity Asset IAAC R735QTJ8XC9X Portfolio Turnover 2.83% |
# region imports from AlgorithmImports import * # Import necessary QuantConnect algorithm classes and methods. import numpy as np # Import numpy for mathematical operations. # endregion class SelectionData(object): """ This class stores data related to each symbol in the universe. It tracks the symbol, calculates its SMA, and stores volume and price-related metrics. """ def __init__(self, symbol, period): # Initialize with the symbol and the period for the SMA calculation. self.symbol = symbol # Ticker symbol (e.g., 'AAPL'). self.sma = SimpleMovingAverage(period) # Create an SMA indicator with the given period. self.is_above_sma = False # Boolean indicating if the current price is above the SMA. self.volume = 0 # Stores the current volume of the symbol. self.price_to_sma_ratio = 0 # Stores the ratio of price to SMA for ranking purposes. def update(self, time, price, volume): """ Update the SMA and related metrics with new price and volume data. """ self.volume = volume # Update the volume with the latest data. # If the SMA value updates with the new price, recalculate metrics. if self.sma.Update(time, price): # Check if the current price is above the SMA. self.is_above_sma = price > self.sma.Current.Value # Calculate the price-to-SMA ratio, handling division by zero. self.price_to_sma_ratio = price / self.sma.Current.Value if self.sma.Current.Value != 0 else 0 class MyAlgorithm(QCAlgorithm): """ Main trading algorithm class that selects and manages a portfolio of stocks based on fundamental and technical indicators. """ def __init__(self): # Initialize key variables for the algorithm. self.num_fine = 30 # Number of fine-selected symbols to keep. self.smaDict = {} # Dictionary to store SelectionData objects by symbol. self.lastWeek = None # Track the last week of trading to rebalance monthly. self.currently_held_symbols = set() # Track symbols currently held in the portfolio. self.fineDict = {} # Store fine selection data by symbol. self.initial_market_cap = 1e9 # Initial minimum market cap threshold. self.inflation_rate = 0.03 # Annual inflation rate for market cap adjustment. def Initialize(self): """ Set up the initial algorithm parameters and universe settings. """ self.SetStartDate(1999, 1, 1) # Start backtest from January 1, 1999. self.SetCash(100000) # Set initial cash to $100,000. # Add a universe using coarse and fine selection filters. self.AddUniverse(self.SelectCoarse, self.SelectFine) # Use raw data normalization and daily resolution for analysis. self.UniverseSettings.DataNormalizationMode = DataNormalizationMode.Raw self.UniverseSettings.Resolution = Resolution.Daily self.symbols = [] # List of selected symbols after filtering. self.SetBenchmark('SPY') # Use SPY as the benchmark index. # Warm up the algorithm with 300 days of data for SMA calculations. self.SetWarmUp(TimeSpan.FromDays(300)) def SelectCoarse(self, coarse: List[CoarseFundamental]) -> List[Symbol]: """ Coarse selection step: Filter stocks based on whether their current price is above the SMA. """ for c in coarse: # If the symbol is not already in the SMA dictionary, add it. if c.Symbol not in self.smaDict: self.smaDict[c.Symbol] = SelectionData(c.Symbol, 200) # Create SMA with 200-day period. # Update the symbol's SMA and volume data. avg = self.smaDict[c.Symbol] avg.update(c.EndTime, c.AdjustedPrice, c.DollarVolume) # Select symbols where the current price is above the SMA. return [c.Symbol for c in coarse if self.smaDict[c.Symbol].is_above_sma] def SelectFine(self, fine: List[FineFundamental]) -> List[Symbol]: """ Fine selection step: Filter stocks based on fundamental metrics and select the top symbols. """ # Store fine selection data for quick access during rebalancing. self.fineDict = {x.Symbol: x for x in fine} # Calculate the number of years since the start date to adjust market cap for inflation. years_since_start = (self.Time - self.StartDate).days / 365.25 adjusted_market_cap = self.initial_market_cap * (1 + self.inflation_rate) ** years_since_start # Filter symbols based on fundamental metrics. filtered_fine = [ x for x in fine if x.Symbol in self.smaDict # Ensure the symbol exists in the SMA dictionary. and x.MarketCap > adjusted_market_cap # Market cap must exceed the inflation-adjusted threshold. and x.ValuationRatios.PSRatio < 1 # Price-to-sales ratio must be less than 1. and x.ValuationRatios.PriceChange1M > 0.01 # Price must have increased over the last month. and x.OperationRatios.RevenueGrowth.ThreeMonths > 0.01 # Revenue growth over three months must be positive. and x.OperationRatios.OperationIncomeGrowth.ThreeMonths > 0.01 # Operating income growth must be positive. and x.OperationRatios.NetIncomeGrowth.ThreeMonths > 0.01 # Net income growth must be positive. and x.OperationRatios.NetIncomeContOpsGrowth.ThreeMonths > 0.01 # Growth in income from operations must be positive. and x.OperationRatios.CFOGrowth.OneYear > 0.01 # CFO growth over one year must be positive. and x.OperationRatios.FCFGrowth.OneYear > 0.01 # FCF growth over one year must be positive. ] # Sort the filtered symbols by price-to-SMA ratio in descending order and select the top symbols. top = sorted(filtered_fine, key=lambda x: self.smaDict[x.Symbol].price_to_sma_ratio, reverse=True)[:self.num_fine] # Store the selected symbols and update the currently held symbols. self.symbols = [x.Symbol for x in top] self.currently_held_symbols = set(self.Portfolio.Keys) return self.symbols def OnData(self, data): """ Rebalance the portfolio monthly by selling underperforming symbols and setting new holdings. """ # Only rebalance on Wednesdays (weekday 2). if self.Time.weekday() != 2: return # Get the current week number. week_number = int(self.Time.strftime('%V')) # Rebalance every 4th week (monthly) and ensure it's not the same week as the last rebalance. if week_number % 4 == 0 and week_number != self.lastWeek: # Liquidate symbols that are no longer selected. symbols_to_liquidate = self.currently_held_symbols - set(self.symbols) for symbol in symbols_to_liquidate: self.Liquidate(symbol) # Calculate the total log market cap for weight distribution. total_log_market_cap = sum(np.log(self.fineDict[symbol].MarketCap) for symbol in self.symbols if symbol in self.fineDict) # Set portfolio holdings based on the log of market cap as a weight. for symbol in self.symbols: if symbol in data.Keys: log_market_cap_weight = np.log(self.fineDict[symbol].MarketCap) / total_log_market_cap if total_log_market_cap != 0 else 0 self.SetHoldings(symbol, log_market_cap_weight) # Update the last week of rebalancing. self.lastWeek = week_number