Overall Statistics |
Total Orders 619 Average Win 0.32% Average Loss -0.24% Compounding Annual Return 35.062% Drawdown 12.200% Expectancy 0.757 Start Equity 100000 End Equity 165656.36 Net Profit 65.656% Sharpe Ratio 1.257 Sortino Ratio 1.786 Probabilistic Sharpe Ratio 64.683% Loss Rate 25% Win Rate 75% Profit-Loss Ratio 1.33 Alpha 0.303 Beta 0.73 Annual Standard Deviation 0.182 Annual Variance 0.033 Information Ratio 2.465 Tracking Error 0.134 Treynor Ratio 0.313 Total Fees $1163.54 Estimated Strategy Capacity $1800000.00 Lowest Capacity Asset ABFS R735QTJ8XC9X Portfolio Turnover 1.77% |
# region imports from AlgorithmImports import * # endregion class FundamentalBacktester1(QCAlgorithm): def Initialize(self): self.SetStartDate(2000, 1, 1) self.SetEndDate(2001, 9, 4) self.SetCash(100000) # Set initial cash balance (portfolio size) # Universe settings self.UniverseSettings.Resolution = Resolution.Daily # Set resolution to daily # portolio constuction model self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel(lambda time: None)) # Use equal weighted portfolio construction module self.Settings.RebalancePortfolioOnInsightChanges = False # means that the portfoli constuction module above will not update porfio when insights change self.Settings.RebalancePortfolioOnSecurityChanges = True # means that the portfoli constuction module above will update porfio when securities change # Set universe selection model self.SetUniverseSelection(FineFundamentalUniverseSelectionModel(self.CoarseFilter, self.FineFilter)) # sets the stock universe to whaterver come out of the course & then fine filter fuctions # set benchmark self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol # call spy (S&P500) to save in self.spy self.SetBenchmark("SPY") # sets spy as the benchmark (aplha determined by outperformance to this) # portfolio Rebalance scedule self.Schedule.On(self.DateRules.MonthStart("SPY"), self.TimeRules.AfterMarketOpen("SPY", 120), self.Rebalance) # call Rebalance function at the start of the month 2hr after market open - base market hours on SPY #initalise variables self.num_coarse = 500 # number of stocks that will come out of coarse selection filter (used in fuction) self.num_fine = 50 # number of stocks that will come out of coarse selection filter (used in fuction) self.day_counter = 0 self.new_symbols = [] self.filtered_stocks = [] self.PV = [] self.nextday = False self.monthly_rebalance = False self.rebalanced = False # coarse universe filter fuction (runs every day at midnight) #filters stock universe, returns 500 sybmols with the largest market cap, that have available fundamdal data and dollar volume > $5m def CoarseFilter(self, coarse): if self.nextday: return Universe.Unchanged # If the rebalance flag is not set, do not change the universe if not self.monthly_rebalance: self.rebalanced = False return Universe.Unchanged self.rebalanced = True self.monthly_rebalance = False filtered_coarse = sorted([x for x in coarse if x.HasFundamentalData and x.Price > 0 and x.DollarVolume > 5000000],key=lambda x: x.market_cap, reverse=True) # Filter out all stocks that have no fundamental data or DollarVolume <= $5M, sort by market cap - large to small # send list of filtered coarse stocks to debugger inc number and pe_ratio debug_output = "" for i, coarse_obj in enumerate(filtered_coarse[:self.num_coarse], start=1): symbol_name = coarse_obj.Symbol.Value # Ensure to get only the symbol string market_cap = coarse_obj.market_cap debug_output += f"{i}. {symbol_name}: Market Cap = {market_cap}\n" self.Debug(f"Course Filtered Symbols:\n{debug_output} ") # Log the selected symbols with P/E ratios return [x.Symbol for x in filtered_coarse[:self.num_coarse]] # Return the top 'num_coarse' symbols # fine universe filter fuction, (runs every day at midnight) #filters Course filter output (500 sybmols) returns 50 sybmols with lowest PE Ratio def FineFilter(self, fine): # Filter and sort stocks by P/E ratio self.filtered_stocks = [] selected_fine = [x for x in fine if x.valuation_ratios.pe_ratio > 0] filtered_fine = sorted([x for x in selected_fine], key=lambda x: x.valuation_ratios.pe_ratio) # Sort by P/E ratio self.filtered_stocks = [x.Symbol for x in filtered_fine[:self.num_fine]] # Select the top 50 'num_fine' symbols # send list of filtered fine stocks to debugger inc number and pe_ratio debug_output = "" for i, fine_obj in enumerate(filtered_fine[:self.num_fine], start=1): symbol_name = fine_obj.Symbol.Value # Ensure to get only the symbol string pe_ratio = fine_obj.valuation_ratios.pe_ratio debug_output += f"{i}. {symbol_name}: PE Ratio = {pe_ratio}\n" self.Debug(f"Fine Filtered Symbols\n{debug_output} ") # Log the selected symbols with P/E ratios return self.filtered_stocks # Return the selected symbols # OnData fuction (runs every day at midnight) def OnData(self, data): self.day_counter += 1 # returns out of function every call exept if just rebalenced if not self.rebalanced: return # If the next day = 1 skip till the next bar/day/on_data call (means that we always buy stock 1 day after getting buy signal) if not self.nextday: self.nextday = True self.Debug("skip day "+str(self.time) +" day "+str(self.day_counter)) return self.Debug("next day "+str(self.time)) self.nextday = False insights = [] # Close insights for symbols no longer in self.filtered_stocks for symbol in list(self.PV): # Create a copy of the list to modify it during iteration if symbol not in self.filtered_stocks: insights.append(Insight.Price(symbol, timedelta(days=7560), InsightDirection.Flat)) self.PV.remove(symbol) self.Debug(f"Close {symbol}") # log symbol close # Add new insights for symbols not in self.PV (open) for symbol in self.filtered_stocks: if symbol not in self.PV: insights.append(Insight.Price(symbol, timedelta(days=7560), InsightDirection.Up)) self.Debug(f"Open {symbol}") # log symbol open self.PV.append(symbol) self.EmitInsights(insights) # this is called according to Schedule.On in Initialize def Rebalance(self): self.monthly_rebalance = True