U.S. Macro Analysis
This notebook fetches key U.S. macroeconomic indicators directly from the FRED API and produces a structured analysis report.
The analysis covers four areas:
- Interest rates: Fed Funds Rate, 2Y/10Y/30Y Treasury yields, yield curve spread
- Inflation: CPI YoY, Core CPI YoY, PCE YoY, 5Y/10Y breakeven inflation
- Growth & employment: Real GDP growth, unemployment rate, nonfarm payrolls, ISM PMI
- Federal debt: Debt-to-GDP, total public debt, net interest payments
| Parameter | Value |
|---|---|
| Data source | FRED API (Federal Reserve Bank of St. Louis) |
| History start | January 2000 |
| Update cadence | Monthly (rates, inflation, debt) · Quarterly (GDP) |
| FRED series | 16 series across 4 sections |
Requires: FRED_API_KEY in the project-level .env.
Free keys are available at fred.stlouisfed.org/docs/api/api_key.html.
import os
import numpy as np
import pandas as pd
import requests
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import matplotlib.ticker as mticker
from pathlib import Path
from dotenv import load_dotenv
from IPython.display import display, HTML
FL_BLUE = '#2563eb'
FL_SLATE = '#64748b'
FL_AMBER = '#f59e0b'
FL_GREEN = '#16a34a'
FL_RED = '#ef4444'
FL_BG = '#ffffff'
FL_GRID = '#e2e8f0'
FL_TEXT = '#0f172a'
FL_TEXT2 = '#334155'
FL_BORDER = '#e2e8f0'
matplotlib.rcParams.update({
'figure.facecolor': FL_BG,
'axes.facecolor': FL_BG,
'axes.edgecolor': FL_BORDER,
'axes.labelcolor': FL_TEXT2,
'axes.spines.top': False,
'axes.spines.right': False,
'axes.grid': True,
'grid.color': FL_GRID,
'grid.linewidth': 0.7,
'xtick.color': FL_TEXT2,
'ytick.color': FL_TEXT2,
'xtick.labelsize': 10,
'ytick.labelsize': 10,
'axes.labelsize': 11,
'axes.titlesize': 12,
'axes.titlecolor': FL_TEXT,
'axes.titlepad': 12,
'legend.frameon': False,
'legend.fontsize': 10,
'figure.dpi': 300,
'savefig.dpi': 300,
'savefig.bbox': 'tight',
'font.family': 'sans-serif',
'font.sans-serif': ['Inter', 'Helvetica Neue', 'Arial', 'DejaVu Sans'],
})
ENV_PATH = Path.cwd() / '.env'
load_dotenv(ENV_PATH)
#print(f'Loaded .env : {ENV_PATH}')
FRED_API_KEY = os.getenv('FRED_API_KEY')
if not FRED_API_KEY:
raise RuntimeError(f'FRED_API_KEY not found in {ENV_PATH}')
FRED_BASE = 'https://api.stlouisfed.org/fred/series/observations'
START_DATE = '2000-01-01'
Helper functions
FRED fetch utilities, formatting functions, and the HTML table/card renderer shared across all sections.
def fetch_fred(series_id: str, start: str = START_DATE) -> pd.Series:
params = {
'series_id': series_id,
'api_key': FRED_API_KEY,
'file_type': 'json',
'observation_start': start,
'sort_order': 'asc',
}
resp = requests.get(FRED_BASE, params=params, timeout=30)
resp.raise_for_status()
records = []
for obs in resp.json().get('observations', []):
try:
records.append({'date': pd.to_datetime(obs['date']), 'value': float(obs['value'])})
except (ValueError, KeyError):
continue
if not records:
return pd.Series(dtype=float)
return pd.DataFrame(records).set_index('date').sort_index()['value']
def fetch_multi(series_map: dict) -> dict:
results = {}
for label, sid in series_map.items():
try:
results[label] = fetch_fred(sid)
print(f' {sid:<28} {label} ({len(results[label])} obs)')
except Exception as e:
print(f' {sid:<28} ERROR: {e}')
results[label] = pd.Series(dtype=float)
return results
def latest(s: pd.Series):
if s.empty: return None
v = float(s.iloc[-1])
return v if np.isfinite(v) else None
def change_1y(s: pd.Series):
"""Absolute pp change vs 12 periods ago."""
if len(s) <= 12: return None
curr, prev = float(s.iloc[-1]), float(s.iloc[-13])
return (curr - prev) if (np.isfinite(curr) and np.isfinite(prev)) else None
def fmt_pct(v, d=2):
return 'N/A' if (v is None or (isinstance(v, float) and not np.isfinite(v))) else f'{v:.{d}f}%'
def render_cards(cards: list) -> str:
items = ''.join(
f'<div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;'
f'padding:18px 16px;text-align:center;min-width:140px;flex:1 1 140px">'
f'<div style="font-size:20px;font-weight:600;color:#0f172a;'
f'letter-spacing:-0.02em;margin-bottom:6px">{c["value"]}</div>'
f'<div style="font-size:11.5px;color:#64748b;font-weight:500">{c["label"]}</div>'
f'</div>'
for c in cards
)
return f'<div style="display:flex;flex-wrap:wrap;gap:12px;margin:16px 0">{items}</div>'
Data fetch
All 16 FRED series are fetched once here. Index-level inflation series (CPI, Core CPI, PCE) are immediately transformed to 12-month percent change.
FRED_SERIES = {
# Rates
'fed_funds': 'FEDFUNDS',
'treasury_2y': 'DGS2',
'treasury_10y': 'DGS10',
'treasury_30y': 'DGS30',
# Inflation
'cpi': 'CPIAUCSL', # index level, YoY
'core_cpi': 'CPILFESL', # index level, YoY
'pce': 'PCEPI', # index level, YoY
'breakeven_5y': 'T5YIE',
'breakeven_10y': 'T10YIE',
# Growth & employment
'gdp_growth': 'A191RL1Q225SBEA', # real GDP QoQ annualized, already %
'unemployment': 'UNRATE',
'nfp': 'PAYEMS', # total nonfarm payrolls, thousands
# Federal debt
'debt_to_gdp': 'GFDEGDQ188S',
'total_debt': 'GFDEBTN', # millions, dollars
'interest_pmts': 'A091RC1Q027SBEA', # net interest, billions, quarterly
}
print('Fetching FRED series...')
raw = fetch_multi(FRED_SERIES)
# Transform price indices to 12-month YoY %
for key in ('cpi', 'core_cpi', 'pce'):
s = raw.get(key, pd.Series(dtype=float))
raw[key] = s.pct_change(12) * 100 if not s.empty else s
print(f'\nDone. {sum(1 for s in raw.values() if not s.empty)}/{len(raw)} series loaded.')
Fetching FRED series... FEDFUNDS fed_funds (316 obs) DGS2 treasury_2y (6600 obs) DGS10 treasury_10y (6600 obs) DGS30 treasury_30y (6600 obs) CPIAUCSL cpi (315 obs) CPILFESL core_cpi (315 obs) PCEPI pce (315 obs) T5YIE breakeven_5y (5852 obs) T10YIE breakeven_10y (5852 obs) A191RL1Q225SBEA gdp_growth (105 obs) UNRATE unemployment (315 obs) PAYEMS nfp (316 obs) GFDEGDQ188S debt_to_gdp (104 obs) GFDEBTN total_debt (104 obs) A091RC1Q027SBEA interest_pmts (105 obs) Done. 15/15 series loaded.
Interest rates
The Federal Funds Rate is the primary policy lever of the Federal Reserve. Treasury yields at different maturities reflect market expectations for growth and inflation. The 2s10s spread (10Y minus 2Y) is the most watched yield curve indicator. When negative (inversion), it has historically preceded recessions by 6-18 months, though with variable lead times.
ffr = raw['fed_funds']
t2y = raw['treasury_2y']
t10y = raw['treasury_10y']
t30y = raw['treasury_30y']
# 2s10s yield curve spread
common = t2y.index.intersection(t10y.index)
spread = (t10y.loc[common] - t2y.loc[common]).dropna() if len(common) > 0 else pd.Series(dtype=float)
curr_ffr = latest(ffr)
curr_2y = latest(t2y)
curr_10y = latest(t10y)
curr_30y = latest(t30y)
curr_spread = latest(spread)
ffr_change = change_1y(ffr)
display(HTML(render_cards([
{'label': 'Fed Funds Rate', 'value': fmt_pct(curr_ffr)},
{'label': '2Y Treasury', 'value': fmt_pct(curr_2y)},
{'label': '10Y Treasury', 'value': fmt_pct(curr_10y)},
{'label': '30Y Treasury', 'value': fmt_pct(curr_30y)},
{'label': '2s10s Spread', 'value': (f'{curr_spread:+.2f}%' if curr_spread is not None else 'N/A')},
{'label': 'FFR 1Y Change', 'value': (f'{ffr_change:+.2f}pp' if ffr_change is not None else 'N/A')},
])))
plt.figure(figsize=(8, 4.5))
for s, label, color, lw, ls in [
(ffr, 'Fed Funds', FL_BLUE, 2.0, '-'),
(t2y, '2Y', FL_SLATE, 1.4, '--'),
(t10y, '10Y', FL_AMBER, 1.4, '--'),
]:
if not s.empty:
plt.plot(
s.index,
s.values,
color=color,
linewidth=lw,
linestyle=ls,
label=label
)
plt.ylabel('Rate (%)')
plt.title('Key interest rates')
plt.gca().yaxis.set_major_formatter(
mticker.FuncFormatter(lambda v, _: f'{v:.1f}%')
)
plt.gca().xaxis.set_major_locator(mdates.YearLocator(4))
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%Y'))
plt.legend()
plt.tick_params(axis='both', which='both', length=0)
plt.tight_layout()
plt.show()
plt.figure(figsize=(8, 4.5))
if not spread.empty:
plt.fill_between(
spread.index,
spread.values,
0,
where=(spread.values >= 0),
alpha=0.18,
color=FL_BLUE,
interpolate=True
)
plt.fill_between(
spread.index,
spread.values,
0,
where=(spread.values < 0),
alpha=0.25,
color=FL_RED,
interpolate=True
)
plt.plot(
spread.index,
spread.values,
color=FL_BLUE,
linewidth=1.6
)
plt.axhline(0, color=FL_GRID, linewidth=0.8)
plt.ylabel('Spread (%)')
plt.title('2s10s yield curve spread')
plt.gca().yaxis.set_major_formatter(
mticker.FuncFormatter(lambda v, _: f'{v:+.2f}%')
)
plt.gca().xaxis.set_major_locator(mdates.YearLocator(4))
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%Y'))
plt.tick_params(axis='both', which='both', length=0)
plt.tight_layout()
plt.show()
rates_summary_df = pd.DataFrame([
{
'Metric': 'Fed Funds Rate',
'Value': fmt_pct(curr_ffr) if curr_ffr is not None else 'N/A'
},
{
'Metric': '2Y Treasury',
'Value': fmt_pct(curr_2y) if curr_2y is not None else 'N/A'
},
{
'Metric': '10Y Treasury',
'Value': fmt_pct(curr_10y) if curr_10y is not None else 'N/A'
},
{
'Metric': '30Y Treasury',
'Value': fmt_pct(curr_30y) if curr_30y is not None else 'N/A'
},
{
'Metric': '2s10s Spread',
'Value': f'{curr_spread:+.2f}%' if curr_spread is not None else 'N/A'
},
{
'Metric': 'FFR 1Y Change',
'Value': f'{ffr_change:+.2f}pp' if ffr_change is not None else 'N/A'
}
])
display(rates_summary_df)
| Metric | Value | |
|---|---|---|
| 0 | Fed Funds Rate | 3.64% |
| 1 | 2Y Treasury | 4.08% |
| 2 | 10Y Treasury | 4.57% |
| 3 | 30Y Treasury | 5.10% |
| 4 | 2s10s Spread | +0.49% |
| 5 | FFR 1Y Change | -0.69pp |
Inflation
CPI is the headline measure of consumer prices. Core CPI strips out food and energy to reveal underlying price pressure. PCE is the Fed's preferred inflation gauge and tends to run slightly below CPI. Breakeven inflation is derived from TIPS spreads and reflects market's implied inflation expectations over 5 and 10 years.
The Fed's long-run target is 2%.
cpi = raw['cpi']
core_cpi = raw['core_cpi']
pce = raw['pce']
be5 = raw['breakeven_5y']
be10 = raw['breakeven_10y']
curr_cpi = latest(cpi)
curr_core = latest(core_cpi)
curr_pce = latest(pce)
curr_be5 = latest(be5)
curr_be10 = latest(be10)
cpi_1y = change_1y(cpi)
display(HTML(render_cards([
{'label': 'CPI YoY', 'value': fmt_pct(curr_cpi)},
{'label': 'Core CPI YoY', 'value': fmt_pct(curr_core)},
{'label': 'PCE YoY', 'value': fmt_pct(curr_pce)},
{'label': '5Y Breakeven', 'value': fmt_pct(curr_be5)},
{'label': '10Y Breakeven', 'value': fmt_pct(curr_be10)},
{'label': 'CPI 1Y Change', 'value': (f'{cpi_1y:+.2f}pp' if cpi_1y is not None else 'N/A')},
])))
plt.figure(figsize=(8, 4.5))
for s, label, color, lw, ls in [
(ffr, 'Fed Funds', FL_BLUE, 2.0, '-'),
(t2y, '2Y', FL_SLATE, 1.4, '--'),
(t10y, '10Y', FL_AMBER, 1.4, '--'),
]:
if not s.empty:
plt.plot(
s.index,
s.values,
color=color,
linewidth=lw,
linestyle=ls,
label=label
)
plt.ylabel('Rate (%)')
plt.title('Key interest rates')
plt.gca().yaxis.set_major_formatter(
mticker.FuncFormatter(lambda v, _: f'{v:.1f}%')
)
plt.gca().xaxis.set_major_locator(mdates.YearLocator(4))
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%Y'))
plt.legend()
plt.tick_params(axis='both', which='both', length=0)
plt.tight_layout()
plt.show()
plt.figure(figsize=(8, 4.5))
if not spread.empty:
plt.fill_between(
spread.index,
spread.values,
0,
where=(spread.values >= 0),
alpha=0.18,
color=FL_BLUE,
interpolate=True
)
plt.fill_between(
spread.index,
spread.values,
0,
where=(spread.values < 0),
alpha=0.25,
color=FL_RED,
interpolate=True
)
plt.plot(
spread.index,
spread.values,
color=FL_BLUE,
linewidth=1.6
)
plt.axhline(0, color=FL_GRID, linewidth=0.8)
plt.ylabel('Spread (%)')
plt.title('2s10s yield curve spread')
plt.gca().yaxis.set_major_formatter(
mticker.FuncFormatter(lambda v, _: f'{v:+.2f}%')
)
plt.gca().xaxis.set_major_locator(mdates.YearLocator(4))
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%Y'))
plt.tick_params(axis='both', which='both', length=0)
plt.tight_layout()
plt.show()
rates_summary_df = pd.DataFrame([
{
'Metric': 'Fed Funds Rate',
'Value': fmt_pct(curr_ffr) if curr_ffr is not None else 'N/A'
},
{
'Metric': '2Y Treasury',
'Value': fmt_pct(curr_2y) if curr_2y is not None else 'N/A'
},
{
'Metric': '10Y Treasury',
'Value': fmt_pct(curr_10y) if curr_10y is not None else 'N/A'
},
{
'Metric': '30Y Treasury',
'Value': fmt_pct(curr_30y) if curr_30y is not None else 'N/A'
},
{
'Metric': '2s10s Spread',
'Value': f'{curr_spread:+.2f}%' if curr_spread is not None else 'N/A'
},
{
'Metric': 'FFR 1Y Change',
'Value': f'{ffr_change:+.2f}pp' if ffr_change is not None else 'N/A'
}
])
display(rates_summary_df)
| Metric | Value | |
|---|---|---|
| 0 | Fed Funds Rate | 3.64% |
| 1 | 2Y Treasury | 4.08% |
| 2 | 10Y Treasury | 4.57% |
| 3 | 30Y Treasury | 5.10% |
| 4 | 2s10s Spread | +0.49% |
| 5 | FFR 1Y Change | -0.69pp |
Growth & employment
Real GDP growth is the BEA's quarterly percent change, seasonally adjusted annual rate. Two consecutive negative quarters is the informal recession definition. Unemployment and nonfarm payrolls are the primary labor market gauges and the Fed monitors both as part of its dual mandate (price stability + maximum employment). ISM Manufacturing PMI above 50 signals expansion; below 50 signals contraction.
gdp = raw['gdp_growth']
unemp = raw['unemployment']
nfp = raw['nfp']
curr_gdp = latest(gdp)
curr_unemp = latest(unemp)
unemp_1y = change_1y(unemp)
nfp_mom = float(nfp.iloc[-1] - nfp.iloc[-2]) if len(nfp) >= 2 else None
display(HTML(render_cards([
{'label': 'Real GDP Growth (QoQ Ann.)', 'value': fmt_pct(curr_gdp)},
{'label': 'Unemployment Rate', 'value': fmt_pct(curr_unemp)},
{'label': 'NFP MoM Change (K)', 'value': (f'{nfp_mom:+,.0f}K' if nfp_mom is not None else 'N/A')},
{'label': 'Unemployment 1Y Change', 'value': (f'{unemp_1y:+.2f}pp' if unemp_1y is not None else 'N/A')},
])))
plt.figure(figsize=(8, 4.5))
if not gdp.empty:
bar_colors = [FL_BLUE if v >= 0 else FL_RED for v in gdp.values]
plt.bar(
gdp.index,
gdp.values,
color=bar_colors,
alpha=0.80,
width=60
)
plt.axhline(0, color=FL_GRID, linewidth=0.8)
plt.ylabel('Growth (%)')
plt.title('Real GDP growth (QoQ annualized)')
plt.gca().yaxis.set_major_formatter(
mticker.FuncFormatter(lambda v, _: f'{v:.1f}%')
)
plt.gca().xaxis.set_major_locator(mdates.YearLocator(4))
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%Y'))
plt.tick_params(axis='both', which='both', length=0)
plt.tight_layout()
plt.show()
plt.figure(figsize=(8, 4.5))
if not unemp.empty:
plt.fill_between(unemp.index, unemp.values, alpha=0.12, color=FL_BLUE)
plt.plot(unemp.index, unemp.values, color=FL_BLUE, linewidth=1.8)
plt.ylabel('Unemployment rate (%)')
plt.title('Unemployment rate')
plt.gca().yaxis.set_major_formatter(
mticker.FuncFormatter(lambda v, _: f'{v:.1f}%')
)
plt.gca().xaxis.set_major_locator(mdates.YearLocator(4))
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%Y'))
plt.tick_params(axis='both', which='both', length=0)
plt.tight_layout()
plt.show()
Federal debt
Debt-to-GDP is the standard measure of fiscal sustainability - values above 100% have historically been associated with fiscal stress in developed economies, though the relationship is not mechanical. Total public debt is the face value of all outstanding Treasury securities. Net interest payments are a growing share of the federal budget as higher rates compound on the existing debt stock.
d2g = raw['debt_to_gdp']
tot_debt = raw['total_debt'] # millions → convert to trillions for display
int_pmts = raw['interest_pmts'] # quarterly billions → annualize ×4
curr_d2g = latest(d2g)
curr_tot_debt = latest(tot_debt)
curr_int = latest(int_pmts)
tot_debt_T = curr_tot_debt / 1e6 if curr_tot_debt is not None else None
int_ann_T = (curr_int * 4) / 1e3 if curr_int is not None else None
display(HTML(render_cards([
{'label': 'Debt / GDP', 'value': fmt_pct(curr_d2g, d=1)},
{'label': 'Total Public Debt', 'value': (f'${tot_debt_T:.1f}T' if tot_debt_T is not None else 'N/A')},
{'label': 'Net Interest (Ann.)', 'value': (f'${int_ann_T:.2f}T' if int_ann_T is not None else 'N/A')},
])))
plt.figure(figsize=(8, 4.5))
for s, label, color, lw, ls in [
(cpi, 'CPI YoY', FL_BLUE, 2.0, '-'),
(core_cpi, 'Core CPI YoY', FL_SLATE, 1.4, '--'),
(pce, 'PCE YoY', FL_AMBER, 1.4, ':'),
]:
if not s.empty:
plt.plot(
s.index,
s.values,
color=color,
linewidth=lw,
linestyle=ls,
label=label
)
plt.axhline(2.0, color=FL_GRID, linewidth=1.0, linestyle='--', label='Fed target (2%)')
plt.ylabel('Inflation (%)')
plt.title('Inflation measures (YoY)')
plt.gca().yaxis.set_major_formatter(
mticker.FuncFormatter(lambda v, _: f'{v:.1f}%')
)
plt.gca().xaxis.set_major_locator(mdates.YearLocator(4))
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%Y'))
plt.legend()
plt.tick_params(axis='both', which='both', length=0)
plt.tight_layout()
plt.show()
plt.figure(figsize=(8, 4.5))
for s, label, color, lw, ls in [
(be5, '5Y Breakeven', FL_BLUE, 1.8, '-'),
(be10, '10Y Breakeven', FL_SLATE, 1.4, '--'),
]:
if not s.empty:
plt.plot(
s.index,
s.values,
color=color,
linewidth=lw,
linestyle=ls,
label=label
)
plt.axhline(2.0, color=FL_GRID, linewidth=1.0, linestyle='--', label='Fed target (2%)')
plt.ylabel('Breakeven inflation (%)')
plt.title('Market-implied breakeven inflation')
plt.gca().yaxis.set_major_formatter(
mticker.FuncFormatter(lambda v, _: f'{v:.1f}%')
)
plt.gca().xaxis.set_major_locator(mdates.YearLocator(4))
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%Y'))
plt.legend()
plt.tick_params(axis='both', which='both', length=0)
plt.tight_layout()
plt.show()
inflation_summary_df = pd.DataFrame([
{
'Metric': 'Headline CPI',
'Value': fmt_pct(curr_cpi) if curr_cpi is not None else 'N/A'
},
{
'Metric': 'Core CPI',
'Value': fmt_pct(curr_core) if curr_core is not None else 'N/A'
},
{
'Metric': 'PCE',
'Value': fmt_pct(curr_pce) if curr_pce is not None else 'N/A'
},
{
'Metric': '5Y breakeven',
'Value': fmt_pct(curr_be5) if curr_be5 is not None else 'N/A'
},
{
'Metric': '10Y breakeven',
'Value': fmt_pct(curr_be10) if curr_be10 is not None else 'N/A'
}
])
display(inflation_summary_df)
| Metric | Value | |
|---|---|---|
| 0 | Headline CPI | 3.95% |
| 1 | Core CPI | 2.99% |
| 2 | PCE | 3.50% |
| 3 | 5Y breakeven | 2.54% |
| 4 | 10Y breakeven | 2.40% |