The strategy involves trading the CRSP value-weighted index by going long after expansionary FOMC decisions and short after contractionary ones, holding positions for 15 days post-announcement.

I. STRATEGY IN A NUTSHELL

The strategy trades the CRSP value-weighted index around FOMC announcements. It goes long following expansionary policy decisions and short after contractionary decisions, holding positions for 15 days post-announcement.

II. ECONOMIC RATIONALE

The strategy exploits time-series momentum triggered by FOMC policy changes. Market-wide reactions are amplified during periods of high uncertainty (proxied by the VIX). Expansionary announcements produce stronger positive drifts under high VIX conditions, reflecting the persistence of momentum across industries and indices.

III. SOURCE PAPER

Monetary Momentum [Click to Open PDF]

Andreas Neuhierl and Michael Weber

<Abstract>

We document a large return drift around monetary policy announcements by the Federal Open Market Committee (FOMC). Stock returns start drifting up 25 days before expansionary monetary policy surprises, whereas they decrease before contractionary surprises. The cumulative return difference across expansionary and contractionary policy decisions amounts to 2.5% until the day of the policy decision and continues to increase to more than 4.5% 15 days after the meeting. The drift is more pronounced during periods of high uncertainty, it is a market-wide phenomenon, and it is present in all industries and many international equity markets. Standard returns factors and time-series momentum do not span the return drift around FOMC policy decisions. A simple trading strategy exploiting the drift around FOMC meetings increases Sharpe ratios relative to a buy-and-hold investment by a factor of 4. The cumulative returns before FOMC meetings significantly predict the subsequent policy surprise.

IV. BACKTEST PERFORMANCE

Annualised Return9.02%
Volatility10.56%
Beta0.388
Sharpe Ratio0.57
Sortino RatioN/A
Maximum DrawdownN/A
Win Rate50%

V. FULL PYTHON CODE

from AlgorithmImports import *
class MonetaryFOMCMomentuminStocks(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(1998, 1, 1)
        self.SetCash(100000)
        
        data = self.AddEquity("SPY", Resolution.Minute)
        data.SetFeeModel(CustomFeeModel())
        self.symbol = data.Symbol
        self.fed_funds = self.AddData(FederalFundsTargetRate, 'FederalFundsTargetRate', Resolution.Daily).Symbol
        
        # import quandl federal rate data
        self.target_rate = self.AddData(QuandlValue, 'FRED/DFEDTARL', Resolution.Daily).Symbol
        
        # Federal funds target rate history.
        self.rate_history = RollingWindow[float](2)
        self.days_to_liquidate = -1
        
        self.Schedule.On(self.DateRules.EveryDay(self.symbol), self.TimeRules.AfterMarketOpen(self.symbol, 1), self.SPYLiquidate)
    
    def OnData(self, data):
        # Until 2009 we are taking data from Quantpedia,
        # then we take data from Quandl
        if self.fed_funds in data and data[self.fed_funds]:
            rate = data[self.fed_funds].Value
            self.rate_history.Add(rate)
            self.Trade() # Trade spy
            
        elif self.target_rate in data and data[self.target_rate] and self.Time.year > 2008:
            rate = data[self.target_rate].Value
            self.rate_history.Add(rate)
            self.Trade() # Trade spy
            
    def Trade(self):
        if self.rate_history.IsReady:
            rates = [x for x in self.rate_history]
            previous_rate = rates[-1]
            current_rate = rates[0]
            if current_rate > previous_rate:    # contractionary policy decision
                self.SetHoldings(self.symbol, -1)
            elif current_rate < previous_rate:  # expansionary policy decision
                self.SetHoldings(self.symbol, 1)
            self.days_to_liquidate = 15 
            
    def SPYLiquidate(self):
        if self.Portfolio[self.symbol].Invested:
            self.days_to_liquidate -= 1
            
            if self.days_to_liquidate == 0:
                self.Liquidate(self.symbol)
 
# Quandl "value" data
class QuandlValue(PythonQuandl):
    def __init__(self):
        self.ValueColumnName = 'Value' 
           
# Custom fee model.
class CustomFeeModel(FeeModel):
    def GetOrderFee(self, parameters):
        fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00001
        return OrderFee(CashAmount(fee, "USD"))
class FederalFundsTargetRate(PythonData):
    def GetSource(self, config, date, isLiveMode):
        return SubscriptionDataSource("data.quantpedia.com/backtesting_data/economic/federal_funds_target_rate_history.csv", SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
    def Reader(self, config, line, date, isLiveMode):
        data = FederalFundsTargetRate()
        data.Symbol = config.Symbol
        
        if not line[0].isdigit(): return None
        split = line.split(';')
        
        data.Time = datetime.strptime(split[0], "%Y-%m-%d") + timedelta(days=1)
        data['rate'] = float(split[1])
        data.Value = float(split[1])
        return data

VI. Backtest Performance

Leave a Reply

Discover more from Quant Buffet

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

Continue reading