
The strategy invests in the worst-performing stocks (losers) and shorts the best-performing stocks (winners) when economic policy uncertainty exceeds the median, rebalancing monthly with equal weighting.
ASSET CLASS: stocks | REGION: United States | FREQUENCY:
Monthly | MARKET: equities | KEYWORD: Reversal, Uncertainty
I. STRATEGY IN A NUTSHELL
U.S. stocks are sorted monthly by prior-month returns. During high economic policy uncertainty (EPU), buy the loser quintile (bottom 20%) and short the winner quintile (top 20%). Portfolios are equally weighted and rebalanced monthly, capturing short-term momentum reversals in uncertain times.
II. ECONOMIC RATIONALE
Short-term reversals arise from investor overreaction and liquidity effects. High EPU amplifies mispricing due to overconfidence and reduced market liquidity, causing exaggerated price deviations that subsequently revert to fundamentals, creating exploitable reversals.
III. SOURCE PAPER
Economic Policy Uncertainty and Short-term Reversals [Click to Open PDF]
Andy Chun Wai Chui, School of Accounting and Finance, Faculty of Business, The Hong Kong Polytechnic University
<Abstract>
This study finds that short-term reversals become more profound in the current month when economic policy uncertainty is larger in the prior month. There is evidence that the economic policy uncertainty influences return reversals through the liquidity channel. Short-term reversal profits are also positively related to the VIX index, the Baker-Wurgler (2007) investor sentiment index, and the Aruoba-Diebold-Scotti (2009) business conditions index in the prior month. Though, the predictability of the latter two indexes is less robust. However, adding these indexes and other variables does not weaken the relationship between economic policy uncertainty and return reversals.
IV. BACKTEST PERFORMANCE
| Annualised Return | 17.36% |
| Volatility | 20.61% |
| Beta | 0.179 |
| Sharpe Ratio | 0.84 |
| Sortino Ratio | 0.124 |
| Maximum Drawdown | N/A |
| Win Rate | 49% |
V. FULL PYTHON CODE
from AlgorithmImports import *
from dateutil.relativedelta import relativedelta
from pandas.core.series import Series
from pandas.core.frame import dataframe
#endregion
class ShortTermReversalAndHighUncertaintyPeriods(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100_000)
self.period: int = 21 # need n daily prices
self.prices: Dict[Symbol, RollingWindow] = {}
self.weight: Dict[Symbol, float] = {}
self.pu_recent_values: List[float] = []
market: Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
self.policy_uncertainty_symbol: Symbol = self.AddData(QuantpediaPolicyUncertainty, 'US_ECONOMIC_POLICY_UNCERTAINTY', Resolution.Daily).Symbol
self.quantile:int = 5
self.leverage:int = 5
self.min_share_price:float = 5.
self.exchange_codes:List[str] = ['NYS', 'NAS', 'ASE']
self.fundamental_count:int = 1_000
self.fundamental_sorting_key = lambda x: x.DollarVolume
self.selection_flag: bool = False
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.FundamentalSelectionFunction)
self.settings.daily_precise_end_time = False
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.Schedule.On(self.DateRules.MonthStart(market), self.TimeRules.BeforeMarketClose(market, 0), self.Selection)
def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
for security in changes.AddedSecurities:
security.SetFeeModel(CustomFeeModel())
security.SetLeverage(self.leverage)
def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
# update daily prices
for stock in fundamental:
symbol: Symbol = stock.Symbol
if symbol in self.prices:
self.prices[symbol].Add(stock.AdjustedPrice)
# rebalance monthly
if not self.selection_flag or len(self.pu_recent_values) == 0:
return Universe.Unchanged
if self.Time.date() >= QuantpediaPolicyUncertainty.get_last_update_date():
return Universe.Unchanged
policy_uncertainty_median: float = np.median(self.pu_recent_values)
prev_month_policy_uncertainty: float = self.pu_recent_values[-1]
# trade only if previous month policy uncertainty is greater than it's median
if prev_month_policy_uncertainty < policy_uncertainty_median:
return Universe.Unchanged
# filter top n U.S. stocks
selected: List[Fundamental] = [
x for x in fundamental if x.HasFundamentalData
and x.Market == 'usa'
and x.Price > self.min_share_price
and x.SecurityReference.ExchangeId in self.exchange_codes
]
if len(selected) > self.fundamental_count:
selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]]
performance: Dict[Symbol, float] = {}
# warm up stock prices
for stock in selected:
symbol: Symbol = stock.Symbol
if symbol not in self.prices:
self.prices[symbol] = RollingWindow[float](self.period)
history: dataframe = self.History(symbol, self.period, Resolution.Daily)
if history.empty:
continue
closes: Series = history.loc[symbol].close
for _, close in closes.items():
self.prices[symbol].Add(close)
if self.prices[symbol].IsReady:
performance_value: float = self.prices[symbol][0] / self.prices[symbol][self.prices[symbol].Count - 1] - 1
# store stock's performance value keyed by stock's symbol
performance[symbol] = performance_value
# there has to be enough stocks for quintile selection
if len(performance) < self.quantile:
return Universe.Unchanged
# quintile selection
quintile: int = int(len(performance) / self.quantile)
sorted_by_id: List[Symbol] = [x[0] for x in sorted(performance.items(), key=lambda item: item[1])]
# Buy the losers and sell the winners when the economic policy uncertainty index of Baker, Bloom, and Davis (2006) for the US in month t-1 is higher than the median of this index since 1985
short: List[Symbol] = sorted_by_id[-quintile:] # winners
long: List[Symbol] = sorted_by_id[:quintile] # losers
# calculate weights for long and short part
for i, portfolio in enumerate([long, short]):
for symbol in portfolio:
self.weight[symbol] = ((-1) ** i) / len(portfolio)
return list(self.weight.keys())
def OnData(self, slice: Slice) -> None:
# update policy uncertainty values each month
if self.policy_uncertainty_symbol in slice and slice[self.policy_uncertainty_symbol]:
policy_uncertainty_value: float = slice[self.policy_uncertainty_symbol].Value
self.pu_recent_values.append(policy_uncertainty_value)
# rebalance monthly
if not self.selection_flag:
return
self.selection_flag = False
# trade execution
portfolio: List[PortfolioTarget] = [PortfolioTarget(symbol, w) for symbol, w in self.weight.items() if symbol in slice and slice[symbol]]
self.SetHoldings(portfolio, True)
self.weight.clear()
def Selection(self) -> None:
self.selection_flag = True
# Quantpedia data.
# NOTE: IMPORTANT: Data order must be ascending (datewise)
class QuantpediaPolicyUncertainty(PythonData):
_last_update_date: datetime.date = datetime(1,1,1).date()
@staticmethod
def get_last_update_date() -> datetime.date:
return QuantpediaPolicyUncertainty._last_update_date
def GetSource(self, config, date, isLiveMode):
return SubscriptionDataSource("data.quantpedia.com/backtesting_data/index/{0}.csv".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
def Reader(self, config, line, date, isLiveMode):
data = QuantpediaPolicyUncertainty()
data.Symbol = config.Symbol
if not line[0].isdigit():
return None
split = line.split(';')
date = split[0] + '-1'
data.Time = datetime.strptime(date, "%Y-%m-%d") + timedelta(days=1) + relativedelta(months=1)
data.Value = float(split[1])
if data.Time.date() > QuantpediaPolicyUncertainty._last_update_date:
QuantpediaPolicyUncertainty._last_update_date = data.Time.date()
return data
# Custom fee model
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))