Overall Statistics |
Total Orders 1699 Average Win 1.36% Average Loss -1.29% Compounding Annual Return 11.534% Drawdown 60.300% Expectancy 0.307 Start Equity 100000 End Equity 1520227.52 Net Profit 1420.228% Sharpe Ratio 0.381 Sortino Ratio 0.42 Probabilistic Sharpe Ratio 0.064% Loss Rate 37% Win Rate 63% Profit-Loss Ratio 1.06 Alpha 0.035 Beta 1.026 Annual Standard Deviation 0.21 Annual Variance 0.044 Information Ratio 0.272 Tracking Error 0.133 Treynor Ratio 0.078 Total Fees $6932.76 Estimated Strategy Capacity $160000.00 Lowest Capacity Asset MMMB XQ652EL0JWPX Portfolio Turnover 0.81% |
#region imports from AlgorithmImports import * #endregion import numpy as np import datetime from datetime import timedelta, date ''' GOING PITBULL This is a value + momentum strategy. we find the best prices in the same way of warren buffet, and then we find the best momentum stocks using the sharpe ratio. Need to add a check for extra cash to be added to the portfolio. symbol_fundamentals = self.fundamentals_values_dict[symbol] pe = symbol_fundamentals[0] fcfps = symbol_fundamentals[1] eps_growth = symbol_fundamentals[2] eg = symbol_fundamentals[3] analysts_eps = symbol_fundamentals[4] price = symbol_fundamentals[5] current_fair_price = symbol_fundamentals[6] price_percent = symbol_fundamentals[7] sharpe_ratio = symbol_fundamentals[8] ''' class BuildingMagic(QCAlgorithm): def Initialize(self): self.SetStartDate(2000, 1, 1) # Set Start Date #self.SetEndDate(2011, 12, 31) # Set End Date self.SetCash(100_000) # Set Cash self.spy = self.AddEquity("SPY", Resolution.Hour).Symbol self.SetBenchmark(self.spy) self.UniverseSettings.Resolution = Resolution.Hour self.UniverseSettings.Leverage = 1 self.AddUniverse(self.SelectCoarse, self.SelectFine) ### VARIABLES ### self.fundamentals_rank = [] # list of tuples (symbol, rank) self.fundamentals_values_dict = {} self.shares_in_portfolio = 12 ### DATES ### self.trading_day = (self.Time).date() # every time initialize is called, trading_day is set to the current day self.manual_trading_date = date.fromisoformat('2023-07-03') # use YYYY-MM-DD format def SelectCoarse(self, coarse): open_positions = [x.Symbol for x in self.Portfolio.Securities.Values if x.Invested] if not open_positions or self.manual_trading_date == (self.Time).date(): self.trading_day = (self.Time).date() if (self.Time).date() != self.trading_day: return Universe.Unchanged self.fundamentals_rank.clear() self.fundamentals_values_dict.clear() volume = self.Portfolio.TotalPortfolioValue if self.Portfolio.TotalPortfolioValue > 500_000 else 500_000 selected = [x for x in coarse if x.HasFundamentalData and x.Price < self.Portfolio.TotalPortfolioValue / 200 and (self.Time - x.Symbol.ID.Date).days > 365 and x.DollarVolume > volume] sortedByDollarVolume = sorted(selected, key=lambda x: x.DollarVolume, reverse=False) return [c.Symbol for c in sortedByDollarVolume] def SelectFine(self, fine): prefilter = [x for x in fine] self.Log(f'there are {len(prefilter)} companies left BEFORE the fine filter') filtered = [x for x in fine if x.CompanyReference.CountryId in {"USA"} and x.CompanyReference.PrimaryExchangeID in {"NYS", "NAS"} and (self.Time - x.SecurityReference.IPODate).days > 365 and x.AssetClassification.MorningstarSectorCode != 103 #filter out financials and x.AssetClassification.MorningstarSectorCode != 207 #filter out utilities according to joel greenblatt and x.ValuationRatios.PERatio >= 5 ] for fine in filtered: symbol = fine.Symbol pe = fine.ValuationRatios.NormalizedPERatio fcfps = (fine.ValuationRatios.CFOPerShare + (fine.FinancialStatements.IncomeStatement.EBIT.TwelveMonths / fine.CompanyProfile.SharesOutstanding))/2 if fine.CompanyProfile.SharesOutstanding > 0 else fine.ValuationRatios.CFOPerShare eps_growth = (fine.EarningRatios.NormalizedDilutedEPSGrowth.FiveYears) eg = (fine.EarningRatios.EquityPerShareGrowth.FiveYears) analysts_eps = (fine.ValuationRatios.first_year_estimated_eps_growth) if eps_growth < 0 and eg < 0 else 0 fundamentals_values = [pe, fcfps, eps_growth, eg, analysts_eps] self.fundamentals_values_dict[symbol] = fundamentals_values self.fundamentals_rank.clear() #list the keys of self.fundamentals_values_dict sorted1 = sorted(filtered, key=lambda x: self.find_buyable_price(x.Symbol) , reverse=False) sorted2 = sorted(filtered, key=lambda x: self.calc_sharpe_ratio(x.Symbol) , reverse=True) stockBySymbol = {} max_rank_allowed = 40 while len(stockBySymbol) < self.shares_in_portfolio: stockBySymbol.clear() for index, stock in enumerate(sorted1): rank1 = index rank2 = sorted2.index(stock) avgRank = (rank1 + rank2) /2 if rank1 < max_rank_allowed and rank2 < max_rank_allowed*2: stockBySymbol[stock.Symbol] = avgRank max_rank_allowed += 1 self.Log(f'there are {len(filtered)} companies left AFTER the fine filter and max rank allowed is {max_rank_allowed}') self.fundamentals_rank = sorted(stockBySymbol.items(), key = lambda x: x[1], reverse = False) #list of tuples (symbol, rank) self.fundamentals_rank = self.fundamentals_rank[:self.shares_in_portfolio] self.Log(f'there are {len(self.fundamentals_rank)} companies left AFTER the rank filter') # for (symbol, rank) in self.fundamentals_rank: # symbol_fundamentals = self.fundamentals_values_dict[symbol] # price = symbol_fundamentals[5] if symbol_fundamentals[5] else 0 # self.Log(f'{symbol} has rank {rank}, pe: {symbol_fundamentals[0]}, fcfps: {symbol_fundamentals[1]}, eps_growth: {symbol_fundamentals[2]}, eg: {symbol_fundamentals[3]}, analysts_eps: {symbol_fundamentals[4]}, price: {price}') symbols = [x[0] for x in self.fundamentals_rank] return symbols def OnData(self, slice: Slice) -> None: if self.Time.date() != self.trading_day: self.SelectTradingDay() return self.liquidations() self.PrintIntentions() self.ExecuteBuyOrders() ####################################### ### Strategy Calculations ### ####################################### def calc_sharpe_ratio(self, symbol): '''Calculate the sharpe ratio of a stock for the last year , daily resolution, and exluding the last month''' history = self.History(symbol, 365, Resolution.Daily) if 'close' not in history or history['close'].empty: return -1 returns = history['close'].pct_change() returns = returns.dropna() returns = returns[:-21] sharpe_ratio = returns.mean() / returns.std() self.fundamentals_values_dict[symbol].append(sharpe_ratio) return sharpe_ratio def find_buyable_price(self, symbol): '''Find the buyable price of each stock and returns in what % of the price it is -0.5 of the evaluated price is the maximum price we can buy''' symbol_fundamentals = self.fundamentals_values_dict[symbol] pe = symbol_fundamentals[0] fcfps = symbol_fundamentals[1] eps_growth = symbol_fundamentals[2] eg = symbol_fundamentals[3] analysts_eps = symbol_fundamentals[4] eps = (fcfps) growth_ratio = (eps_growth + eg + analysts_eps )/3 # growth_ratio = analysts_eps if eps_growth < 0 and eg < 0 else (eps_growth + eg) earnings_dict = {0: eps} # calculate earnings for 10 year for i in range(1,10): j = i - 1 earnings_dict[i] = round(earnings_dict[j]+(earnings_dict[j] * growth_ratio),2) fair_price_dict = {9: earnings_dict[9]*pe} # discount 15% for 10 years for i in range(8,-1,-1): j = i + 1 fair_price_dict[i] = fair_price_dict[j]/(1+0.15) current_fair_price = round(fair_price_dict[0],2) history = self.History(symbol, 3, Resolution.Minute) if 'close' not in history or history['close'].empty: price = 999_999_999 current_fair_price = 1 price_percent = 100 self.fundamentals_values_dict[symbol].append(price) self.fundamentals_values_dict[symbol].append(current_fair_price) self.fundamentals_values_dict[symbol].append(price_percent) return 100 price = history['close'] price = price.dropna() price = history['close'].iloc[-1] self.fundamentals_values_dict[symbol].append(price) # self.fundamentals_values_dict self.fundamentals_values_dict[symbol].append(current_fair_price) # self.fundamentals_values_dict #price = self.Securities[symbol].Price if current_fair_price > 0 and price > 0: price_percent = round(((price / current_fair_price)- 1) , 2) self.fundamentals_values_dict[symbol].append(price_percent) # self.fundamentals_values_dict return price_percent price_percent = 100 self.fundamentals_values_dict[symbol].append(price_percent) return price_percent ####################################### ### Portfolio Operations ### ####################################### def ExecuteBuyOrders(self): buy_list = [x[0] for x in self.fundamentals_rank] holding = 1/round(len(buy_list)*0.98,3) for symbol in buy_list: self.SetHoldings(symbol, holding) def liquidations(self): open_positions = [x.Symbol for x in self.Portfolio.Securities.Values if x.Invested] sell_list = [symbol for symbol in open_positions if symbol not in [x[0] for x in self.fundamentals_rank]] if len(sell_list) == 0: return #if a symbol is in open_positions but not in self.fundamentals_rank, liquidate for symbol in sell_list: self.Log(f'SELLING {symbol.Value}') self.Liquidate(symbol) ####################################### ### Logistics ### ####################################### def PrintIntentions(self): symbols = [x[0] for x in self.fundamentals_rank] stock_allocation = self.Portfolio.Cash / self.shares_in_portfolio self.Log(f'free cash in portfolio: {self.Portfolio.Cash}') self.Log(f"we're allocating {stock_allocation} for each stock") for symbol in symbols: symbol_fundamentals = self.fundamentals_values_dict[symbol] updated_symbol = symbol.Value pe = round(symbol_fundamentals[0],2) fcfps = round(symbol_fundamentals[1],2) eps_growth = round(symbol_fundamentals[2],2) eg = round(symbol_fundamentals[3],2) analysts_eps = round(symbol_fundamentals[4],2) price = round(symbol_fundamentals[5],2) current_fair_price = round(symbol_fundamentals[6],2) price_percent = round(symbol_fundamentals[7],2) sharpe_ratio = round(symbol_fundamentals[8],2) shares_amount = stock_allocation // price self.Log(f'For {updated_symbol}, buy {shares_amount} at {price}') self.Log(f'{updated_symbol}, has a current fair price of {current_fair_price} so the profit of margin is {price_percent} (Best: -1)') self.Log(f'VALUE DATA: PE Ratio: {pe}, fcfps: {fcfps}, eps_growth: {eps_growth}, Equity Growth: {eg}, Analyst Growth: { analysts_eps}') self.Log(f'Sharpe Ratio: {sharpe_ratio}') self.Log(f'------') def SelectTradingDay(self): # buys last day of training of the year --> check for connection and set alarm! #buying once a year has vastly outperformed shorter periods (6 and 4) both in gains and in consistency over 22 years of backtest quarter_last_month = (self.Time.month - 1) // 6 * 6 + 6 quarter_last_day = DateTime(self.Time.year, quarter_last_month, DateTime.DaysInMonth(self.Time.year, quarter_last_month)) # Get the trading days within the current quarter trading_days = self.TradingCalendar.GetDaysByType(TradingDayType.BusinessDay, self.Time, quarter_last_day) # Find the last trading day of the quarter for x in trading_days: day = x.Date.date() if day.weekday() == 0 or day.weekday() == 1: continue self.trading_day = day ''' This code has been updated on 27-12-2023 to include the following changes: - the stock size calcultion is now based on the free cash in the portfolio and not on the total portfolio value '''