
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.
ASSET CLASS: CFDs, ETFs, futures | REGION: United States | FREQUENCY:
Daily | MARKET: equities | KEYWORD: FOMC, Momentum
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 Return | 9.02% |
| Volatility | 10.56% |
| Beta | 0.388 |
| Sharpe Ratio | 0.57 |
| Sortino Ratio | N/A |
| Maximum Drawdown | N/A |
| Win Rate | 50% |
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