| 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))