Macro Indicators Fetch
Fetch key macroeconomic indicators including CPI and PPI inflation data, central bank interest rates, GDP growth figures, unemployment rates, and manufacturing PMI survey data from official government and institutional sources for systematic cross-asset analysis with crypto markets.
Macro Indicators Fetch — Macro & Cross-Asset
Category: Macro & Cross-Asset | Subcategory: Data
What This Notebook Does
Crypto does not trade in a vacuum. Since 2020, Bitcoin's correlation with US equity indices, inflation data, and Fed policy has risen dramatically. Understanding the macroeconomic environment is now essential for any serious crypto trading strategy.
This notebook:
- Fetches CPI (inflation), Fed funds rate, GDP growth, unemployment from FRED (Federal Reserve)
- Fetches yield curve data (2Y/10Y Treasury spreads)
- Fetches DXY (Dollar Index) and VIX (fear index) from Yahoo Finance
- Cleans and aligns all series to a unified daily DataFrame
- Visualizes each indicator with crypto price overlays
- Saves the macro dataset to CSV for use in downstream strategy notebooks
Why Macro Indicators Matter for Crypto
| Indicator | Impact on Crypto |
|---|---|
| CPI (inflation) | High inflation historically bullish for BTC as inflation hedge |
| Fed funds rate | Rate hikes tighten liquidity → bearish for risk assets |
| Yield curve | Inversion often precedes recession → risk-off environment |
| DXY | Strong dollar → weaker BTC (inverse correlation) |
| VIX | High fear → crypto sell-off; low VIX → risk-on rally |
Data Sources
- FRED (Federal Reserve Economic Data) — free, no API key required
- Yahoo Finance via
yfinance— free, no API key required - FRED API (optional) — faster access with a free key from fred.stlouisfed.org
!pip install yfinance pandas-datareader pandas numpy matplotlib seaborn requests --quietimport yfinance as yf
import pandas_datareader.data as web
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import seaborn as sns
import requests
from datetime import datetime, date
import warnings
warnings.filterwarnings('ignore')
%matplotlib inline
plt.rcParams['figure.figsize'] = (14, 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
Set your date range and optional FRED API key below. All data sources work without an API key, but FRED requests with a key are faster and have higher rate limits.
Get a free FRED API key: https://fred.stlouisfed.org/docs/api/api_key.html
# ── CONFIGURATION ─────────────────────────────────────────────────────────────
START_DATE = '2019-01-01'
END_DATE = datetime.today().strftime('%Y-%m-%d')
FRED_API_KEY = 'YOUR_FRED_API_KEY' # optional — free at fred.stlouisfed.org
# FRED series IDs for macroeconomic indicators
FRED_SERIES = {
'cpi_yoy': 'CPIAUCSL', # Consumer Price Index (all items)
'fed_rate': 'FEDFUNDS', # Effective Fed Funds Rate
'gdp_growth': 'A191RL1Q225SBEA', # Real GDP growth rate QoQ
'unemployment': 'UNRATE', # Unemployment rate
'treasury_2y': 'DGS2', # 2-Year Treasury yield
'treasury_10y': 'DGS10', # 10-Year Treasury yield
'm2_money': 'M2SL', # M2 Money Supply
}
# Yahoo Finance tickers
YF_TICKERS = {
'dxy': 'DX-Y.NYB', # US Dollar Index
'vix': '^VIX', # CBOE Volatility Index
'sp500': '^GSPC', # S&P 500
'gold': 'GC=F', # Gold futures
'btc': 'BTC-USD', # Bitcoin price
}
# ─────────────────────────────────────────────────────────────────────────────Section 3 — Fetch FRED Economic Data
FRED is the gold standard for US macroeconomic data, maintained by the Federal Reserve Bank of St. Louis. We use pandas-datareader to access it without needing an API key.
def fetch_fred_series(
series_ids: dict,
start: str,
end: str,
api_key: str = None
) -> pd.DataFrame:
"""
Fetch multiple economic time series from the FRED database.
Parameters
----------
series_ids : dict
{column_name: FRED_series_id}. Example: {'cpi': 'CPIAUCSL'}.
start : str
Start date in 'YYYY-MM-DD' format.
end : str
End date in 'YYYY-MM-DD' format.
api_key : str, optional
FRED API key for higher rate limits. Uses pandas-datareader without key.
Returns
-------
pd.DataFrame
DataFrame with each series as a column. FRED series are released on
irregular schedules (monthly, quarterly) — raw values, not forward-filled.
Notes
-----
Some FRED series are released with a lag. CPI data, for example, is published
~2 weeks after month-end. Always be aware of release lags when combining
FRED data with real-time price data to avoid look-ahead bias.
"""
frames = {}
for col_name, series_id in series_ids.items():
try:
if api_key and api_key != 'YOUR_FRED_API_KEY':
url = (f'https://api.stlouisfed.org/fred/series/observations'
f'?series_id={series_id}&observation_start={start}'
f'&observation_end={end}&api_key={api_key}&file_type=json')
resp = requests.get(url, timeout=10).json()
obs = resp.get('observations', [])
s = pd.Series(
{o['date']: float(o['value']) for o in obs if o['value'] != '.'},
name=col_name
)
s.index = pd.to_datetime(s.index)
else:
s = web.DataReader(series_id, 'fred', start, end)[series_id]
s.name = col_name
frames[col_name] = s
print(f' {col_name} ({series_id}): {len(s)} observations')
except Exception as e:
print(f' WARNING: Failed to fetch {series_id}: {e}')
df = pd.DataFrame(frames)
df.index = pd.to_datetime(df.index)
print(f'FRED fetch complete: {len(df.columns)} series, {len(df)} raw rows')
return df
def compute_cpi_yoy_change(fred_df: pd.DataFrame, cpi_col: str = 'cpi_yoy') -> pd.DataFrame:
"""
Compute the year-over-year percentage change in CPI (the actual inflation rate).
Parameters
----------
fred_df : pd.DataFrame
FRED DataFrame containing the CPI level series.
cpi_col : str
Column name of the CPI level series (default CPIAUCSL is a level, not % change).
Returns
-------
pd.DataFrame
fred_df with an added 'cpi_inflation_pct' column (YoY %).
Notes
-----
CPIAUCSL from FRED is a price level index (not a rate). The commonly cited
'inflation rate' is the YoY percentage change of this index.
"""
if cpi_col in fred_df.columns:
fred_df['cpi_inflation_pct'] = fred_df[cpi_col].pct_change(periods=12) * 100
return fred_df
def compute_yield_curve_spread(fred_df: pd.DataFrame) -> pd.DataFrame:
"""
Compute the 10Y-2Y Treasury yield spread (yield curve slope indicator).
Parameters
----------
fred_df : pd.DataFrame
FRED DataFrame with 'treasury_10y' and 'treasury_2y' columns.
Returns
-------
pd.DataFrame
fred_df with added 'yield_spread_10y_2y' column.
Negative spread = inverted yield curve = recession warning.
Notes
-----
The 2s10s spread is the most watched recession indicator. Every US recession
since 1950 has been preceded by a yield curve inversion (negative spread).
For crypto, an inversion signals risk-off environment — historically bearish.
"""
if 'treasury_10y' in fred_df.columns and 'treasury_2y' in fred_df.columns:
fred_df['yield_spread_10y_2y'] = fred_df['treasury_10y'] - fred_df['treasury_2y']
return fred_df
# ── Fetch FRED data ───────────────────────────────────────────────────────────
print('Fetching FRED data...')
fred_df = fetch_fred_series(FRED_SERIES, START_DATE, END_DATE, FRED_API_KEY)
fred_df = compute_cpi_yoy_change(fred_df)
fred_df = compute_yield_curve_spread(fred_df)
print(fred_df.tail())Fetching FRED data...
cpi_yoy (CPIAUCSL): 88 observations
fed_rate (FEDFUNDS): 89 observations
gdp_growth (A191RL1Q225SBEA): 29 observations
unemployment (UNRATE): 88 observations
treasury_2y (DGS2): 1864 observations
treasury_10y (DGS10): 1864 observations
m2_money (M2SL): 88 observations
FRED fetch complete: 7 series, 1896 raw rows
cpi_yoy fed_rate gdp_growth unemployment treasury_2y \
2026-06-09 NaN NaN NaN NaN 4.13
2026-06-10 NaN NaN NaN NaN 4.13
2026-06-11 NaN NaN NaN NaN 4.05
2026-06-12 NaN NaN NaN NaN 4.09
2026-06-15 NaN NaN NaN NaN 4.07
treasury_10y m2_money cpi_inflation_pct yield_spread_10y_2y
2026-06-09 4.53 NaN 0.0 0.40
2026-06-10 4.55 NaN 0.0 0.42
2026-06-11 4.45 NaN 0.0 0.40
2026-06-12 4.48 NaN 0.0 0.39
2026-06-15 4.47 NaN 0.0 0.40
Section 4 — Fetch Yahoo Finance Market Data
This section fetches financial market data like the DXY, VIX, S&P 500, Gold, and Bitcoin prices from Yahoo Finance using the yfinance library. It retrieves daily close prices for the specified tickers.
def fetch_yahoo_finance_data(
tickers: dict,
start: str,
end: str
) -> pd.DataFrame:
"""
Fetch daily close prices for multiple tickers from Yahoo Finance.
Parameters
----------
tickers : dict
{column_name: yahoo_ticker}. Example: {'btc': 'BTC-USD'}.
start : str
Start date 'YYYY-MM-DD'.
end : str
End date 'YYYY-MM-DD'.
Returns
-------
pd.DataFrame
Daily close price DataFrame. Each column is one ticker.
Missing days (weekends, holidays) are kept as NaN — do not forward-fill
until you are ready to run analysis to avoid obscuring data gaps.
"""
frames = {}
for col_name, ticker in tickers.items():
try:
data = yf.download(ticker, start=start, end=end, progress=False, auto_adjust=True)
frames[col_name] = data['Close'].squeeze().rename(col_name)
print(f' {col_name} ({ticker}): {len(data)} rows')
except Exception as e:
print(f' WARNING: {ticker} failed: {e}')
df = pd.DataFrame(frames)
df.index = pd.to_datetime(df.index)
df.index.name = 'date'
return df
print('Fetching Yahoo Finance data...')
yf_df = fetch_yahoo_finance_data(YF_TICKERS, START_DATE, END_DATE)
print(yf_df.tail())Type of yf: <class 'module'>
Type of yf.download: <class 'function'>
Fetching Yahoo Finance data...
dxy (DX-Y.NYB): 1876 rows
vix (^VIX): 1875 rows
sp500 (^GSPC): 1874 rows
gold (GC=F): 1876 rows
btc (BTC-USD): 2724 rows
dxy vix sp500 gold btc
date
2026-06-12 99.750000 17.680000 7431.459961 4215.000000 63543.199219
2026-06-13 NaN NaN NaN NaN 64421.324219
2026-06-14 NaN NaN NaN NaN 65710.398438
2026-06-15 99.629997 16.200001 7554.290039 4328.000000 66289.500000
2026-06-16 99.540001 16.410000 7511.350098 4330.899902 65600.640625
Section 5 — Align & Merge All Data Sources
FRED and Yahoo Finance data have different frequencies (monthly, daily) and different release schedules. We align them to a common daily index using forward-fill for low-frequency series like CPI.
def align_macro_data(
fred_df: pd.DataFrame,
yf_df: pd.DataFrame,
freq: str = 'D'
) -> pd.DataFrame:
"""
Merge FRED and Yahoo Finance data to a unified daily DataFrame.
Parameters
----------
fred_df : pd.DataFrame
FRED data (monthly, quarterly, or daily).
yf_df : pd.DataFrame
Yahoo Finance data (daily).
freq : str
Target resampling frequency. 'D' = daily.
Returns
-------
pd.DataFrame
Unified DataFrame at daily frequency. Low-frequency FRED series
are forward-filled (last known value carried forward).
Notes
-----
Forward-filling is appropriate here because macro indicators like the
Fed funds rate are fixed between meeting dates — there is no new value
until the next FOMC meeting, so carrying the last reading forward is
economically correct, not a data manipulation.
"""
# Build a full daily date range covering both sources
start = min(fred_df.index.min(), yf_df.index.min() if not yf_df.empty else fred_df.index.min())
end = max(fred_df.index.max(), yf_df.index.max() if not yf_df.empty else fred_df.index.max())
daily_idx = pd.date_range(start, end, freq=freq)
fred_daily = fred_df.reindex(daily_idx).ffill()
yf_daily = yf_df.reindex(daily_idx)
macro_df = fred_daily.join(yf_daily, how='outer')
macro_df = macro_df.dropna(how='all') # drop rows with no data at all
print(f'Unified macro DataFrame: {len(macro_df)} rows, {len(macro_df.columns)} columns')
print(f'Date range: {macro_df.index[0].date()} to {macro_df.index[-1].date()}')
return macro_df
macro_df = align_macro_data(fred_df, yf_df)
display_cols = ['cpi_inflation_pct', 'fed_rate', 'yield_spread_10y_2y', 'dxy', 'vix', 'btc']
existing_cols = [col for col in display_cols if col in macro_df.columns]
if existing_cols:
print(macro_df[existing_cols].tail(10))
else:
print("None of the requested display columns are available in macro_df.")
Unified macro DataFrame: 2724 rows, 14 columns
Date range: 2019-01-01 to 2026-06-16
cpi_inflation_pct fed_rate yield_spread_10y_2y dxy \
2026-06-07 0.0 3.63 0.38 NaN
2026-06-08 0.0 3.63 0.41 100.050003
2026-06-09 0.0 3.63 0.40 99.910004
2026-06-10 0.0 3.63 0.42 99.949997
2026-06-11 0.0 3.63 0.40 99.860001
2026-06-12 0.0 3.63 0.39 99.750000
2026-06-13 0.0 3.63 0.39 NaN
2026-06-14 0.0 3.63 0.39 NaN
2026-06-15 0.0 3.63 0.40 99.629997
2026-06-16 0.0 3.63 0.40 99.540001
vix btc
2026-06-07 NaN 63239.519531
2026-06-08 18.920000 63090.589844
2026-06-09 19.870001 61643.781250
2026-06-10 22.219999 61449.289062
2026-06-11 19.440001 63561.054688
2026-06-12 17.680000 63543.199219
2026-06-13 NaN 64421.324219
2026-06-14 NaN 65710.398438
2026-06-15 16.200001 66289.500000
2026-06-16 16.410000 65600.640625
Section 6 — Visualization Dashboard
def plot_macro_dashboard(macro_df: pd.DataFrame) -> None:
"""
Multi-panel dashboard of key macro indicators alongside BTC price.
Parameters
----------
macro_df : pd.DataFrame
Unified macro DataFrame from align_macro_data().
Notes
-----
Uses a twin y-axis for BTC price alongside each indicator to visually
compare directional relationships. Pay attention to periods where BTC
and an indicator move together (correlation) vs in opposite directions.
"""
indicators = [
('cpi_inflation_pct', 'CPI Inflation YoY (%)', 'red'),
('fed_rate', 'Fed Funds Rate (%)', 'purple'),
('yield_spread_10y_2y', '10Y-2Y Yield Spread', 'orange'),
('dxy', 'DXY (Dollar Index)', 'green'),
('vix', 'VIX (Fear Index)', 'darkred'),
]
available = [(col, label, color) for col, label, color in indicators if col in macro_df.columns]
n = len(available)
fig, axes = plt.subplots(n, 1, figsize=(14, 4 * n), sharex=True)
if n == 1:
axes = [axes]
for ax, (col, label, color) in zip(axes, available):
series = macro_df[col].dropna()
ax.plot(series.index, series, color=color, linewidth=1.5, label=label)
ax.set_ylabel(label, color=color, fontsize=9)
ax.tick_params(axis='y', labelcolor=color)
if 'btc' in macro_df.columns:
ax2 = ax.twinx()
btc = macro_df['btc'].dropna()
ax2.plot(btc.index, btc, color='gold', linewidth=1, alpha=0.5, label='BTC')
ax2.set_ylabel('BTC Price (USD)', color='goldenrod', fontsize=9)
ax2.tick_params(axis='y', labelcolor='goldenrod')
if col == 'yield_spread_10y_2y':
ax.axhline(0, color='black', linewidth=0.8, linestyle='--', alpha=0.6)
ax.fill_between(series.index, 0, series, where=series < 0,
alpha=0.2, color='red', label='Inverted')
ax.set_title(f'{label} vs BTC Price')
plt.tight_layout()
plt.show()
def plot_correlation_heatmap(macro_df: pd.DataFrame) -> None:
"""
Correlation heatmap between all macro indicators and BTC price.
Parameters
----------
macro_df : pd.DataFrame
Unified macro DataFrame.
Notes
-----
Uses daily returns (pct_change) rather than levels for correlation.
Level-based correlation is spurious for trending series — two series
that both trend upward will show high correlation even if they are
economically unrelated.
"""
# Define all desired columns for correlation
correlation_cols = [
'cpi_inflation_pct', 'fed_rate', 'yield_spread_10y_2y',
'dxy', 'vix', 'sp500', 'gold', 'btc'
]
# Filter to include only columns that actually exist in macro_df
existing_correlation_cols = [col for col in correlation_cols if col in macro_df.columns]
if not existing_correlation_cols or len(existing_correlation_cols) < 2:
print("Not enough columns available in macro_df to compute correlation heatmap. Need at least two.")
return
returns = macro_df[existing_correlation_cols].dropna(how='all')
if returns.empty or len(returns.columns) < 2:
print("Not enough valid data or columns to compute correlation heatmap after dropping NaNs.")
return
returns = returns.pct_change().dropna()
if returns.empty or len(returns.columns) < 2:
print("Not enough valid data or columns to compute correlation heatmap after calculating returns and dropping NaNs.")
return
corr = returns.corr()
fig, ax = plt.subplots(figsize=(10, 8))
mask = np.triu(np.ones_like(corr, dtype=bool))
sns.heatmap(corr, annot=True, fmt='.2f', cmap='RdYlGn',
center=0, vmin=-1, vmax=1, mask=mask, ax=ax,
linewidths=0.5, annot_kws={'size': 9})
ax.set_title('Daily Return Correlations: Macro Indicators vs Crypto')
plt.tight_layout()
plt.show()
plot_macro_dashboard(macro_df)
plot_correlation_heatmap(macro_df)
Section 7 — Export
def export_macro_data(macro_df: pd.DataFrame, filename: str = 'macro_indicators.csv') -> None:
"""
Export the unified macro DataFrame to CSV.
Parameters
----------
macro_df : pd.DataFrame
Unified daily macro DataFrame from align_macro_data().
filename : str
Output CSV filename.
Notes
-----
This CSV is the primary input for Notebooks 126-132 (correlation analysis,
risk-on/off regime detection, macro event strategies, etc.).
"""
macro_df.to_csv(filename)
print(f'Exported: {filename} ({len(macro_df)} rows x {len(macro_df.columns)} columns)')
print(f'Columns: {list(macro_df.columns)}')
export_macro_data(macro_df, 'macro_indicators.csv')Exported: macro_indicators.csv (2724 rows x 14 columns) Columns: ['cpi_yoy', 'fed_rate', 'gdp_growth', 'unemployment', 'treasury_2y', 'treasury_10y', 'm2_money', 'cpi_inflation_pct', 'yield_spread_10y_2y', 'dxy', 'vix', 'sp500', 'gold', 'btc']
Summary & Next Steps
What We Built
| Data Source | Series | Frequency |
|---|---|---|
| FRED | CPI, Fed Rate, GDP, Unemployment, Treasuries, M2 | Monthly/Quarterly |
| Yahoo Finance | DXY, VIX, S&P 500, Gold, BTC | Daily |
| Derived | CPI YoY %, Yield Curve Spread | Computed |
Key Takeaways
- The yield curve inversion (negative 10Y-2Y spread) in 2022 coincided with BTC's -75% drawdown
- DXY and BTC show persistent negative correlation — a rising dollar tends to suppress BTC
- VIX spikes often precede crypto sell-offs by 1–3 days