Overall Statistics |
Total Trades 391 Average Win 3.87% Average Loss -1.04% Compounding Annual Return 30.411% Drawdown 19.400% Expectancy 1.864 Net Profit 3017.883% Sharpe Ratio 1.665 Probabilistic Sharpe Ratio 97.293% Loss Rate 39% Win Rate 61% Profit-Loss Ratio 3.73 Alpha 0.254 Beta 0.058 Annual Standard Deviation 0.156 Annual Variance 0.024 Information Ratio 0.691 Tracking Error 0.235 Treynor Ratio 4.468 Total Fees $18056.11 |
""" Based on 'In & Out' strategy by Peter Guenther 4 Oct 2020 expanded/inspired by Tentor Testivis, Dan Whitnable, Vladimir, and Thomas Chang. """ import numpy as np class DualMomentumInOut(QCAlgorithm): def Initialize(self): self.SetStartDate(2008, 1, 1) # self.SetEndDate(2020, 11, 27) self.cap = 100000 self.BND1 = self.AddEquity('TLT', Resolution.Minute).Symbol self.BND2 = self.AddEquity('TLH', Resolution.Minute).Symbol self.STK1 = self.AddEquity('QQQ', Resolution.Minute).Symbol self.STK2 = self.AddEquity('FDN', Resolution.Minute).Symbol self.MKT = self.AddEquity('SPY', Resolution.Daily).Symbol self.XLI = self.AddEquity('XLI', Resolution.Daily).Symbol self.XLU = self.AddEquity('XLU', Resolution.Daily).Symbol self.SLV = self.AddEquity('SLV', Resolution.Daily).Symbol self.GLD = self.AddEquity('GLD', Resolution.Daily).Symbol self.FXA = self.AddEquity('FXA', Resolution.Daily).Symbol self.FXF = self.AddEquity('FXF', Resolution.Daily).Symbol self.DBB = self.AddEquity('DBB', Resolution.Daily).Symbol self.IGE = self.AddEquity('IGE', Resolution.Daily).Symbol self.SHY = self.AddEquity('SHY', Resolution.Daily).Symbol self.UUP = self.AddEquity('UUP', Resolution.Daily).Symbol self.FORPAIRS = [self.XLI, self.XLU, self.SLV, self.GLD, self.FXA, self.FXF] self.SIGNALS = [self.XLI, self.DBB, self.IGE, self.SHY, self.UUP] self.pairlist = ['S_G', 'I_U', 'A_F'] self.INI_WAIT_DAYS = 15 self.mom = 126 self.excl = 5 self.BNDselect = self.BND1 self.STKselect = self.STK1 self.HLD_OUT = {self.BNDselect: 1} self.HLD_IN = {self.STKselect: 1} self.bull = 1 self.count = 0 self.outday = 0 self.spy = [] self.wait_days = self.INI_WAIT_DAYS self.SetWarmUp(timedelta(126)) self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.AfterMarketOpen('SPY', 1), self.calculate_signal) self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.AfterMarketOpen('SPY', 120), self.rebalance_when_out_of_the_market) self.Schedule.On(self.DateRules.WeekEnd(), self.TimeRules.AfterMarketOpen('SPY', 121), self.rebalance_when_in_the_market) self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.BeforeMarketClose('SPY', 0), self.record_vars) symbols = self.SIGNALS + [self.MKT] + self.FORPAIRS for symbol in symbols: self.consolidator = TradeBarConsolidator(timedelta(days = 1)) self.consolidator.DataConsolidated += self.consolidation_handler self.SubscriptionManager.AddConsolidator(symbol, self.consolidator) self.lookback = 252 self.history = self.History(symbols, self.lookback, Resolution.Daily) if self.history.empty or 'close' not in self.history.columns: return self.history = self.history['close'].unstack(level=0).dropna() self.update_history_shift() def consolidation_handler(self, sender, consolidated): self.history.loc[consolidated.EndTime, consolidated.Symbol] = consolidated.Close self.history = self.history.iloc[-self.lookback:] self.update_history_shift() def update_history_shift(self): self.history_shift_mean = self.history.shift(55).rolling(11).mean() def Returns(self, symbol, period, excl): prices = self.History(symbol, TimeSpan.FromDays(period + excl), Resolution.Daily).close return prices[-excl] / prices[0] def calculate_signal(self): mom = (self.history / self.history_shift_mean - 1) mom[self.UUP] = mom[self.UUP] * (-1) mom['S_G'] = mom[self.SLV] - mom[self.GLD] mom['I_U'] = mom[self.XLI] - mom[self.XLU] mom['A_F'] = mom[self.FXA] - mom[self.FXF] pctl = np.nanpercentile(mom, 1, axis=0) extreme = mom.iloc[-1] < pctl self.wait_days = int( max(0.50 * self.wait_days, self.INI_WAIT_DAYS * max(1, np.where((mom[self.GLD].iloc[-1]>0) & (mom[self.SLV].iloc[-1]<0) & (mom[self.SLV].iloc[-2]>0), self.INI_WAIT_DAYS, 1), np.where((mom[self.XLU].iloc[-1]>0) & (mom[self.XLI].iloc[-1]<0) & (mom[self.XLI].iloc[-2]>0), self.INI_WAIT_DAYS, 1), np.where((mom[self.FXF].iloc[-1]>0) & (mom[self.FXA].iloc[-1]<0) & (mom[self.FXA].iloc[-2]>0), self.INI_WAIT_DAYS, 1) )) ) adjwaitdays = min(60, self.wait_days) # self.Debug('{}'.format(self.wait_days)) if (extreme[self.SIGNALS + self.pairlist]).any(): self.bull = False self.outday = self.count if self.count >= self.outday + adjwaitdays: self.bull = True self.count += 1 self.Plot("In Out", "in_market", int(self.bull)) self.Plot("In Out", "num_out_signals", extreme[self.SIGNALS + self.pairlist].sum()) self.Plot("Wait Days", "waitdays", adjwaitdays) if self.Returns(self.BND1, self.mom, self.excl) < self.Returns(self.BND2, self.mom, self.excl): self.BNDselect = self.BND2 elif self.Returns(self.BND1, self.mom, self.excl) > self.Returns(self.BND2, self.mom, self.excl): self.BNDselect = self.BND1 if self.Returns(self.STK1, self.mom, self.excl) < self.Returns(self.STK2, self.mom, self.excl): self.STKselect = self.STK2 elif self.Returns(self.STK1, self.mom, self.excl) > self.Returns(self.STK2, self.mom, self.excl): self.STKselect = self.STK1 self.HLD_IN = {self.STKselect: 1} self.HLD_OUT = {self.BNDselect: 1} def rebalance_when_out_of_the_market(self): if not self.bull: self.trade({**dict.fromkeys(self.HLD_IN, 0), **self.HLD_OUT}) def rebalance_when_in_the_market(self): if self.bull: self.trade({**self.HLD_IN, **dict.fromkeys(self.HLD_OUT, 0)}) self.Log(f"TotalPortfolioValue: {self.Portfolio.TotalPortfolioValue}, TotalMarginUsed: {self.Portfolio.TotalMarginUsed}, MarginRemaining: {self.Portfolio.MarginRemaining}, Cash: {self.Portfolio.Cash}") for key in sorted(self.Portfolio.keys()): if self.Portfolio[key].Quantity > 0.0: self.Log(f"Symbol/Qty: {key} / {self.Portfolio[key].Quantity}, Avg: {self.Portfolio[key].AveragePrice}, Curr: { self.Portfolio[key].Price}, Profit($): {self.Portfolio[key].UnrealizedProfit}") def trade(self, weight_by_sec): if self.Portfolio.Invested: for symbol in self.Portfolio.Keys: if symbol not in weight_by_sec: self.Liquidate(symbol) buys = [] for sec, weight in weight_by_sec.items(): if not self.CurrentSlice.ContainsKey(sec) or self.CurrentSlice[sec] is None: continue cond1 = weight == 0 and self.Portfolio[sec].IsLong cond2 = weight > 0 and not self.Portfolio[sec].Invested if cond1 or cond2: quantity = self.CalculateOrderQuantity(sec, weight) if quantity > 0: buys.append((sec, quantity)) elif quantity < 0: self.Order(sec, quantity) for sec, quantity in buys: self.Order(sec, quantity) def record_vars(self): hist = self.History([self.MKT], 2, Resolution.Daily)['close'].unstack(level= 0).dropna() self.spy.append(hist[self.MKT].iloc[-1]) spy_perf = self.spy[-1] / self.spy[0] * self.cap self.Plot("Strategy Equity", "SPY", spy_perf) account_leverage = self.Portfolio.TotalHoldingsValue / self.Portfolio.TotalPortfolioValue self.Plot('Holdings', 'leverage', round(account_leverage, 1))