Overall Statistics |
Total Trades 219 Average Win 6.19% Average Loss -1.96% Compounding Annual Return 21.358% Drawdown 36.000% Expectancy 1.326 Net Profit 1130.662% Sharpe Ratio 1.161 Probabilistic Sharpe Ratio 55.889% Loss Rate 44% Win Rate 56% Profit-Loss Ratio 3.16 Alpha 0.18 Beta 0.502 Annual Standard Deviation 0.207 Annual Variance 0.043 Information Ratio 0.584 Tracking Error 0.206 Treynor Ratio 0.478 Total Fees $5511.89 |
""" DUAL MOMENTUM-IN OUT v2 by Vladimir https://www.quantconnect.com/forum/discussion/9597/the-in-amp-out-strategy-continued-from-quantopian/p3/comment-28146 inspired by Peter Guenther, Tentor Testivis, Dan Whitnable, Thomas Chang and T Smith. """ import numpy as np class DualMomentumInOut(QCAlgorithm): def Initialize(self): self.SetStartDate(2008, 1, 1) # self.SetEndDate(2020, 11, 27) self.cap = 100000 self.STK1 = self.AddEquity('QQQ', Resolution.Hour).Symbol self.STK2 = self.AddEquity('FDN', Resolution.Hour).Symbol self.BND1 = self.AddEquity('TLT', Resolution.Hour).Symbol self.BND2 = self.AddEquity('TLH', Resolution.Hour).Symbol self.ASSETS = [self.STK1, self.STK2, self.BND1, self.BND2] self.MKT = self.AddEquity('SPY', Resolution.Hour).Symbol self.UUP = self.AddEquity('UUP', Resolution.Hour).Symbol self.SIGNALS = [self.UUP] self.INI_WAIT_DAYS = 15 self.SHIFT = 55 self.MEAN = 11 self.RET = 126 self.EXCL = 5 self.leveragePercentage = 101 self.selected_bond = self.BND1 self.selected_stock = self.STK1 self.init = 0 self.bull = 1 self.count = 0 self.outday = 0 self.in_stock = 0 self.spy = [] self.wait_days = self.INI_WAIT_DAYS self.wt = {} self.real_wt = {} self.SetWarmUp(timedelta(126)) self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.AfterMarketOpen('SPY', 100), self.calculate_signal) self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.Every(TimeSpan.FromMinutes(60)), self.trade_out) self.Schedule.On(self.DateRules.WeekEnd(), self.TimeRules.AfterMarketOpen('SPY', 120), self.trade_in) self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.BeforeMarketClose('SPY', 0), self.record_vars) symbols = self.SIGNALS + [self.MKT] 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(self.SHIFT).rolling(self.MEAN).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) pctl = np.nanpercentile(mom, 5, axis=0) extreme = mom.iloc[-1] < pctl self.wait_days = self.INI_WAIT_DAYS adjwaitdays = min(60, self.wait_days) # self.Debug('{}'.format(self.wait_days)) if extreme[self.SIGNALS].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].sum()) self.Plot("Wait Days", "waitdays", adjwaitdays) if self.returns(self.BND1, self.RET, self.EXCL) < self.returns(self.BND2, self.RET, self.EXCL): self.selected_bond = self.BND2 elif self.returns(self.BND1, self.RET, self.EXCL) > self.returns(self.BND2, self.RET, self.EXCL): self.selected_bond = self.BND1 if self.returns(self.STK1, self.RET, self.EXCL) < self.returns(self.STK2, self.RET, self.EXCL): self.selected_stock = self.STK2 elif self.returns(self.STK1, self.RET, self.EXCL) > self.returns(self.STK2, self.RET, self.EXCL): self.selected_stock = self.STK1 def trade_out(self): if not self.bull: for sec in self.ASSETS: self.wt[sec] = 0.99 if sec is self.selected_bond else 0 if sec is self.selected_bond else 0 self.trade() def trade_in(self): if self.bull: for sec in self.ASSETS: self.wt[sec] = 0.99 if sec is self.selected_stock else 0 self.trade() def trade(self): for sec, weight in self.wt.items(): if weight == 0 and self.Portfolio[sec].IsLong: self.Liquidate(sec) cond1 = weight == 0 and self.Portfolio[sec].IsLong cond2 = weight > 0 and not self.Portfolio[sec].Invested if cond1 or cond2: self.SetHoldings(sec, weight) 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)) for sec, weight in self.wt.items(): self.real_wt[sec] = round(self.ActiveSecurities[sec].Holdings.Quantity * self.Securities[sec].Price / self.Portfolio.TotalPortfolioValue,4) self.Plot('Holdings', self.Securities[sec].Symbol, round(self.real_wt[sec], 3))