Overall Statistics |
Total Orders 783 Average Win 0.45% Average Loss -0.36% Compounding Annual Return 13.597% Drawdown 33.900% Expectancy 0.417 Start Equity 1000000 End Equity 1665785.75 Net Profit 66.579% Sharpe Ratio 0.485 Sortino Ratio 0.624 Probabilistic Sharpe Ratio 15.318% Loss Rate 38% Win Rate 62% Profit-Loss Ratio 1.27 Alpha 0.123 Beta 0.661 Annual Standard Deviation 0.18 Annual Variance 0.032 Information Ratio 0.961 Tracking Error 0.147 Treynor Ratio 0.132 Total Fees $64154.75 Estimated Strategy Capacity $3200000.00 Lowest Capacity Asset TSN R735QTJ8XC9X Portfolio Turnover 1.51% |
# region imports from AlgorithmImports import * # endregion class FundamentalBacktester1(QCAlgorithm): def Initialize(self): self.SetStartDate(1999, 1, 1) self.SetEndDate(2003, 1, 1) self.SetCash(1000000) # Set initial cash balance (portfolio size) # Universe settings self.UniverseSettings.Resolution = Resolution.Daily # Set resolution to daily self.settings.minimum_order_margin_portfolio_percentage = 0 # 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 rebalance portfolio when insights change self.Settings.RebalancePortfolioOnSecurityChanges = True # means that the portfoli constuction module above will rebalance portfolio 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 = 250 # number of stocks that will be returned from coarse selection filter (used in fuction) self.num_fine = 30 # number of stocks that will be returned from fine selection filter (used in fuction) self.day_counter = 0 # counts number of days (number of time on_data is called) self.filtered_stocks = [] # output of FineFilter function self.PV = [] # stores currently held symbols self.charts = [] # stores symbols to plot self.nextday = False # checker to skip a day before order (so we dont buy based on fundmental data released that day - if rebalence is on a data_release day) self.monthly_rebalance = False # checker to see if rebalence is due self.rebalanced = False # checker to see if rebalence has happended self.no_data_day = False # checker to skip insight-changes / orders-created in on_data on non-market days self.blacklist = ['CNI','BCE'] # stores symbols that have data issues (exclude these in coarse filter) # Set a custom security initializer self.SetSecurityInitializer(self.CustomSecurityInitializer) # Set a custom security initializer def CustomSecurityInitializer(self, security): security.SetFeeModel(CustomPercentageFeeModel()) # call class containing fee model # 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.valuation_ratios.pe_ratio >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 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 if "SPY" in data: self.no_data_day = False spy_data = data["SPY"] self.Plot("SPY", "SPY", spy_data.Open, spy_data.High, spy_data.Low, spy_data.Close) else: self.no_data_day = True # returns out of function if not just rebalenced if not self.rebalanced: return if self.no_data_day: self.nextday = True self.Debug("No SPY data: skip a day "+str(self.time) +" day "+str(self.day_counter)) 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 a day "+str(self.time) +" day "+str(self.day_counter)) return self.Debug("next day "+str(self.time)) self.nextday = False insights = [] self.open_counter = 0 # counts opened symbols self.close_counter = 0 # counts closed symbols # Close insights for symbols no longer in self.filtered_stocks for symbol in list(self.PV): if symbol not in self.filtered_stocks: if not data.ContainsKey(symbol): # skip if no stock data self.Debug(f"Skipping sell: {symbol} as it does not have data {self.time}") # log skip continue insights.append(Insight.Price(symbol, timedelta(days=7560), InsightDirection.Flat)) # create insight to close symbol self.PV.remove(symbol) # remove symbol from PV list self.close_counter += 1 # add 1 to number of closed postions this rebalence if data[symbol] is not None: # should not need this if/else (one stock trade only is returning an error) close_price = data[symbol].Close self.Debug(f"Close {symbol} @ {close_price}") # log symbol close else: self.Debug(f"Close {symbol} @ could not retrieve price") # log symbol close (no price) # Add new open insights for symbols not in self.PV (open) for symbol in self.filtered_stocks: if symbol not in self.PV: if not data.ContainsKey(symbol): # skip if no stock data self.Debug(f"Skipping Buy: {symbol} as it does not have data {self.time}") # log skip continue insights.append(Insight.Price(symbol, timedelta(days=7560), InsightDirection.Up)) # create insight to open symbol self.PV.append(symbol) # add symbol from PV list self.open_counter += 1 # add 1 to number of opended postions this rebalence if data[symbol] is not None: # should not need this if/else (one stock trade only is returning an error) open_price = data[symbol].Close self.Debug(f"Open {symbol} @ {open_price}") # log symbol open else: self.Debug(f"Open {symbol} @ could not retrieve price") # log symbol open (no price) self.Debug("Currently holding "+str(len(self.PV))+" Securities: Opened "+str(self.open_counter)+" Closed "+str(self.close_counter)) # log number of opened, closed, and held securities self.EmitInsights(insights) # this is called according to Schedule.On in Initialize def Rebalance(self): self.monthly_rebalance = True class CustomPercentageFeeModel(FeeModel): # class called each trade, here we set the fee model (0.2% of trade value) def GetOrderFee(self, parameters): # Calculate fee as 0.2% of the order value order = parameters.Order value = order.AbsoluteQuantity * parameters.Security.Price fee = value * 0.002 # 0.2% of order value return OrderFee(CashAmount(fee, "USD")) ### spare/unused code: ''' #Plot for symbol in self.charts: # plot all charts that we have held during strategy if not data.ContainsKey(symbol): self.add_equity(symbol, Resolution.DAILY) self.Plot(symbol.Value, "Price", self.Securities[symbol].Price) insights = [] # Close insights for previously skipped symbols for symbol in list(self.close_list): if data.ContainsKey(symbol): # close if stock now has data insights.append(Insight.Price(symbol, timedelta(days=7560), InsightDirection.Flat)) self.close_list.remove(symbol) self.Debug(f"Closed {symbol} on {self.time} (previously skipped)") # Open insights for previously skipped symbols for symbol in list(self.open_list): if data.ContainsKey(symbol): # open if stock now has data insights.append(Insight.Price(symbol, timedelta(days=7560), InsightDirection.Up)) self.open_list.remove(symbol) self.Debug(f"Opened {symbol} on {self.time} (previously skipped)") # create plot for each symbol opened (for debugging) if symbol.Value not in self.charts: self.charts.append(symbol) chart = Chart(symbol.Value) chart.AddSeries(Series("Price", SeriesType.Line, 0)) self.AddChart(chart) '''