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.

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 Return17.36%
Volatility20.61%
Beta0.179
Sharpe Ratio0.84
Sortino Ratio0.124
Maximum DrawdownN/A
Win Rate49%

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

Leave a Reply

Discover more from Quant Buffet

Subscribe now to keep reading and get access to the full archive.

Continue reading