| 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