Macro·Macro Data Fetching·Beginner

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.

data-fetchingmacro

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:

  1. Scrapes the historical FOMC meeting schedule and rate decisions from the Federal Reserve website
  2. Fetches the actual rate decision for each meeting (hike / cut / hold) from FRED
  3. Builds a clean event calendar with decision type, basis-point change, and press conference flag
  4. Measures crypto price impact in the ±3 day window around each decision
  5. Classifies meeting surprises (actual vs market-implied rate from Fed Funds futures)
  6. Exports a structured FOMC calendar DataFrame for use in event-driven strategy notebooks

FOMC Basics for Crypto Traders

DecisionHistorical Crypto Impact
Rate hike (surprise)Strong sell-off — tightening liquidity, risk-off
Rate hike (as expected)Muted — already priced in; relief rally possible
Rate cutRally — 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
[1]
!pip install yfinance pandas-datareader pandas numpy matplotlib seaborn requests beautifulsoup4 lxml --quiet
[2]
import 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

[3]
# ── 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.

[4]
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

[5]
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

[6]
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)
cell output
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
cell output

Section 6 — Export

[7]
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