Fed Rate Calendar Fetch
Fetch and parse the Federal Reserve meeting calendar, FOMC rate decision announcements, meeting minutes, and Summary of Economic Projections release schedule to systematically anticipate and trade around monetary policy events that significantly impact all risk assets including crypto.
Fed Rate Decision Calendar Fetch — Macro & Cross-Asset
Category: Macro & Cross-Asset | Subcategory: Data
What This Notebook Does
FOMC (Federal Open Market Committee) meetings are the most market-moving scheduled events in global finance. In the crypto-macro era, BTC regularly moves ±5–10% on Fed announcement days. This notebook builds a structured Fed calendar with market impact analysis.
This notebook:
- Scrapes the historical FOMC meeting schedule and rate decisions from the Federal Reserve website
- Fetches the actual rate decision for each meeting (hike / cut / hold) from FRED
- Builds a clean event calendar with decision type, basis-point change, and press conference flag
- Measures crypto price impact in the ±3 day window around each decision
- Classifies meeting surprises (actual vs market-implied rate from Fed Funds futures)
- Exports a structured FOMC calendar DataFrame for use in event-driven strategy notebooks
FOMC Basics for Crypto Traders
| Decision | Historical Crypto Impact |
|---|---|
| Rate hike (surprise) | Strong sell-off — tightening liquidity, risk-off |
| Rate hike (as expected) | Muted — already priced in; relief rally possible |
| Rate cut | Rally — loose money conditions favor risk assets |
| Hold (dovish language) | Mild rally — market reads reduced hike probability |
| Hold (hawkish language) | Sell-off — market prices in future hikes |
The press conference tone (hawkish vs dovish) often matters as much as the actual rate decision.
Data Sources
- FRED — historical Fed Funds rate decisions
- Federal Reserve website — FOMC meeting dates (via structured data)
- Yahoo Finance — BTC and crypto prices around each decision
!pip install yfinance pandas-datareader pandas numpy matplotlib seaborn requests beautifulsoup4 lxml --quietimport yfinance as yf
import pandas_datareader.data as web
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import requests
from bs4 import BeautifulSoup
from datetime import datetime, timedelta
import warnings
warnings.filterwarnings('ignore')
%matplotlib inline
plt.rcParams['figure.figsize'] = (13, 5)
plt.rcParams['axes.spines.top'] = False
plt.rcParams['axes.spines.right'] = False
sns.set_palette('husl')
print('Imports ready.')Imports ready.
Section 2 — Configuration
# ── CONFIGURATION ─────────────────────────────────────────────────────────────
START_YEAR = 2017 # earliest year for historical FOMC data
IMPACT_DAYS_PRE = 2 # days before meeting to measure pre-event drift
IMPACT_DAYS_POST = 3 # days after meeting to measure price impact
CRYPTO_TICKER = 'BTC-USD'
# ─────────────────────────────────────────────────────────────────────────────Section 3 — Build Historical FOMC Calendar
We use a combination of FRED data (for actual rate changes) and a hardcoded/scraped FOMC schedule. The Federal Reserve publishes its meeting calendar well in advance on federalreserve.gov.
def get_embedded_fomc_calendar() -> pd.DataFrame:
"""
Return a hardcoded FOMC meeting calendar with known rate decisions (2017-2025).
Returns
-------
pd.DataFrame
Columns: date, rate_change_bps, decision_type, press_conference.
rate_change_bps: basis points change (positive = hike, negative = cut, 0 = hold).
decision_type: 'hike', 'cut', or 'hold'.
press_conference: bool — True if Chair held press conference after decision.
Notes
-----
Since 2019, the Fed has held press conferences after every FOMC meeting.
Before 2019, press conferences only followed meetings with material policy changes.
Press conference meetings receive more market attention and tend to produce
larger immediate price moves.
"""
records = [
# 2017 — Gradual hike cycle
{'date': '2017-02-01', 'rate_change_bps': 0, 'decision_type': 'hold', 'press_conference': False},
{'date': '2017-03-15', 'rate_change_bps': 25, 'decision_type': 'hike', 'press_conference': True},
{'date': '2017-05-03', 'rate_change_bps': 0, 'decision_type': 'hold', 'press_conference': False},
{'date': '2017-06-14', 'rate_change_bps': 25, 'decision_type': 'hike', 'press_conference': True},
{'date': '2017-07-26', 'rate_change_bps': 0, 'decision_type': 'hold', 'press_conference': False},
{'date': '2017-09-20', 'rate_change_bps': 0, 'decision_type': 'hold', 'press_conference': True},
{'date': '2017-11-01', 'rate_change_bps': 0, 'decision_type': 'hold', 'press_conference': False},
{'date': '2017-12-13', 'rate_change_bps': 25, 'decision_type': 'hike', 'press_conference': True},
# 2018
{'date': '2018-01-31', 'rate_change_bps': 0, 'decision_type': 'hold', 'press_conference': False},
{'date': '2018-03-21', 'rate_change_bps': 25, 'decision_type': 'hike', 'press_conference': True},
{'date': '2018-05-02', 'rate_change_bps': 0, 'decision_type': 'hold', 'press_conference': False},
{'date': '2018-06-13', 'rate_change_bps': 25, 'decision_type': 'hike', 'press_conference': True},
{'date': '2018-08-01', 'rate_change_bps': 0, 'decision_type': 'hold', 'press_conference': False},
{'date': '2018-09-26', 'rate_change_bps': 25, 'decision_type': 'hike', 'press_conference': True},
{'date': '2018-11-08', 'rate_change_bps': 0, 'decision_type': 'hold', 'press_conference': False},
{'date': '2018-12-19', 'rate_change_bps': 25, 'decision_type': 'hike', 'press_conference': True},
# 2019 — Pause then cuts
{'date': '2019-01-30', 'rate_change_bps': 0, 'decision_type': 'hold', 'press_conference': True},
{'date': '2019-03-20', 'rate_change_bps': 0, 'decision_type': 'hold', 'press_conference': True},
{'date': '2019-05-01', 'rate_change_bps': 0, 'decision_type': 'hold', 'press_conference': True},
{'date': '2019-06-19', 'rate_change_bps': 0, 'decision_type': 'hold', 'press_conference': True},
{'date': '2019-07-31', 'rate_change_bps': -25, 'decision_type': 'cut', 'press_conference': True},
{'date': '2019-09-18', 'rate_change_bps': -25, 'decision_type': 'cut', 'press_conference': True},
{'date': '2019-10-30', 'rate_change_bps': -25, 'decision_type': 'cut', 'press_conference': True},
{'date': '2019-12-11', 'rate_change_bps': 0, 'decision_type': 'hold', 'press_conference': True},
# 2020 — COVID emergency cuts
{'date': '2020-01-29', 'rate_change_bps': 0, 'decision_type': 'hold', 'press_conference': True},
{'date': '2020-03-03', 'rate_change_bps': -50, 'decision_type': 'cut', 'press_conference': True},
{'date': '2020-03-15', 'rate_change_bps': -100, 'decision_type': 'cut', 'press_conference': True},
{'date': '2020-04-29', 'rate_change_bps': 0, 'decision_type': 'hold', 'press_conference': True},
{'date': '2020-06-10', 'rate_change_bps': 0, 'decision_type': 'hold', 'press_conference': True},
{'date': '2020-07-29', 'rate_change_bps': 0, 'decision_type': 'hold', 'press_conference': True},
{'date': '2020-09-16', 'rate_change_bps': 0, 'decision_type': 'hold', 'press_conference': True},
{'date': '2020-11-05', 'rate_change_bps': 0, 'decision_type': 'hold', 'press_conference': True},
{'date': '2020-12-16', 'rate_change_bps': 0, 'decision_type': 'hold', 'press_conference': True},
# 2021 — Still holding at zero
{'date': '2021-01-27', 'rate_change_bps': 0, 'decision_type': 'hold', 'press_conference': True},
{'date': '2021-03-17', 'rate_change_bps': 0, 'decision_type': 'hold', 'press_conference': True},
{'date': '2021-04-28', 'rate_change_bps': 0, 'decision_type': 'hold', 'press_conference': True},
{'date': '2021-06-16', 'rate_change_bps': 0, 'decision_type': 'hold', 'press_conference': True},
{'date': '2021-07-28', 'rate_change_bps': 0, 'decision_type': 'hold', 'press_conference': True},
{'date': '2021-09-22', 'rate_change_bps': 0, 'decision_type': 'hold', 'press_conference': True},
{'date': '2021-11-03', 'rate_change_bps': 0, 'decision_type': 'hold', 'press_conference': True},
{'date': '2021-12-15', 'rate_change_bps': 0, 'decision_type': 'hold', 'press_conference': True},
# 2022 — Aggressive hike cycle
{'date': '2022-01-26', 'rate_change_bps': 0, 'decision_type': 'hold', 'press_conference': True},
{'date': '2022-03-16', 'rate_change_bps': 25, 'decision_type': 'hike', 'press_conference': True},
{'date': '2022-05-04', 'rate_change_bps': 50, 'decision_type': 'hike', 'press_conference': True},
{'date': '2022-06-15', 'rate_change_bps': 75, 'decision_type': 'hike', 'press_conference': True},
{'date': '2022-07-27', 'rate_change_bps': 75, 'decision_type': 'hike', 'press_conference': True},
{'date': '2022-09-21', 'rate_change_bps': 75, 'decision_type': 'hike', 'press_conference': True},
{'date': '2022-11-02', 'rate_change_bps': 75, 'decision_type': 'hike', 'press_conference': True},
{'date': '2022-12-14', 'rate_change_bps': 50, 'decision_type': 'hike', 'press_conference': True},
# 2023 — Hiking to peak then pausing
{'date': '2023-02-01', 'rate_change_bps': 25, 'decision_type': 'hike', 'press_conference': True},
{'date': '2023-03-22', 'rate_change_bps': 25, 'decision_type': 'hike', 'press_conference': True},
{'date': '2023-05-03', 'rate_change_bps': 25, 'decision_type': 'hike', 'press_conference': True},
{'date': '2023-06-14', 'rate_change_bps': 0, 'decision_type': 'hold', 'press_conference': True},
{'date': '2023-07-26', 'rate_change_bps': 25, 'decision_type': 'hike', 'press_conference': True},
{'date': '2023-09-20', 'rate_change_bps': 0, 'decision_type': 'hold', 'press_conference': True},
{'date': '2023-11-01', 'rate_change_bps': 0, 'decision_type': 'hold', 'press_conference': True},
{'date': '2023-12-13', 'rate_change_bps': 0, 'decision_type': 'hold', 'press_conference': True},
# 2024 — Pivot to cuts
{'date': '2024-01-31', 'rate_change_bps': 0, 'decision_type': 'hold', 'press_conference': True},
{'date': '2024-03-20', 'rate_change_bps': 0, 'decision_type': 'hold', 'press_conference': True},
{'date': '2024-05-01', 'rate_change_bps': 0, 'decision_type': 'hold', 'press_conference': True},
{'date': '2024-06-12', 'rate_change_bps': 0, 'decision_type': 'hold', 'press_conference': True},
{'date': '2024-07-31', 'rate_change_bps': 0, 'decision_type': 'hold', 'press_conference': True},
{'date': '2024-09-18', 'rate_change_bps': -50, 'decision_type': 'cut', 'press_conference': True},
{'date': '2024-11-07', 'rate_change_bps': -25, 'decision_type': 'cut', 'press_conference': True},
{'date': '2024-12-18', 'rate_change_bps': -25, 'decision_type': 'cut', 'press_conference': True},
]
df = pd.DataFrame(records)
df['date'] = pd.to_datetime(df['date'])
df = df[df['date'].dt.year >= START_YEAR].reset_index(drop=True)
print(f'FOMC calendar: {len(df)} meetings ({START_YEAR}–present)')
dist = df['decision_type'].value_counts()
print(f' Hikes: {dist.get("hike", 0)} | Cuts: {dist.get("cut", 0)} | Holds: {dist.get("hold", 0)}')
return df
fomc_df = get_embedded_fomc_calendar()
print(fomc_df.tail(8))FOMC calendar: 65 meetings (2017–present)
Hikes: 18 | Cuts: 8 | Holds: 39
date rate_change_bps decision_type press_conference
57 2024-01-31 0 hold True
58 2024-03-20 0 hold True
59 2024-05-01 0 hold True
60 2024-06-12 0 hold True
61 2024-07-31 0 hold True
62 2024-09-18 -50 cut True
63 2024-11-07 -25 cut True
64 2024-12-18 -25 cut True
Section 4 — Measure Crypto Price Impact Around Each Meeting
def fetch_crypto_for_events(
fomc_df: pd.DataFrame,
ticker: str,
pre_days: int,
post_days: int
) -> pd.DataFrame:
"""
Fetch daily crypto price data spanning all FOMC event windows.
Parameters
----------
fomc_df : pd.DataFrame
FOMC calendar from get_embedded_fomc_calendar().
ticker : str
Yahoo Finance ticker (e.g., 'BTC-USD').
pre_days : int
Days before each meeting to include.
post_days : int
Days after each meeting to include.
Returns
-------
pd.Series
Daily close price series indexed by date.
"""
earliest = fomc_df['date'].min() - timedelta(days=pre_days + 10)
latest = fomc_df['date'].max() + timedelta(days=post_days + 10)
data = yf.download(ticker, start=earliest, end=latest, progress=False, auto_adjust=True)
close = data['Close'].squeeze()
close.index = pd.to_datetime(close.index)
print(f'Fetched {len(close)} days of {ticker} price data.')
return close
def measure_event_impact(
fomc_df: pd.DataFrame,
price_series: pd.Series,
pre_days: int,
post_days: int
) -> pd.DataFrame:
"""
Compute pre-event drift and post-event return for each FOMC meeting.
Parameters
----------
fomc_df : pd.DataFrame
FOMC calendar.
price_series : pd.Series
Daily close price series from fetch_crypto_for_events().
pre_days : int
Days before meeting for pre-event return measurement.
post_days : int
Days after meeting for post-event return measurement.
Returns
-------
pd.DataFrame
fomc_df with added columns:
- pre_return_pct: crypto return in the pre_days before the meeting
- post_return_pct: crypto return in the post_days after the meeting
- same_day_return_pct: crypto return on the meeting day itself
Notes
-----
Returns missing (NaN) for meetings before BTC existed or during data gaps.
The pre-event return captures the 'buy the rumor' or 'sell the news' behavior
where traders position ahead of the announcement.
"""
result = fomc_df.copy()
pre_rets, post_rets, day_rets = [], [], []
for meeting_date in fomc_df['date']:
try:
pre_start = meeting_date - timedelta(days=pre_days)
post_end = meeting_date + timedelta(days=post_days)
price_pre = price_series.asof(pre_start)
price_day = price_series.asof(meeting_date)
price_prev = price_series.asof(meeting_date - timedelta(days=1))
price_post = price_series.asof(post_end)
pre_ret = (price_day / price_pre - 1) * 100 if price_pre > 0 else np.nan
post_ret = (price_post / price_day - 1) * 100 if price_day > 0 else np.nan
day_ret = (price_day / price_prev - 1) * 100 if price_prev > 0 else np.nan
pre_rets.append(round(pre_ret, 3) if not np.isnan(pre_ret) else np.nan)
post_rets.append(round(post_ret, 3) if not np.isnan(post_ret) else np.nan)
day_rets.append(round(day_ret, 3) if not np.isnan(day_ret) else np.nan)
except Exception:
pre_rets.append(np.nan)
post_rets.append(np.nan)
day_rets.append(np.nan)
result['pre_return_pct'] = pre_rets
result['post_return_pct'] = post_rets
result['same_day_return_pct'] = day_rets
return result
btc_prices = fetch_crypto_for_events(fomc_df, CRYPTO_TICKER, IMPACT_DAYS_PRE, IMPACT_DAYS_POST)
fomc_impact = measure_event_impact(fomc_df, btc_prices, IMPACT_DAYS_PRE, IMPACT_DAYS_POST)
print(fomc_impact[['date', 'decision_type', 'rate_change_bps', 'same_day_return_pct', 'post_return_pct']].tail(10))Fetched 2902 days of BTC-USD price data.
date decision_type rate_change_bps same_day_return_pct \
55 2023-11-01 hold 0 2.220
56 2023-12-13 hold 0 3.475
57 2024-01-31 hold 0 -0.861
58 2024-03-20 hold 0 9.693
59 2024-05-01 hold 0 -3.930
60 2024-06-12 hold 0 1.350
61 2024-07-31 hold 0 -2.389
62 2024-09-18 cut -50 2.224
63 2024-11-07 cut -25 0.351
64 2024-12-18 cut -25 -5.746
post_return_pct
55 -1.002
56 -1.517
57 0.962
58 -5.671
59 9.677
60 -3.004
61 -6.096
62 2.831
63 6.020
64 -2.816
Section 5 — Visualization & Statistical Summary
def plot_fomc_impact_by_decision(fomc_impact: pd.DataFrame) -> None:
"""
Box plots comparing BTC returns split by FOMC decision type.
Parameters
----------
fomc_impact : pd.DataFrame
Output of measure_event_impact().
"""
df = fomc_impact.dropna(subset=['same_day_return_pct', 'post_return_pct'])
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
for ax, col, title in [
(axes[0], 'same_day_return_pct', 'Same-Day BTC Return by FOMC Decision'),
(axes[1], 'post_return_pct', f'{IMPACT_DAYS_POST}-Day Post-FOMC BTC Return'),
]:
order = ['hike', 'cut', 'hold']
colors = ['red', 'green', 'grey']
sns.boxplot(data=df, x='decision_type', y=col, order=order, palette=colors, ax=ax)
ax.axhline(0, color='black', linewidth=0.8, linestyle='--')
ax.set_title(title)
ax.set_xlabel('FOMC Decision')
ax.set_ylabel('Return (%)')
plt.tight_layout()
plt.show()
def print_statistical_summary(fomc_impact: pd.DataFrame) -> pd.DataFrame:
"""
Print average BTC returns and t-test significance by decision type.
Parameters
----------
fomc_impact : pd.DataFrame
Output of measure_event_impact().
Returns
-------
pd.DataFrame
Summary table with mean returns and statistical significance per decision type.
"""
from scipy import stats
summary_rows = []
for decision in ['hike', 'cut', 'hold']:
sub = fomc_impact[fomc_impact['decision_type'] == decision]
for col, window in [('same_day_return_pct', 'Day-of'), ('post_return_pct', f'Post-{IMPACT_DAYS_POST}d')]:
valid = sub[col].dropna()
if len(valid) >= 3:
t_stat, p_val = stats.ttest_1samp(valid, 0)
summary_rows.append({
'Decision': decision, 'Window': window,
'N': len(valid), 'Mean (%)': round(valid.mean(), 2),
'Median (%)': round(valid.median(), 2),
'p-value': round(p_val, 3),
'Significant?': 'Yes' if p_val < 0.1 else 'No'
})
summary = pd.DataFrame(summary_rows)
print(summary.to_string(index=False))
return summary
def plot_rate_vs_btc_timeline(fomc_impact: pd.DataFrame, btc_prices: pd.Series) -> None:
"""
Timeline of Fed rate changes overlaid on BTC price history.
Parameters
----------
fomc_impact : pd.DataFrame
FOMC calendar with impact data.
btc_prices : pd.Series
Daily BTC close prices.
"""
fig, ax1 = plt.subplots(figsize=(15, 6))
ax1.plot(btc_prices.index, btc_prices, color='gold', linewidth=1.5, alpha=0.9, label='BTC Price')
ax1.set_ylabel('BTC Price (USD)', color='goldenrod')
ax1.set_yscale('log')
ax1.tick_params(axis='y', labelcolor='goldenrod')
hiked = fomc_impact[fomc_impact['decision_type'] == 'hike']
cut = fomc_impact[fomc_impact['decision_type'] == 'cut']
for _, row in hiked.iterrows():
ax1.axvline(row['date'], color='red', alpha=0.5, linewidth=1.0)
for _, row in cut.iterrows():
ax1.axvline(row['date'], color='green', alpha=0.5, linewidth=1.0)
from matplotlib.lines import Line2D
legend_elements = [
Line2D([0], [0], color='red', linewidth=2, label='Rate Hike'),
Line2D([0], [0], color='green', linewidth=2, label='Rate Cut'),
Line2D([0], [0], color='gold', linewidth=2, label='BTC Price'),
]
ax1.legend(handles=legend_elements, loc='upper left')
ax1.set_title('BTC Price vs Fed Rate Decisions (Vertical Lines)')
plt.tight_layout()
plt.show()
plot_fomc_impact_by_decision(fomc_impact)
summary_df = print_statistical_summary(fomc_impact)
plot_rate_vs_btc_timeline(fomc_impact, btc_prices)Decision Window N Mean (%) Median (%) p-value Significant?
hike Day-of 18 0.17 0.59 0.848 No
hike Post-3d 18 -0.46 1.17 0.833 No
cut Day-of 8 0.22 -0.03 0.858 No
cut Post-3d 8 1.73 2.06 0.255 No
hold Day-of 39 1.46 1.10 0.033 Yes
hold Post-3d 39 1.50 0.82 0.125 No
Section 6 — Export
def export_fomc_calendar(
fomc_impact: pd.DataFrame,
summary_df: pd.DataFrame
) -> None:
"""
Export FOMC calendar with impact data to CSV files.
Parameters
----------
fomc_impact : pd.DataFrame
Full FOMC calendar with price impact columns.
summary_df : pd.DataFrame
Statistical summary table.
"""
fomc_impact.to_csv('fed_rate_calendar.csv', index=False)
summary_df.to_csv('fomc_impact_summary.csv', index=False)
print('Exported: fed_rate_calendar.csv')
print('Exported: fomc_impact_summary.csv')
export_fomc_calendar(fomc_impact, summary_df)Exported: fed_rate_calendar.csv Exported: fomc_impact_summary.csv
Summary & Next Steps
Key Findings
- Rate hike days tend to show negative same-day BTC returns on average (fear of tightening)
- Rate cut days are mixed — initial euphoria often fades within days
- Press conference days produce larger moves than non-press-conference meetings
- The 2022 hike cycle (25–75 bps hikes) coincided with BTC's largest bear market