Avoid Equity Bear Markets with a Market Timing Strategy
Log in to collectAcademic paper
Avoid Equity Bear Markets with a Market Timing Strategy
Ladislav Ďurian; Radovan Vojtko
- ?Quantpedia
- ?Quantpedia.com
Strategy in a nutshell
The strategy combines trend-following signals (200-day SMA, Rachev ratio, yield curve) with macroeconomic indicators (retail sales, industrial production, housing starts) to dynamically allocate between the stock market (MKT) and one-month Treasury bills (RF). The portfolio stays invested only when both trend and/or macro signals are positive, otherwise it moves to RF.
Economic rationale
By blending trend and macro signals, the strategy captures market momentum while using leading indicators to anticipate recessions. This dual approach reduces unnecessary exits, mitigates early bear market losses, and keeps the portfolio invested when the economy is strong.
Backtest performance
Annualised return6.59%
Volatility11.87%
Beta0.642
Sharpe ratio0.56
Sortino ratio0.26
Maximum drawdown-25.13%
Win rate74%
Full Python code
from AlgorithmImports import *
from typing import List
from pandas.core.frame import DataFrame
import data_tools
# endregion
class AvoidEquityBearMarketswithaMarketTimingStrategy(QCAlgorithm):
def Initialize(self) -> None:
self.SetStartDate(2001, 1, 1)
self.SetCash(100000)
self.period:int = 365
self.moving_average_period:int = 200
self.leverage:int = 5
# custom data subscription
self.market_sec:Security = self.AddData(data_tools.MarketEQ, 'MKT', Resolution.Daily)
self.t10y3m:Symbol = self.AddData(data_tools.TreasureBill, 'T10Y3M', Resolution.Daily).Symbol
self.rrsfs:Symbol = self.AddData(data_tools.RRSFS, 'RRSFS_YOY', Resolution.Daily).Symbol
self.indpro:Symbol = self.AddData(data_tools.IndustrialProduction, 'INDPRO_YOY', Resolution.Daily).Symbol
self.house_started:Symbol = self.AddData(data_tools.HouseStarted, 'HOUST_YOY', Resolution.Daily).Symbol
self.signal_market:Symbol = self.market_sec.Symbol
self.traded_market:Symbol = self.AddEquity("SPY", Resolution.Daily).Symbol
self.bill:Symbol = self.AddEquity("SHY", Resolution.Daily).Symbol
for symbol in [self.traded_market, self.bill]:
self.Securities[symbol].SetLeverage(self.leverage)
self.rebalance:bool = False
self.Schedule.On(self.DateRules.MonthEnd(self.bill), self.TimeRules.BeforeMarketClose(self.bill), self.Selection)
def OnData(self, data: Slice) -> None:
if not self.rebalance:
return
self.rebalance = False
market_last_update_date:datetime.date = data_tools.MarketEQ._last_update_date
t10y3m_last_update_date:datetime.date = data_tools.TreasureBill._last_update_date
rrsfs_last_update_date:datetime.date = data_tools.RRSFS._last_update_date
indpro_last_update_date:datetime.date = data_tools.IndustrialProduction._last_update_date
house_started_last_update_date:datetime.date = data_tools.HouseStarted._last_update_date
# call history on assets
symbols:List[Symbol] = [self.t10y3m, self.rrsfs, self.indpro, self.house_started]
history:DataFrame = self.History(symbols, self.period, Resolution.Daily)['value'].unstack(level=0)
history_market:DataFrame = self.History([self.signal_market], self.period, Resolution.Daily)['value']
if len(history.dropna().iloc[-3:]) != 3 or history.dropna().iloc[-3:].isnull().values.any() or not all(x in list(history.columns) for x in symbols):
return
if len(history) >= self.moving_average_period:
history.reset_index(inplace=True)
history.set_index('time', inplace=True)
market_returns:DataFrame = history_market.iloc[-self.moving_average_period:].pct_change()
rr:float = self.rachev_ratio(market_returns.iloc[1:], 0.5)
trend_signal:bool = False
macro_signal:bool = False
# trend signal evaluation
if history_market.iloc[-1] > history_market.iloc[-self.moving_average_period:].mean() and rr >= 1. \
and history[self.t10y3m].iloc[-1] > 0:
trend_signal = True
if (self.Securities[self.signal_market].GetLastData() and self.Time.date() >= market_last_update_date) or \
(self.Securities[self.t10y3m].GetLastData() and self.Time.date() >= t10y3m_last_update_date) or \
(self.Securities[self.rrsfs].GetLastData() and self.Time.date() >= rrsfs_last_update_date) or \
(self.Securities[self.indpro].GetLastData() and self.Time.date() >= indpro_last_update_date) or \
(self.Securities[self.house_started].GetLastData() and self.Time.date() >= house_started_last_update_date):
self.Liquidate()
return
# macro signal evaluation
if history[self.rrsfs].dropna().iloc[-2] > 0 and history[self.indpro].dropna().iloc[-2] > 0 and history[self.house_started].dropna().iloc[-2] > history[self.house_started].dropna().iloc[-3]:
macro_signal = True
if self.traded_market in data and data[self.traded_market] and self.bill in data and data[self.bill]:
traded_asset:Symbol = self.traded_market
if not self.Portfolio.Invested:
if trend_signal:
traded_asset:Symbol = self.traded_market
else:
traded_asset = self.bill
elif self.Portfolio[self.bill].Invested and trend_signal:
traded_asset = self.traded_market
elif self.Portfolio[self.traded_market].Invested and not trend_signal and not macro_signal:
traded_asset = self.bill
if not self.Portfolio[traded_asset].Invested:
self.Liquidate()
self.SetHoldings(traded_asset, 1)
def Selection(self) -> None:
self.rebalance = True
def rachev_ratio(self, df:DataFrame, alpha=0.5) -> float:
# calculate VaR for left and right tails
left_var:float = df.quantile(alpha)
right_var:float = df.quantile(1 - alpha)
# calculate Expected Shortfall for left and right tails
left_es:float = df[df <= left_var].mean()
right_es:float = df[df > right_var].mean()
# calculate the Rachev Ratio
rr:float = right_es / -left_es
return rr