Overall Statistics |
Total Trades 573 Average Win 2.58% Average Loss -1.50% Compounding Annual Return 37.101% Drawdown 36.300% Expectancy 0.669 Net Profit 6139.727% Sharpe Ratio 0.982 Probabilistic Sharpe Ratio 18.616% Loss Rate 39% Win Rate 61% Profit-Loss Ratio 1.72 Alpha 0 Beta 0 Annual Standard Deviation 0.389 Annual Variance 0.151 Information Ratio 0.982 Tracking Error 0.389 Treynor Ratio 0 Total Fees $11602.40 |
''' 1.3 Intersection of ROC comparison using OUT_DAY approach by Vladimir v1.3 (with dynamic selector for fundamental factors and momentum) inspired by Peter Guenther, Tentor Testivis, Dan Whitnable, Thomas Chang, Miko M, Leandro Maia Leandro Maia setup modified by Vladimir https://www.quantconnect.com/forum/discussion/9632/amazing-returns-superior-stock-selection-strategy-superior-in-amp-out-strategy/p2/comment-29437 ''' from QuantConnect.Data.UniverseSelection import * import numpy as np import pandas as pd # -------------------------------------------------------------------------------------------------------- BONDS = ['TLT']; VOLA = 126; BASE_RET = 85; STK_MOM = 126; N_COARSE = 1000; N_FACTOR = 100; N_MOM = 5; LEV = 1.00; # -------------------------------------------------------------------------------------------------------- class Fundamental_Factors_Momentum_ROC_Comparison_OUT_DAY(QCAlgorithm): def Initialize(self): self.SetStartDate(2008, 1, 1) self.SetEndDate(2021, 2, 1) self.InitCash = 100000 self.SetCash(self.InitCash) self.MKT = self.AddEquity("SPY", Resolution.Hour).Symbol self.mkt = [] self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin) res = Resolution.Hour self.BONDS = [self.AddEquity(ticker, res).Symbol for ticker in BONDS] self.INI_WAIT_DAYS = 15 self.wait_days = self.INI_WAIT_DAYS self.GLD = self.AddEquity('GLD', res).Symbol self.SLV = self.AddEquity('SLV', res).Symbol self.XLU = self.AddEquity('XLU', res).Symbol self.XLI = self.AddEquity('XLI', res).Symbol self.UUP = self.AddEquity('UUP', res).Symbol self.DBB = self.AddEquity('DBB', res).Symbol self.pairs = [self.GLD, self.SLV, self.XLU, self.XLI, self.UUP, self.DBB] self.bull = 1 self.bull_prior = 0 self.count = 0 self.outday = (-self.INI_WAIT_DAYS+1) self.SetWarmUp(timedelta(350)) self.UniverseSettings.Resolution = res self.AddUniverse(self.CoarseFilter, self.FineFilter) self.data = {} self.RebalanceFreq = 60 self.UpdateFineFilter = 0 self.symbols = None self.RebalanceCount = 0 self.wt = {} self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.AfterMarketOpen('SPY', 30), self.daily_check) self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.AfterMarketOpen('SPY', 60), self.trade) symbols = [self.MKT] + self.pairs for symbol in symbols: self.consolidator = TradeBarConsolidator(timedelta(days=1)) self.consolidator.DataConsolidated += self.consolidation_handler self.SubscriptionManager.AddConsolidator(symbol, self.consolidator) self.history = self.History(symbols, VOLA, Resolution.Daily) if self.history.empty or 'close' not in self.history.columns: return self.history = self.history['close'].unstack(level=0).dropna() def consolidation_handler(self, sender, consolidated): self.history.loc[consolidated.EndTime, consolidated.Symbol] = consolidated.Close self.history = self.history.iloc[-VOLA:] def derive_vola_waitdays(self): sigma = 0.6 * np.log1p(self.history[[self.MKT]].pct_change()).std() * np.sqrt(252) wait_days = int(sigma * BASE_RET) period = int((1.0 - sigma) * BASE_RET) return wait_days, period def CoarseFilter(self, coarse): if not (((self.count-self.RebalanceCount) == self.RebalanceFreq) or (self.count == self.outday + self.wait_days - 1)): self.UpdateFineFilter = 0 return Universe.Unchanged self.UpdateFineFilter = 1 selected = [x for x in coarse if (x.HasFundamentalData) and (float(x.Price) > 3)] filtered = sorted(selected, key=lambda x: x.DollarVolume, reverse=True) return [x.Symbol for x in filtered[:N_COARSE]] def FineFilter(self, fundamental): if self.UpdateFineFilter == 0: return Universe.Unchanged universe_valid = [x for x in fundamental if float(x.EarningReports.BasicAverageShares.ThreeMonths) * x.Price > 1e9 and x.SecurityReference.IsPrimaryShare and x.SecurityReference.SecurityType == "ST00000001" and x.SecurityReference.IsDepositaryReceipt == 0 and x.CompanyReference.IsLimitedPartnership == 0 and x.OperationRatios.ROIC and x.OperationRatios.CapExGrowth and x.OperationRatios.FCFGrowth and x.ValuationRatios.BookValueYield and x.ValuationRatios.EVToEBITDA and x.ValuationRatios.PricetoEBITDA and x.ValuationRatios.PERatio and x.ValuationRatios.FCFYield ] returns, volatility, sharpe_ratio = self.get_momentum(universe_valid) sortedByfactor0 = sorted(universe_valid, key=lambda x: returns[x.Symbol], reverse=False) # high return or sharpe or low volatility sortedByfactor1 = sorted(universe_valid, key=lambda x: x.OperationRatios.ROIC.OneYear, reverse=False) # high ROIC sortedByfactor2 = sorted(universe_valid, key=lambda x: x.OperationRatios.CapExGrowth.ThreeYears, reverse=False) # high growth sortedByfactor3 = sorted(universe_valid, key=lambda x: x.OperationRatios.FCFGrowth.ThreeYears, reverse=False) # high growth sortedByfactor4 = sorted(universe_valid, key=lambda x: x.ValuationRatios.BookValueYield, reverse=False) # high Book Value Yield sortedByfactor5 = sorted(universe_valid, key=lambda x: x.ValuationRatios.EVToEBITDA, reverse=True) # low enterprise value to EBITDA sortedByfactor6 = sorted(universe_valid, key=lambda x: x.ValuationRatios.PricetoEBITDA, reverse=True) # low share price to EBITDA sortedByfactor7 = sorted(universe_valid, key=lambda x: x.ValuationRatios.PERatio, reverse=True) # low share price to its per-share earnings sortedByfactor8 = sorted(universe_valid, key=lambda x: x.ValuationRatios.FCFYield, reverse=False) stock_dict = {} for i, elem in enumerate(sortedByfactor0): rank0 = i rank1 = sortedByfactor1.index(elem) rank2 = sortedByfactor2.index(elem) rank3 = sortedByfactor3.index(elem) rank4 = sortedByfactor4.index(elem) rank5 = sortedByfactor5.index(elem) rank6 = sortedByfactor6.index(elem) rank7 = sortedByfactor7.index(elem) rank8 = sortedByfactor8.index(elem) score = sum([rank0*1.0, rank1*0.0, rank2*0.0, rank3*0.0, rank4*0.0, rank5*0.0, rank6*0.0, rank7*0.0, rank8*1.0]) stock_dict[elem] = score self.sorted_stock_dict = sorted(stock_dict.items(), key=lambda x:x[1], reverse=True) sorted_symbol = [x[0] for x in self.sorted_stock_dict] top = [x for x in sorted_symbol[:N_FACTOR]] self.symbols = [i.Symbol for i in top] self.UpdateFineFilter = 0 self.RebalanceCount = self.count return self.symbols def get_momentum(self, universe): symbols = [i.Symbol for i in universe] hist_df = self.History(symbols, 63, Resolution.Daily) returns = {} volatility = {} sharpe = {} for s in symbols: ret = np.log( hist_df.loc[str(s)]['close'] / hist_df.loc[str(s)]['close'].shift(1) ) returns[s] = ret.mean() * 252 volatility[s] = ret.std() * np.sqrt(252) sharpe[s] = (returns[s] - 0.03) / volatility[s] return returns, volatility, sharpe def OnSecuritiesChanged(self, changes): addedSymbols = [] for security in changes.AddedSecurities: addedSymbols.append(security.Symbol) if security.Symbol not in self.data: self.data[security.Symbol] = SymbolData(security.Symbol, STK_MOM, self) if len(addedSymbols) > 0: history = self.History(addedSymbols, 1 + STK_MOM, Resolution.Daily).loc[addedSymbols] for symbol in addedSymbols: try: self.data[symbol].Warmup(history.loc[symbol]) except: self.Debug(str(symbol)) continue def calc_return(self, stocks): ret = {} for symbol in stocks: try: ret[symbol] = self.data[symbol].Roc.Current.Value except: self.Debug(str(symbol)) continue df_ret = pd.DataFrame.from_dict(ret, orient='index') df_ret.columns = ['return'] sort_return = df_ret.sort_values(by = ['return'], ascending = False) return sort_return def daily_check(self): self.wait_days, period = self.derive_vola_waitdays() r = self.history.pct_change(period).iloc[-1] self.bear = ((r[self.SLV] < r[self.GLD]) and (r[self.XLI] < r[self.XLU]) and (r[self.DBB] < r[self.UUP])) if self.bear: self.bull = False self.outday = self.count if (self.count >= self.outday + self.wait_days): self.bull = True self.wt_stk = LEV if self.bull else 0 self.wt_bnd = 0 if self.bull else LEV def trade(self): if self.bear: self.trade_out() if (self.bull and not self.bull_prior) or (self.bull and (self.count==self.RebalanceCount)): self.trade_in() self.bull_prior = self.bull self.count += 1 def trade_out(self): for sec in self.BONDS: self.wt[sec] = self.wt_bnd/len(self.BONDS) for sec in self.Portfolio.Keys: if sec not in self.BONDS: self.wt[sec] = 0 for sec, weight in self.wt.items(): if weight == 0 and self.Portfolio[sec].IsLong: self.Liquidate(sec) for sec, weight in self.wt.items(): if weight != 0: self.SetHoldings(sec, weight) def trade_in(self): if self.symbols is None: return output = self.calc_return(self.symbols) stocks = output.iloc[:N_MOM].index for sec in self.Portfolio.Keys: if sec not in stocks: self.wt[sec] = 0 for sec in stocks: self.wt[sec] = self.wt_stk/N_MOM for sec, weight in self.wt.items(): self.SetHoldings(sec, weight) def OnEndOfDay(self): mkt_price = self.Securities[self.MKT].Close self.mkt.append(mkt_price) mkt_perf = self.InitCash * self.mkt[-1] / self.mkt[0] self.Plot('Strategy Equity', self.MKT, mkt_perf) account_leverage = self.Portfolio.TotalHoldingsValue / self.Portfolio.TotalPortfolioValue self.Plot('Holdings', 'leverage', round(account_leverage, 2)) self.Plot('Holdings', 'Target Leverage', LEV) class SymbolData(object): def __init__(self, symbol, roc, algorithm): self.Symbol = symbol self.Roc = RateOfChange(roc) self.algorithm = algorithm self.consolidator = algorithm.ResolveConsolidator(symbol, Resolution.Daily) algorithm.RegisterIndicator(symbol, self.Roc, self.consolidator) def Warmup(self, history): for index, row in history.iterrows(): self.Roc.Update(index, row['close'])