| 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)
'''