Overall Statistics |
Total Orders 570 Average Win 2.44% Average Loss -1.12% Compounding Annual Return 61.779% Drawdown 53.800% Expectancy 1.032 Start Equity 100000 End Equity 1676247.61 Net Profit 1576.248% Sharpe Ratio 1.316 Sortino Ratio 1.667 Probabilistic Sharpe Ratio 65.705% Loss Rate 36% Win Rate 64% Profit-Loss Ratio 2.18 Alpha 0.309 Beta 1.309 Annual Standard Deviation 0.344 Annual Variance 0.118 Information Ratio 1.264 Tracking Error 0.271 Treynor Ratio 0.346 Total Fees $14189.90 Estimated Strategy Capacity $3000.00 Lowest Capacity Asset SENEB R735QTJ8XC9X Portfolio Turnover 1.14% |
# Import all the required libraries and functionalities from QuantConnect from AlgorithmImports import * import numpy as np # Define the main algorithm class that extends QCAlgorithm class SmallCapNetCurrentAssetValueEffect(QCAlgorithm): def Initialize(self) -> None: # Initialization and configuration of the algorithm self.SetStartDate(2019, 1, 1) # Set the backtest start date self.SetCash(100000) # Set the initial capital for the algorithm # Set universe settings self.UniverseSettings.Leverage = 1.5 # Define the leverage for the securities self.UniverseSettings.Resolution = Resolution.Daily # Use daily data resolution # Configure universe selection based on a custom fundamental function self.AddUniverse(self.FundamentalFunction) # Set some portfolio and algorithm settings self.Settings.MinimumOrderMarginPortfolioPercentage = 0.0 self.Settings.daily_precise_end_time = False # Variables for filtering and selecting securities self.fundamental_count = 3000 # Maximum number of securities to consider self.market = 'usa' # Market filter for USA self.country_id = 'USA' # Country ID filter for USA self.fin_sector_code = 103 # Exclude financial sector based on sector code self.ncav_threshold = 1.75 # Threshold for Net Current Asset Value # Define a range for small-cap market capitalization (between $300 million and $2 billion) self.small_cap_lower_threshold = 0.1e9 # $100 million self.small_cap_upper_threshold = 0.75e9 # $750 miilio # List to store selected long symbols self.long_symbols = [] # Define the months when rebalancing should occur self.rebalance_months = [1, 4, 7, 10] # Rebalance in January, April, July, and October # A flag to trigger universe selection self.selection_flag = True # Add benchmark securities (SPY and GLD) self.spy = self.AddEquity('SPY', Resolution.Daily).Symbol # S&P 500 ETF self.gld = self.AddEquity('GLD', Resolution.Daily).Symbol # Gold # Calculate 100-day and 200-day SMAs for SPY self.spy_sma_100 = self.SMA(self.spy, 100, Resolution.Daily) self.spy_sma_200 = self.SMA(self.spy, 200, Resolution.Daily) self.previous_crossover = None # Variable to track the previous crossover state # Warm up the data to initialize indicators self.SetWarmUp(200) # Warm-up period of 200 days to ensure SMAs are ready # Schedule events for rebalancing self.Schedule.On( self.DateRules.MonthStart(self.spy), self.TimeRules.AfterMarketOpen(self.spy), self.Selection ) # Schedule rebalancing at the start of the defined months # Perform an initial selection at the start of the algorithm self.Schedule.On( self.DateRules.Today, self.TimeRules.AfterMarketOpen(self.spy), self.Selection ) def FundamentalFunction(self, fundamental: List[Fundamental]) -> List[Symbol]: # Return an unchanged universe if selection is not needed if not self.selection_flag: return Universe.Unchanged # Filter out securities based on various fundamental criteria filtered = [ f for f in fundamental if f.HasFundamentalData and f.Market == self.market and f.CompanyReference.CountryId == self.country_id and f.AssetClassification.MorningstarSectorCode != self.fin_sector_code and not np.isnan(f.EarningReports.BasicAverageShares.TwelveMonths) and f.EarningReports.BasicAverageShares.TwelveMonths != 0 and not np.isnan(f.MarketCap) and f.MarketCap >= self.small_cap_lower_threshold # Small-cap lower bound and f.MarketCap <= self.small_cap_upper_threshold # Small-cap upper bound and f.ValuationRatios.WorkingCapitalPerShare != 0 and f.Volume > 0 # Ensure stocks have volume data ] # Sort the filtered securities by market cap in descending order and take the top fundamental_count sorted_by_market_cap = sorted(filtered, key=lambda f: f.MarketCap, reverse=True)[:self.fundamental_count] # Select securities with a Net Current Asset Value (NCAV) ratio above the threshold self.long_symbols = [ x.Symbol for x in sorted_by_market_cap if ((x.ValuationRatios.WorkingCapitalPerShare * x.EarningReports.BasicAverageShares.TwelveMonths) / x.MarketCap) > self.ncav_threshold ] return self.long_symbols def OnData(self, slice: Slice) -> None: # Ensure SMAs are ready if self.IsWarmingUp: return # Check if SMAs are ready if not (self.spy_sma_100.IsReady and self.spy_sma_200.IsReady): return # Check for SMA crossovers current_crossover = self.spy_sma_100.Current.Value < self.spy_sma_200.Current.Value # If the 100 SMA crosses below the 200 SMA, invest fully in GLD if current_crossover and (self.previous_crossover is None or not self.previous_crossover): self.SetHoldings(self.gld, 1.0) # If the 100 SMA crosses above the 200 SMA, exit GLD elif not current_crossover and (self.previous_crossover is None or self.previous_crossover): self.Liquidate(self.gld) # Update the previous crossover state self.previous_crossover = current_crossover # If selection is not needed, exit the method if not self.selection_flag: return # Set the flag to False after selection is processed self.selection_flag = False # Create portfolio targets for each selected symbol and set holdings portfolio = [ PortfolioTarget(symbol, 1 / len(self.long_symbols)) for symbol in self.long_symbols if slice.ContainsKey(symbol) and slice[symbol] is not None ] self.SetHoldings(portfolio, True) # Adjust holdings in the portfolio self.long_symbols.clear() # Clear the list of long symbols def Selection(self) -> None: # Rebalance if it's one of the rebalance months or at the start date if self.Time.month in self.rebalance_months or self.Time == self.StartDate: self.selection_flag = True def OnSecuritiesChanged(self, changes: SecurityChanges) -> None: # Set leverage and fee model for newly added securities for security in changes.AddedSecurities: security.SetLeverage(1.5) security.SetFeeModel(InteractiveBrokersFeeModel()) # Custom fee model for Interactive Brokers class InteractiveBrokersFeeModel(FeeModel): def GetOrderFee(self, parameters: OrderFeeParameters) -> OrderFee: # Calculate commission based on Interactive Brokers' fee structure commission_per_share = 0.005 # $0.005 per share min_commission = 1.00 # Minimum commission per trade max_commission = 0.5 / 100 * parameters.Order.AbsoluteQuantity * parameters.Security.Price # 0.5% of trade value # Determine the final commission amount commission = max(min_commission, min(commission_per_share * parameters.Order.AbsoluteQuantity, max_commission)) return OrderFee(CashAmount(commission, "USD"))