Overall Statistics |
Total Trades 162 Average Win 4.58% Average Loss -1.18% Compounding Annual Return 22.462% Drawdown 16.500% Expectancy 2.720 Net Profit 1296.248% Sharpe Ratio 1.408 Probabilistic Sharpe Ratio 87.659% Loss Rate 24% Win Rate 76% Profit-Loss Ratio 3.88 Alpha 0.18 Beta 0.125 Annual Standard Deviation 0.137 Annual Variance 0.019 Information Ratio 0.447 Tracking Error 0.212 Treynor Ratio 1.54 Total Fees $1421.64 |
''' Looks at trailing return for various indicators to predict bear and bull market conditions. See: https://www.quantconnect.com/forum/discussion/10246/intersection-of-roc-comparison-using-out-day-approach/p1 See: https://drive.google.com/file/d/1JE-2Ter1TWuQvZC12vC892c2wLPHAcUS/view ''' import numpy as np # Will invest in these tickers when bullish conditions are predicted. BULLISH_TICKERS = ['SPY', 'QQQ'] # Will invest in these tickers when bearish conditions are predicted. BEARISH_TICKERS = ['TLT','TLH'] # How many samples (1x sample per day) to record for volatility calculations. HISTORICAL_SAMPLE_SIZE = 126 # Maximum amount of portfolio value (ratio) that will be invested at once. MAXIMUM_PORTFOLIO_USAGE = 0.99 # Initial cash, USD. STARTING_CASH = 100000 class BullishBearishInOut(QCAlgorithm): def Initialize(self): self.SetStartDate(2008, 1, 1) # self.SetEndDate(2021, 1, 1) self.SetCash(STARTING_CASH) self.bullish_symbols = [self.AddEquity(ticker, Resolution.Minute).Symbol for ticker in BULLISH_TICKERS] self.bearish_symbols = [self.AddEquity(ticker, Resolution.Minute).Symbol for ticker in BEARISH_TICKERS] # Default should be set to 85. A larger time constant means this algorithm will trade less frequenty. # Time constant is used to calucate two things: # lookback_n_days: How many days to look back to calculate trailing returns for the indicators. # settling_n_days: How many days to wait after last downturn prediction before investing in bullish tickers. # # Assuming an annualized volatility of 0.02: # time_constant = 50 -> lookback_n_days: 40, settling_n_days: 10 # time_constant = 85 -> lookback_n_days: 65, settling_n_days: 17 # time_constat = 100 -> lookback_n_days: 80, settling_n_days: 20 # time_constant = 200 -> lookback_n_days: 160, settling_n_days: 400 self.time_constant = float(self.GetParameter("time_constant")) self.SLV = self.AddEquity('SLV', Resolution.Daily).Symbol self.GLD = self.AddEquity('GLD', Resolution.Daily).Symbol self.XLI = self.AddEquity('XLI', Resolution.Daily).Symbol self.XLU = self.AddEquity('XLU', Resolution.Daily).Symbol self.DBB = self.AddEquity('DBB', Resolution.Daily).Symbol self.UUP = self.AddEquity('UUP', Resolution.Daily).Symbol self.MKT = self.AddEquity('SPY', Resolution.Daily).Symbol indicators = [self.SLV, self.GLD, self.XLI, self.XLU, self.DBB, self.UUP] self.bull_market_suspected = True self.day_count = 0 self.last_downturn_indicator_count = 0 self.desired_weight = {} self.initial_market_price = None self.SetWarmUp(timedelta(350)) self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.AfterMarketOpen('SPY', 60), self.daily_check) self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.AfterMarketOpen('SPY', 120), self.trade) symbols = [self.MKT] + indicators 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, HISTORICAL_SAMPLE_SIZE + 1, 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): """Adds close price of this symbol to history.""" self.history.loc[consolidated.EndTime, consolidated.Symbol] = consolidated.Close def daily_check(self): """Looks to see if a market downturn is supected. If so, sets self.bull_market_suspected to false. Clears the indicator after a certain number of days have passed. """ # Trim history so that it includes only the dates we need. # This protectes history size from growing with each passing day. self.history = self.history.iloc[-(HISTORICAL_SAMPLE_SIZE + 1):] # Calculate the anualized volatility. # self.history[[self.MKT]]: The last HISTORICAL_SAMPLE_SIZE + 1 days of closing prices # .pct_change(): HISTORICAL_SAMPLE_SIZE daily price changes # .std(): standard deviation of price changes # * np.sqrt(252): Turn from daily to anual volatility. # Details: https://www.investopedia.com/ask/answers/021015/how-can-you-calculate-volatility-excel.asp annualized_volatility = self.history[[self.MKT]].pct_change().std() * np.sqrt(252) # Calculate setting and lookback days. # For details see parameter sensativity discussion: https://quantopian-archive.netlify.app/forum/threads/new-strategy-in-and-out.html settling_n_days = int(annualized_volatility * self.time_constant) lookback_n_days = int((1.0 - annualized_volatility) * self.time_constant) # Calculate the percentage change over the lookback days. percent_change_over_lookback = self.history.pct_change(lookback_n_days).iloc[-1] market_downturn_suspected = ( # Gold (GLD) has increased more than silver (SLV) percent_change_over_lookback[self.SLV] < percent_change_over_lookback[self.GLD] and # Utilities sector (XLU) has increased more than industrial sector (XLI) percent_change_over_lookback[self.XLI] < percent_change_over_lookback[self.XLU] and # Dollar bullish (UUP) has increased more than base metals fund (DBB) percent_change_over_lookback[self.DBB] < percent_change_over_lookback[self.UUP]) if market_downturn_suspected: self.bull_market_suspected = False self.last_downturn_indicator_count = self.day_count # Wait settling_period from the previous market_downturn_suspected signal before flagging a bull market. if self.day_count >= self.last_downturn_indicator_count + settling_n_days: self.bull_market_suspected = True self.day_count += 1 def trade(self): """Buys stocks or bonds, as determined by bull indicator. If self.bull_market_suspected: Buys all bullish securities at an equal weight. Sells all bearish securities. If not self.bull_market_suspected: Buys all bearish securities at an equal weight. Sells all bullish securities. Does not trade unless self.bull_market_suspected changes. """ if self.bull_market_suspected: bullish_security_weight = MAXIMUM_PORTFOLIO_USAGE / len(self.bullish_symbols) bearish_security_weight = 0 else: bullish_security_weight = 0 bearish_security_weight = MAXIMUM_PORTFOLIO_USAGE / len(self.bearish_symbols); for bullish_symbol in self.bullish_symbols: self.desired_weight[bullish_symbol] = bullish_security_weight for bearish_symbol in self.bearish_symbols: self.desired_weight[bearish_symbol] = bearish_security_weight for security, weight in self.desired_weight.items(): if weight == 0 and self.Portfolio[security].IsLong: self.Liquidate(security) is_held_and_should_sell = weight == 0 and self.Portfolio[security].IsLong is_not_held_and_should_buy = weight != 0 and not self.Portfolio[security].Invested if is_held_and_should_sell or is_not_held_and_should_buy: self.SetHoldings(security, weight) def OnEndOfDay(self): """Plots handy information. Benchmark progress on main plot "Strategy Equity" Portfolio holdings on smaller plot "Holdings" Does not transact. Only affects plots. """ market_price = self.Securities[self.MKT].Close if self.initial_market_price is None: self.initial_market_price = market_price self.Plot("Strategy Equity", "SPY", STARTING_CASH * market_price / self.initial_market_price) account_leverage = self.Portfolio.TotalHoldingsValue / self.Portfolio.TotalPortfolioValue self.Plot('Holdings', 'leverage', round(account_leverage, 1)) actual_weight = {} for sec, weight in self.desired_weight.items(): actual_weight[sec] = round(self.ActiveSecurities[sec].Holdings.Quantity * self.Securities[sec].Price / self.Portfolio.TotalPortfolioValue,4) self.Plot('Holdings', self.Securities[sec].Symbol, round(actual_weight[sec], 3))