Overall Statistics |
Total Orders 7304 Average Win 0.43% Average Loss -0.37% Compounding Annual Return 45.593% Drawdown 22.300% Expectancy 0.191 Start Equity 100000 End Equity 1304960.68 Net Profit 1204.961% Sharpe Ratio 1.319 Sortino Ratio 1.514 Probabilistic Sharpe Ratio 76.840% Loss Rate 45% Win Rate 55% Profit-Loss Ratio 1.16 Alpha 0.292 Beta 0.168 Annual Standard Deviation 0.232 Annual Variance 0.054 Information Ratio 0.835 Tracking Error 0.267 Treynor Ratio 1.817 Total Fees $6418.57 Estimated Strategy Capacity $5200000.00 Lowest Capacity Asset NUZE XFH59UK0MWYT Portfolio Turnover 10.52% |
#region imports from AlgorithmImports import * #endregion # https://quantpedia.com/strategies/the-52-week-high-and-short-term-reversal-in-stock-returns/ # # The investment universe consists of all common stocks (share codes 10 or 11) trading at the NYSE, AMEX and NASDAQ. Exclude stocks with prices # that are less than $5 at the end of month t. Divide stocks into three groups: the micro-cap, small-cap, and large-cap firms, using the NYSE 20th # and 50th percentile as breakpoints. From now on focus purely on the large-cap firms. Firstly, compute PTH, the ratio of the current price of a # stock to its highest price within the past 52 weeks to measure nearness to the 52-week high. At the end of month t, stocks are sorted into # quintile portfolios based on their past 1-month returns. Secondly, stocks are also sorted into quintile portfolios based on their PTH ranking # based on the price information up to month t−1. The intersection of these reversal and PTH portfolios produces 25 portfolios. Long low PTH past # losers portfolio and short low PTH past winners portfolio. Portfolio is equally weighted and rebalanced on a monthly basis. # # QC implementation changes: # - Instead of all listed stock, we select 500 most liquid stocks traded on NYSE, AMEX, or NASDAQ. # - Instead of historical stock highs, stock closes are used as a proxy. class ReversalCombinedwithVolatility(QCAlgorithm): def Initialize(self): # self.SetStartDate(2000, 1, 1) self.SetStartDate(2017, 12, 1) # self.SetEndDate(2017, 1, 31) self.SetEndDate(2024, 9, 30) self.SetCash(100000) self.saltare_allocation = 0.25 self.max_short_size = 0.05 self.SetSecurityInitializer(lambda x: x.SetMarketPrice(self.GetLastKnownPrice(x))) self.coarse_count = 500 self.data = {} self.period = 52 * 5 self.symbol = self.AddEquity('SPY', Resolution.Daily).Symbol self.long = [] self.short = [] # self.meme_months = [6, 7] self.meme_months = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] # self.restricted_stocks = ['CVNA', 'MARA', 'PLUG', 'AI', 'NIO', 'RIVN', 'DOCU'] # self.restricted_stocks = ['NKLA', 'CVNA', 'AMC', 'BYND', 'UPST', 'MARA', 'RIOT', # 'COIN', 'FUBO', 'HUT', 'SNAP', 'FSR', 'SPCE', 'TLRY', 'PACW', # 'SOFI', 'ENVX', 'AFRM', 'GME', 'PLUG', 'ASTS', 'MULN', 'SIRI', # 'LCID', 'CCL', 'LAZR', 'CLOV', 'MSTR', 'RKLB', 'XXII', 'VRM', # 'RGTI', 'EOSE', 'MVST', 'RBT', 'EDTX', 'MVIS', 'IONQ', 'NVAX', # 'BRCC', 'TMC', 'QBTS', 'CHPT', 'PGY', 'DWAC', 'NVTA', 'SQL', # 'DNA', 'WISH', 'CPA'] # This list is produced using both a Quiver Quant and a Roundhill MEME ETF Holdings Scrape # 'https://www.quiverquant.com/scores/memestocks' # 'https://www.roundhillinvestments.com/etf/meme/' self.restricted_stocks = ['HOOD', 'DNA', 'SOFI', 'TLRY', 'VRM', 'SPCE', 'EDTX', 'ENVX', 'MULN', 'AMC', 'PLTR', 'CHPT', 'MSTR', 'PGY', 'UPST', 'EOSE', 'CVNA', 'IONQ', 'XXII', 'SIRI', 'MVST', 'CLOV', 'BRCC', 'LCID', 'HUT', 'ASTS', 'SQL', 'ENPH', 'TSLA', 'DWAC', 'DAL', 'BYND', 'CPA', 'QBTS', 'NKLA', 'IBM', 'RGTI', 'RIVN', 'NVTA', 'TMC', 'GME', 'LAZR', 'MARA', 'MVIS', 'AAL', 'PLUG', 'FUBO', 'COIN', 'RBT', 'RIOT', 'RKLB', 'AFRM', 'FSR', 'SNAP', 'CCL', 'NVAX', 'NIO', 'MAT', 'PACW', 'WISH', 'DISH'] # self.restricted_stocks = ['HOOD', 'DNA', 'SOFI', 'TLRY', 'VRM', 'SPCE', 'EDTX', 'ENVX', # 'MULN', 'AMC', 'PLTR', 'CHPT', 'MSTR', 'PGY', 'UPST', 'EOSE', # 'CVNA', 'IONQ', 'XXII', 'SIRI', 'MVST', 'CLOV', 'BRCC', 'LCID', # 'HUT', 'ASTS', 'SQL', 'ENPH', 'TSLA', 'DWAC', 'DAL', 'BYND', 'CPA', # 'QBTS', 'NKLA', 'IBM', 'RGTI', 'RIVN', 'NVTA', 'TMC', 'GME', 'LAZR', # 'MARA', 'MVIS', 'AAL', 'PLUG', 'FUBO', 'COIN', 'RBT', 'RIOT', 'RKLB', # 'AFRM', 'FSR', 'SNAP', 'CCL', 'NVAX', 'NIO', 'MAT', 'PACW', 'WISH', # 'DISH', 'MVST', 'NVAX', 'QBTS', 'MARA', 'TSLA', 'CAVA', 'BYND', 'FSR', # 'SNAP', 'SMCI', 'RBLX', 'LAZR', 'BRCC', 'CPA', 'RIVN', 'FUBO', 'ASTS', # 'ENVX', 'MSTR', 'SNOW', 'ZM', 'DWAC', 'NKLA', 'PLTR', 'COIN', 'MRNA', # 'MVIS', 'AMC', 'RIOT', 'CLOV', 'RGTI', 'MULN', 'GME', 'SOFI', 'TMC', # 'SIRI', 'EDTX', 'HUT', 'JNJ', 'IONQ', 'RBT', 'CVNA', 'NVTA', 'TLRY', # 'CCL', 'CLF', 'PLUG', 'PACW', 'AFRM', 'RKLB', 'SE', 'EOSE', 'LCID', # 'PGY', 'ULTA', 'SQL', 'DNA', 'PTON', 'WISH', 'UPST', 'PANW', 'HOOD', # 'DKS', 'SPCE', 'VRM', 'CHPT', 'XXII'] self.meme_etf = self.AddEquity('MEME', Resolution.Daily).Symbol self.selection_flag = False self.UniverseSettings.Resolution = Resolution.Daily self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction) self.Schedule.On(self.DateRules.MonthEnd(self.symbol), self.TimeRules.AfterMarketOpen(self.symbol), self.Selection) self.settings.daily_precise_end_time = False def OnSecuritiesChanged(self, changes): for security in changes.AddedSecurities: # security.SetFeeModel(CustomFeeModel(self)) security.SetFeeModel(CustomFeeModel()) security.SetLeverage(10) def CoarseSelectionFunction(self, coarse): # Update the rolling window every day. for stock in coarse: symbol = stock.Symbol # Store monthly price. if symbol in self.data: self.data[symbol].update(stock.AdjustedPrice) if not self.selection_flag: return Universe.Unchanged # selected = [x.Symbol for x in coarse if x.HasFundamentalData and x.Market == 'usa'] selected = [x.Symbol for x in sorted([x for x in coarse if x.HasFundamentalData and x.Market == 'usa'], key = lambda x: x.DollarVolume, reverse = True)[:self.coarse_count]] # Warmup price rolling windows. for symbol in selected: if symbol in self.data: continue self.data[symbol] = SymbolData(symbol, self.period) history = self.History(symbol, self.period, Resolution.Daily) if history.empty: self.Log(f"Not enough data for {symbol} yet.") continue closes = history.loc[symbol].close # for time, close in closes.iteritems(): for time, close in closes.items(): self.data[symbol].update(close) return [x for x in selected if self.data[x].is_ready()] def FineSelectionFunction(self, fine): fine = [x for x in fine if ((x.SecurityReference.ExchangeId == "NYS") or (x.SecurityReference.ExchangeId == "NAS") or (x.SecurityReference.ExchangeId == "ASE"))] # if len(fine) > self.coarse_count: # sorted_by_market_cap = sorted(fine, key = lambda x: x.MarketCap, reverse=True) # top_by_market_cap = sorted_by_market_cap[:self.coarse_count] # else: # top_by_market_cap = fine top_by_market_cap = fine pth_performance = {x.Symbol : (self.data[x.Symbol].pth(), self.data[x.Symbol].performance()) for x in top_by_market_cap} sorted_by_pth = sorted(pth_performance.items(), key = lambda x: x[1][0], reverse = True) sorted_by_pth = [x[0] for x in sorted_by_pth] sorted_by_ret = sorted(pth_performance.items(), key = lambda x: x[1][1], reverse = True) sorted_by_ret = [x[0] for x in sorted_by_ret] quintile = int(len(sorted_by_ret) / 5) low_pth = sorted_by_pth[-quintile:] top_ret = sorted_by_ret[:quintile] low_ret = sorted_by_ret[-quintile:] self.long = [x for x in low_pth if x in low_ret] self.short = [x for x in low_pth if x in top_ret] return self.long + self.short def OnData(self, data): if not self.selection_flag: return self.selection_flag = False # Trade execution. long_count = len(self.long) short_count = len(self.short) stocks_invested = [x.Key for x in self.Portfolio if x.Value.Invested] for symbol in stocks_invested: if symbol not in self.long + self.short: self.Liquidate(symbol) for symbol in self.long: self.SetHoldings(symbol, 1 / long_count) # self.Log(str(symbol.Value)) for symbol in self.short: if self.Time.year == 2023: if self.Time.month in self.meme_months: if symbol.Value not in self.restricted_stocks: # self.SetHoldings(symbol, -1 / short_count) # self.SetHoldings(symbol, max(-0.20, -1 / short_count)) self.SetHoldings(symbol, max(-(self.max_short_size/self.saltare_allocation), -1 / short_count)) # self.SetHoldings(symbol, max(-0.12, -1 / short_count)) else: # self.SetHoldings(symbol, -1 / short_count) # self.SetHoldings(symbol, max(-0.20, -1 / short_count)) self.SetHoldings(symbol, max(-(self.max_short_size/self.saltare_allocation), -1 / short_count)) # self.SetHoldings(symbol, max(-0.12, -1 / short_count)) # self.SetHoldings(symbol, -1 / short_count) # if self.Time.year == 2022 or self.Time.year == 2023: # if self.Time.year == 2023: # for symbol in self.short: # self.SetHoldings(symbol, -1 / short_count) # self.SetHoldings(symbol, -0.5 / short_count) # self.SetHoldings(self.meme_etf, 0.1) # else: # for symbol in self.short: # self.SetHoldings(symbol, -1 / short_count) self.long.clear() self.short.clear() def Selection(self): self.selection_flag = True class SymbolData(): def __init__(self, symbol, period): self.Symbol = symbol self.Price = RollingWindow[float](period) def update(self, value): self.Price.Add(value) def is_ready(self) -> bool: return self.Price.IsReady def pth(self): high_proxy = [x for x in self.Price] symbol_price = high_proxy[0] return symbol_price / max(high_proxy[21:]) def performance(self) -> float: closes = [x for x in self.Price][:21] return (closes[0] / closes[-1] - 1) # Custom fee model. class CustomFeeModel(FeeModel): def GetOrderFee(self, parameters): fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 return OrderFee(CashAmount(fee, "USD"))