import pandas as pd
from IPython.display import Markdown, HTML
from tulip.data.haver import (
get_series as get_haver_series,
get_collection as get_haver_collections,
)
from tulip.plots import plot_lines, add_line_on_opposite_axis
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from tulip_mania.columns import columns
from tulip.plots import plot_lines, plot_line
from tulip.plots import plot_bar
import statsmodels.api as sm
# ------- Functions ---------------
COLOR_MAP = {
"Domestic Non Financial Sectors": "#000000", # black
"Households and Nonprofit Organizations": "#ff8333", # orange-500
"Government": "#3372ff", # blue-500
"Non Financial Corporations": "#ff3333", # red-500
}
def style_cc_plot(fig):
# Base colors for the main HH / NFC / Gov series
base_colors = {
"HH": "#ff8333", # blue
"NFC": "#ff3333", # green
"Gov": "#3372ff", # red
}
def lighten_hex(hex_color, factor=0.4):
"""
Lighten a #RRGGBB color by mixing it with white.
factor in (0,1): 0 = no change, 1 = white.
"""
hex_color = hex_color.lstrip("#")
r = int(hex_color[0:2], 16)
g = int(hex_color[2:4], 16)
b = int(hex_color[4:6], 16)
r_l = int(r + (255 - r) * factor)
g_l = int(g + (255 - g) * factor)
b_l = int(b + (255 - b) * factor)
return f"#{r_l:02x}{g_l:02x}{b_l:02x}"
# Apply colors based on trace name
for tr in fig.data:
name = tr.name or ""
# Decide which block: HH, NFC, Gov
if name.startswith("HH"):
base = base_colors["HH"]
elif name.startswith("NFC"):
base = base_colors["NFC"]
elif name.startswith("Gov"):
base = base_colors["Gov"]
else:
continue # leave anything else unchanged
# "o/w" traces get a lighter shade of the same color
is_sub = "o/w" in name
color = base if not is_sub else lighten_hex(base, factor=0.5)
# Set both line and marker colors if present (covers Bar/Scatter/etc.)
if hasattr(tr, "marker"):
tr.marker.color = color
if hasattr(tr, "line"):
tr.line.color = color
return figCredit Creation Monitor¶
# Costs of debt
industrials_bbplus = get_haver_series("FMLCT@USECON").ts.rename(
"BAML U.S. Corp. Bond Yield"
)
mortgage_30y = get_haver_series("MTGF30@USECON").ts.rename("FRM 30-Year Mortgage Rate")
otr_treasury_10y = get_haver_series("F10JON@USECON").ts.rename(
"U.S. 10-Year Treasury OTR Yield"
)
# Nominal GDP
us_gdp = get_haver_series("S111NGDP@G10").ts.rolling(4).sum()Saving vs. Borrowing: Who saves and who spends?¶
# Unit is SAAR billions
savings_vs_borrowing = get_haver_collections(
{
"Domestic Non Financial Sectors": "FQ38NIF5@FFUNDS",
"Households and Nonprofit Organizations": "FQ15NIF5@FFUNDS",
"Non Financial Corporations": "FQ14NIF5@FFUNDS",
"Government": "FQ36NIF5@FFUNDS",
}
)savings_vs_borrowing_df = savings_vs_borrowing.df.rename(
columns=savings_vs_borrowing.titles
)
savings_vs_borrowing_df = savings_vs_borrowing_df.loc["1970":,]
savings_vs_borrowing_df_pgdp = savings_vs_borrowing_df.truediv(
us_gdp.loc["1970":,], axis=0
)
ma = savings_vs_borrowing_df_pgdp.rolling(4).mean()
ma = ma.add_suffix(" (4Q MA)")
df_out = pd.concat([savings_vs_borrowing_df_pgdp, ma], axis=1)MAIN_SERIES = "Domestic Non Financial Sectors"
WINDOW = 4
fig = plot_lines(
df_out,
show_0=True,
tick_format="0.0%",
years_limit=10,
top_margin=180,
figsize=(900, 500),
title="<b>U.S. Savings vs. Borrowing by Sector (Percent of GDP)</b> <br>Net Lending (+) or Borrowing (-)",
)
for tr in fig.data:
# Base name = strip MA suffix if present
base = tr.name.replace(" (4Q MA)", "")
if base not in COLOR_MAP:
continue
tr.legendgroup = base
tr.line.color = COLOR_MAP[base]
if tr.name.endswith("(4Q MA)"):
tr.showlegend = True
tr.line.width = 3
tr.visible = True if base == MAIN_SERIES else "legendonly"
else:
tr.showlegend = False
tr.line.width = 1
tr.visible = True if base == MAIN_SERIES else False
fig.update_layout(
legend=dict(
font=dict(size=10),
itemsizing="constant",
itemwidth=30,
tracegroupgap=2,
)
)
fig.show()df_to_show = ma.dropna(how="all").iloc[:, 1:]
df_to_show.columns = [
"HH",
"NFC",
"Gov",
]
last_row = df_to_show.iloc[-1]
four_periods_prior = df_to_show.iloc[-5]
change = last_row - four_periods_prior
def lighten_hex(hex_color, factor=0.4):
"""Lighten a #RRGGBB color by mixing it with white."""
hex_color = hex_color.lstrip("#")
r, g, b = (int(hex_color[i : i + 2], 16) for i in (0, 2, 4))
r_l = int(r + (255 - r) * factor)
g_l = int(g + (255 - g) * factor)
b_l = int(b + (255 - b) * factor)
return f"#{r_l:02x}{g_l:02x}{b_l:02x}"
colors_recent = ["#ff8333", "#ff3333", "#3372ff"] # NFC, HH, Gov
colors_prior = [lighten_hex(c, factor=0.5) for c in colors_recent]
bar_chart = plot_bar(
pd.DataFrame(
{
"Four Periods Prior": four_periods_prior,
"Most Recent Period": last_row,
}
),
tick_format="0.1%",
top_margin=100,
figsize=(400, 600),
title="Most Recent vs. Four Periods Prior",
)
bar_chart.data[0].marker.color = colors_prior
bar_chart.data[1].marker.color = colors_recent
bar_chart.update_layout(
template="plotly_white",
height=500,
width=500,
font=dict(
family='"Avenir Next LT Pro", "Open Sans", verdana, arial, sans-serif',
color="#333133",
),
yaxis=dict(tickformat="0.1%", zeroline=True, zerolinecolor="gray"),
)
# Calculate the change from 4 periods prior for the last row
# Create a bar chart
chg_fig = go.Figure()
chg_fig.add_trace(
go.Bar(
x=change.index,
y=change.values,
marker_color=["#ff8333", "#ff3333", "#3372ff"],
text=[f"{v:.2%}" for v in change.values],
textposition="outside",
)
)
chg_fig.update_layout(
title="Diff",
xaxis_title="Sector",
yaxis_title="Change (% of GDP)",
template="plotly_white",
height=500,
width=500,
showlegend=False,
yaxis=dict(tickformat="0.1%", zeroline=True, zerolinecolor="gray"),
)
display(
HTML(
"<center><b> U.S. Savings vs. Borrowing by Sector (Percent of GDP)</b> <br>Net Lending (+) or Borrowing (-)</center>"
)
)
with columns(2, gap="1px", vertical_alignment="bottom") as cols:
cols[0].plot(bar_chart)
cols[1].plot(chg_fig)sub_fig = {}
for g in ["HH", "NFC", "Gov"]:
fig = plot_lines(
df_to_show.filter(like=g),
years_limit=10,
default_y_range=(-0.1, 0.3),
figsize=(400, 600),
# title=f"<b>US {g} Credit Creation as % of GDP</b>",
top_margin=170,
tick_format="0.1%",
show_0=True,
)
style_cc_plot(fig)
sub_fig[g] = fig
sub_fig["HH"] = add_line_on_opposite_axis(
sub_fig["HH"],
mortgage_30y,
years_limit=5,
invert=True,
color="black",
tick_suffix={"y1": "%", "y2": "%"},
)
sub_fig["Gov"] = add_line_on_opposite_axis(
sub_fig["Gov"],
otr_treasury_10y,
years_limit=5,
invert=True,
color="black",
tick_suffix={"y1": "%", "y2": "%"},
)
sub_fig["NFC"] = add_line_on_opposite_axis(
sub_fig["NFC"],
industrials_bbplus,
years_limit=5,
invert=True,
color="black",
tick_suffix={"y1": "%", "y2": "%"},
)
display(
HTML(
"<center>Against approximate borrow costs <br><sup>Positive values indicate <b>net saving</b>, negative values indicate <b>net borrowing</b></sup></center>"
)
)
with columns(3, gap="1px", vertical_alignment="bottom") as cols:
cols[0].plot(sub_fig["HH"])
cols[1].plot(sub_fig["NFC"])
cols[2].plot(sub_fig["Gov"])nfc_dict = {
"Net Acquisition of Assets": "FA14TAO5@FFUNDS",
"Net Acquisition of Liabilities": "FL14TAO5@FFUNDS",
"Net Debt Securities": "FL10AIJ5@FFUNDS",
"Net Loans": "FL14ABN5@FFUNDS",
"Net Equity": "FL14ADS5@FFUNDS",
"Net Lending/Saving": "FQ14NIF5@FFUNDS",
}
nfc_collection = get_haver_collections(nfc_dict)
nfc_collection_df = nfc_collection.df.rename(columns=nfc_collection.titles)
nfc_collection_df = nfc_collection_df.loc["1970":,]
nfc_collection_df_pgdp = nfc_collection_df.truediv(us_gdp.loc["1970":,], axis=0)
YEARS_LIMIT = 10
fig = plot_lines(
nfc_collection_df_pgdp.loc[
:, ["Net Acquisition of Liabilities", "Net Debt Securities", "Net Loans"]
],
years_limit=YEARS_LIMIT,
connect_gaps=True,
tick_format="0.1%",
title="<b>U.S. Non-Financial Corporations: Credit Creation Components (% of GDP)</b>",
top_margin=120,
axis_title="% of GDP",
)
for tr, color in zip(fig.data, ["#ff8333", "#d39973", "#ac988b"]):
# Base name = strip MA suffix if present
tr.line.color = color
tr.line.width = 3
if tr.name == "Net Acquisition of Liabilities":
tr.visible = "legendonly"
fig = add_line_on_opposite_axis(
fig,
otr_treasury_10y,
invert=True,
years_limit=YEARS_LIMIT,
color="crimson",
line_width=2,
tick_suffix={"y1": "%", "y2": "%"},
)
fig = add_line_on_opposite_axis(
fig,
industrials_bbplus,
invert=True,
color="black",
years_limit=YEARS_LIMIT,
line_width=2,
tick_suffix={"y1": "%", "y2": "%"},
axis_title="Yield (%)",
)
# fig.update_yaxes(title_text="% of GDP", secondary_y=False)
# fig.update_yaxes(title_text="Yield (%)", secondary_y=True)
fig.show()Current composition of the actual flow of funds¶
How to Read This Chart
The chart below shows flow-of-funds transactions — actual flows of funds, not changes in levels (which would also include valuation effects). We show the last 12.
The two main aggregates are:
Net Acquisition of Assets — financial asset transactions
Net Acquisition of Liabilities — liability transactions
Net acquisition of liabilities, plus net equity issuance and net lending/saving, should equal net acquisition of assets.
The chart then shows two components of net acquisition of liabilities:
Net Debt Securities — bonds and other tradable debt instruments
Net Loans — bank loans and other non-securitized borrowing
Other liability components (e.g., trade payables, other accounts) are not shown but would complete the total.
pit = nfc_collection_df.rolling(4).mean().iloc[-1, :]
ngdp = us_gdp.rolling(4).mean().iloc[-1]
fig = plot_bar(
nfc_collection_df.rolling(4).mean().iloc[-1, :].rename("Last 4 Quarters"),
default_y_range=(-500, 2000),
figsize=(800, 500),
top_margin=120,
title="<b>U.S. Non-Financial Corporations: Components of Net Lending/Borrowing (SAAR Billions)</b><br><sup>Seasonally Adjusted Annual Rate</sup>",
)
fig.update_traces(
text=[f"{v:,.1f}bln ({v / ngdp:.2%})" for v in pit], textposition="outside"
)Bond Channel¶
# Bond Channel - SIFMA Corporate Bond Issuance
from scipy import stats
url_sifma = "https://www.sifma.org/wp-content/uploads/2024/01/US-Corporate-Bonds-Statistics-SIFMA.xlsx"
sifma_issuance_data = pd.read_excel(
url_sifma, sheet_name="Issuance", skiprows=6, header=[0, 1, 2]
)
# Extract period and total columns (column 0 = periods, column 13 = TOTAL)
periods = sifma_issuance_data.iloc[:, 0]
total = sifma_issuance_data.iloc[:, 13]
# Split by frequency (Column A has mixed: years, YTD, quarters, monthly)
annual_df = pd.DataFrame(
{"Year": periods.iloc[:11].astype(int), "Total": total.iloc[:11]}
)
ytd_df = pd.DataFrame({"Period": periods.iloc[12:14], "Total": total.iloc[12:14]})
quarterly_df = pd.DataFrame(
{"Quarter": periods.iloc[15:24], "Total": total.iloc[15:24]}
)
monthly_df = pd.DataFrame(
{"Date": pd.to_datetime(periods.iloc[25:38]), "Total": total.iloc[25:38]}
).reset_index(drop=True)
# Annual Trend with CAGR
fig_annual = go.Figure()
fig_annual.add_trace(
go.Bar(
x=annual_df["Year"],
y=annual_df["Total"],
marker_color=[
"#aec7e8"
if y < 2020
else "#ff7f0e"
if y == 2020
else "#1f77b4"
if y < 2024
else "#2ca02c"
for y in annual_df["Year"]
],
text=[f"${v:,.0f}B" for v in annual_df["Total"]],
textposition="outside",
textfont=dict(size=11),
hovertemplate="<b>%{x}</b><br>Issuance: $%{y:,.1f}B<extra></extra>",
)
)
# Add trend line
x_numeric = annual_df["Year"].values
y_values = annual_df["Total"].values
slope, intercept, r, p, se = stats.linregress(x_numeric, y_values)
trend_line = slope * x_numeric + intercept
fig_annual.add_trace(
go.Scatter(
x=annual_df["Year"],
y=trend_line,
mode="lines",
line=dict(color="red", dash="dash", width=2),
name="Trend",
hoverinfo="skip",
)
)
cagr = (
(annual_df["Total"].iloc[-1] / annual_df["Total"].iloc[0]) ** (1 / 10) - 1
) * 100
fig_annual.update_layout(
title=dict(
text=f"<b>US Corporate Bond Issuance - Annual Trend</b><br><sup>Source: SIFMA | CAGR (2014-2024): {cagr:.1f}%</sup>",
x=0.5,
font=dict(size=16),
),
xaxis_title="Year",
yaxis_title="Issuance ($B)",
template="plotly_white",
height=450,
width=900,
showlegend=False,
yaxis=dict(range=[0, max(annual_df["Total"]) * 1.15]),
)
fig_annual.add_annotation(
x=2020,
y=annual_df[annual_df["Year"] == 2020]["Total"].values[0],
text="COVID<br>Spike",
showarrow=True,
arrowhead=2,
ax=0,
ay=-50,
font=dict(size=10, color="#ff7f0e"),
)
fig_annual.show()# Comprehensive Dashboard - All Frequencies
fig = make_subplots(
rows=2,
cols=2,
subplot_titles=(
"<b>Annual Issuance</b>",
"<b>YTD Comparison (Jan-Nov)</b>",
"<b>Quarterly Issuance</b>",
"<b>Monthly Issuance (Recent)</b>",
),
vertical_spacing=0.15,
horizontal_spacing=0.1,
)
primary_color, highlight_color, accent_color = "#1f77b4", "#2ca02c", "#ff7f0e"
# 1. Annual
colors_annual = [primary_color] * len(annual_df)
colors_annual[-1] = highlight_color
colors_annual[6] = accent_color
fig.add_trace(
go.Bar(
x=annual_df["Year"],
y=annual_df["Total"],
marker_color=colors_annual,
text=annual_df["Total"].round(0).astype(int),
textposition="outside",
textfont_size=9,
name="Annual",
),
row=1,
col=1,
)
# 2. YTD
ytd_colors = [primary_color, highlight_color]
fig.add_trace(
go.Bar(
x=["YTD 2024", "YTD 2025"],
y=ytd_df["Total"].values,
marker_color=ytd_colors,
text=[f"${v:,.0f}B" for v in ytd_df["Total"].values],
textposition="outside",
textfont_size=12,
name="YTD",
),
row=1,
col=2,
)
ytd_change = (ytd_df["Total"].values[1] / ytd_df["Total"].values[0] - 1) * 100
fig.add_annotation(
x=0.5,
y=max(ytd_df["Total"].values) * 1.15,
text=f"<b>+{ytd_change:.1f}% YoY</b>",
showarrow=False,
font=dict(size=14, color=highlight_color),
xref="x2",
yref="y2",
)
# 3. Quarterly
quarterly_colors = [
highlight_color
if "25" in str(q)
else primary_color
if "24" in str(q)
else "#aec7e8"
for q in quarterly_df["Quarter"]
]
fig.add_trace(
go.Bar(
x=quarterly_df["Quarter"],
y=quarterly_df["Total"],
marker_color=quarterly_colors,
text=quarterly_df["Total"].round(0).astype(int),
textposition="outside",
textfont_size=9,
name="Quarterly",
),
row=2,
col=1,
)
# 4. Monthly
fig.add_trace(
go.Bar(
x=monthly_df["Date"].dt.strftime("%b %Y"),
y=monthly_df["Total"],
marker_color=primary_color,
opacity=0.7,
name="Monthly",
),
row=2,
col=2,
)
fig.add_trace(
go.Scatter(
x=monthly_df["Date"].dt.strftime("%b %Y"),
y=monthly_df["Total"].rolling(3, min_periods=1).mean(),
mode="lines+markers",
line=dict(color=accent_color, width=2),
marker=dict(size=6),
name="3M Avg",
),
row=2,
col=2,
)
fig.update_layout(
height=700,
width=1100,
title=dict(
text="<b>US Corporate Bond Issuance</b><br><sup>Source: SIFMA ($ Billions)</sup>",
x=0.5,
font=dict(size=18),
),
showlegend=False,
template="plotly_white",
)
fig.update_yaxes(title_text="$B", row=1, col=1)
fig.update_yaxes(title_text="$B", row=1, col=2)
fig.update_yaxes(title_text="$B", row=2, col=1)
fig.update_yaxes(title_text="$B", row=2, col=2)
fig.update_xaxes(tickangle=-45, row=2, col=1)
fig.update_xaxes(tickangle=-45, row=2, col=2)
fig.show()Assessing the potential direction from here:
def qtr_to_qe(q: str) -> pd.Timestamp:
"""'4Q23' -> 2023-12-31"""
qtr, yr = int(q[0]), int(q[2:])
yr += 2000 if yr < 100 else 0
return pd.Timestamp(year=yr, month=qtr * 3, day=1) + pd.offsets.MonthEnd(0)
quarterly_df["date"] = quarterly_df["Quarter"].apply(qtr_to_qe)
quarterly_gross_issuance = quarterly_df.set_index("date")["Total"].rename(
"SIFMA US Corporate Bond Issuance"
)net_debt_securities_against_issuance = pd.concat(
[
nfc_collection_df["Net Debt Securities"],
quarterly_df.set_index("date")["Total"].rename(
"SIFMA US Corporate Bond Issuance"
),
],
axis=1,
).loc[quarterly_gross_issuance.first_valid_index() :,]
diff_in_issuance = net_debt_securities_against_issuance.diff().iloc[-1, 1]
stat_df = net_debt_securities_against_issuance.diff().dropna()
X = stat_df["SIFMA US Corporate Bond Issuance"]
y = stat_df["Net Debt Securities"]
res = sm.OLS(y, X).fit()
beta = res.params
fig = plot_line(
blue=net_debt_securities_against_issuance["Net Debt Securities"],
red=net_debt_securities_against_issuance["SIFMA US Corporate Bond Issuance"],
align_z=True,
)
prior_idx = net_debt_securities_against_issuance.index[-2]
last_idx = net_debt_securities_against_issuance.index[-1]
last_val = net_debt_securities_against_issuance["Net Debt Securities"].iloc[-2]
proj_val = (last_val + (diff_in_issuance * beta)).squeeze()
fig.add_scatter(
x=[prior_idx, last_idx], # or offset second: last_idx + pd.DateOffset(months=3)
y=[last_val, proj_val],
mode="lines+markers",
marker=dict(color="blue", size=10),
line=dict(color="blue", dash="dash"),
showlegend=False,
)
fig.show()Loans Channel¶
From Financial Institutions¶
Point to an acceleration - though the bank channel seems weak
banks_loans_CI = get_haver_series("FABWCN@USECON").ts.rename(
"Banks Loans to NFCs"
) # this is a level
banks_loans_CI_net_change = banks_loans_CI.resample("QE").last().diff(1)
pd.concat(
[
nfc_collection_df["Net Loans"],
banks_loans_CI_net_change,
],
axis=1,
)
plot_line(
blue=nfc_collection_df["Net Loans"], red=banks_loans_CI_net_change, years_limit=12
)Private Credit Channel¶
This is capital raising of US BDC can be used as proxy of the whole market. This data is of Q3 2025.
Source Raymond James
import pandas as pd
import matplotlib.pyplot as plt
data = {
"Year": [
"2015",
"2016",
"2017",
"2018",
"2019",
"2020",
"2021",
"2022",
"2023",
"2024",
"YTD 2024",
"YTD 2025",
],
"Capital Raised ($M)": [
1989,
1596,
4347,
2635,
4876,
8509,
14790,
3369,
4064,
10267,
9947,
9828,
],
}
df = pd.DataFrame(data)
# Define colors - darker blue for YTD entries
colors = ["#1f77b4" if "YTD" not in year else "#003f7f" for year in df["Year"]]
fig, ax = plt.subplots(figsize=(12, 6))
df.set_index("Year")["Capital Raised ($M)"].plot(kind="bar", ax=ax, color=colors)
ax.set_ylabel("Capital Raised ($M)")
ax.set_title("Public BDC Financing Activity")
plt.tight_layout()
plt.show()Another possible method is Bloomberg US Loan Index’s market valuem
from tulip.data.bloomberg import BloombergClient as bb
market_val_us_lev_loans = bb.bdh(ticker="I38932US Index", fields=["INDX_MARKET_CAP"])
banks_loans_CI = get_haver_series("FABWCN@USECON").ts.rename(
"Banks Loans to NFCs"
) # this is a level
leveraged_loans_CI_net_change = (
market_val_us_lev_loans.resample("QE").last().diff().squeeze()
)
pd.concat(
[
nfc_collection_df["Net Loans"],
leveraged_loans_CI_net_change.rename("Leveraged Loans Market Value Change")
/ 1e6,
],
axis=1,
)
plot_line(
blue=nfc_collection_df["Net Loans"],
red=leveraged_loans_CI_net_change,
years_limit=5,
)hh_dict = {
"Net Acquisition of Assets": "FA15TAO5@FFUNDS",
"Net Acquisition of Liabilities": "FL15TAO5@FFUNDS",
"Net Home Mortgages": "FL15HOM5@FFUNDS",
"Net Consumer Credit": "FL15CNC0@FFUNDS",
"Net Lending/Saving": "FQ15NIF5@FFUNDS",
}
hh_collection = get_haver_collections(hh_dict)
hh_collection_df = hh_collection.df.rename(columns=hh_collection.titles)
hh_collection_df = hh_collection_df.loc["1970":,]
hh_collection_df_pgdp = hh_collection_df.truediv(us_gdp.loc["1970":,], axis=0)
YEARS_LIMIT = 10
fig = plot_lines(
hh_collection_df_pgdp.loc[
:,
["Net Acquisition of Liabilities", "Net Home Mortgages", "Net Consumer Credit"],
],
years_limit=YEARS_LIMIT,
connect_gaps=True,
tick_format="0.1%",
title="<b>U.S. Households: Credit Creation Components (% of GDP)</b>",
top_margin=120,
axis_title="% of GDP",
)
for tr, color in zip(fig.data, ["#ff8333", "#d39973", "#ac988b"]):
# Base name = strip MA suffix if present
tr.line.color = color
tr.line.width = 3
if tr.name == "Net Acquisition of Liabilities":
tr.visible = "legendonly"
fig = add_line_on_opposite_axis(
fig,
otr_treasury_10y,
invert=True,
years_limit=YEARS_LIMIT,
color="crimson",
line_width=2,
axis_title="",
tick_suffix={"y1": "%", "y2": "%"},
)
fig = add_line_on_opposite_axis(
fig,
mortgage_30y,
invert=True,
color="black",
years_limit=YEARS_LIMIT,
line_width=2,
axis_title="Yield (%)",
tick_suffix={"y1": "%", "y2": "%"},
)
fig.show()Current State¶
pit = hh_collection_df.rolling(4).mean().iloc[-1, :]
ngdp = us_gdp.rolling(4).mean().iloc[-1]
fig = plot_bar(
hh_collection_df.rolling(4).mean().iloc[-1, :].rename("Last 4 Quarters"),
default_y_range=(-500, 5000),
figsize=(800, 500),
top_margin=120,
title="<b> Households and Non-profits. Components of Net Lending/Borrowing (SAAR Billions)</b><br><sup>Seasonally Adjusted Annual Rate</sup>",
)
fig.update_traces(
text=[f"{v:,.1f}bln ({v / ngdp:.2%})" for v in pit], textposition="outside"
)From the H.8 release of the Federal Reserve, we can analyze household debt growth as a percentage of GDP. The frequency is monthly.
hh_debt_index = {"Real Estate": "FABWRN@USECON", "Consumer Loans": "FABWQN@USECON"}
hh_debt_collection = get_haver_collections(hh_debt_index)
hh_debt_df = hh_debt_collection.df.rename(columns=hh_debt_collection.titles)
hh_debt_df = hh_debt_df.loc["1970":,]
hh_debt_df_pgdp = hh_debt_df.truediv(
us_gdp.loc["1970":,].resample("ME").interpolate(), axis=0
)
hh_debt_df_chg_pgdp = hh_debt_df.diff(12).truediv(
us_gdp.loc["1970":,].resample("ME").interpolate(), axis=0
)plot_lines(
hh_debt_df_chg_pgdp,
years_limit=10,
title="<b>Household Debt Growth (YoY Change as % of GDP)</b>",
tick_format="0.2%",
show_0=True,
top_margin=120,
)Mortgage¶
MBA US Purchase Index: Weekly index from the Mortgage Bankers Association measuring the volume of mortgage applications for home purchases; a leading indicator of housing demand and residential activity.
MBA US Refinance Index: Weekly MBA index measuring the volume of mortgage refinancing applications; primarily reflects households’ response to interest rate changes rather than underlying housing demand.
from tulip.data.bloomberg import BloombergClient as bb
mba_indeces = bb.create_collection(
{
"MBA US Purchase Index": "MBAVPRCH Index",
"MBA US Refi Index": "MBAVREFI Index",
}
)
mba_indices_df = mba_indeces.df
mba_indices_df.columns = ["MBA US Purchase Index", "MBA US Refinance Index"]
plot_line(
blue=mba_indices_df.iloc[:, 0],
red=mba_indices_df.iloc[:, 1],
years_limit=5,
title="MBA US Mortgage Indices",
)
Consumer Credit¶
# Billions
hh_credit_creation_series = {
"Consumer Credit": "FOTN@USECON",
"Student Loans": "FOSN@USECON",
"Motor Vehicle Loans": "FOMVN@USECON",
}
# Fetch the collection
hh_credit_creation_series_collection = get_haver_collections(hh_credit_creation_series)
hh_credit_creation_series_df = hh_credit_creation_series_collection.df
hh_credit_creation_series_df.columns = (
hh_credit_creation_series_collection.titles.values()
)
hh_credit_creation_series_df = hh_credit_creation_series_df.ffill().diff(12)
hh_credit_creation_series_df = hh_credit_creation_series_df.div(
us_gdp.reindex(hh_credit_creation_series_df.index).ffill(), axis=0
)
plot_lines(
hh_credit_creation_series_df,
years_limit=10,
title="<b>Household Credit Creation as % of GDP</b>",
tick_format="0.1%",
show_0=True,
)Can we nowcast changes in household borrowing using the Fed H8’s release?
plot_line(
blue=hh_collection_df["Net Home Mortgages"],
red=hh_debt_df["Real Estate"].resample("QE").last().diff(),
years_limit=20,
)and consumer loans?
plot_line(
blue=hh_collection_df["Net Consumer Credit"],
red=hh_debt_df["Consumer Loans"].resample("QE").last().diff(),
years_limit=10,
)from tulip_mania.notebook_related import notebook_updated
notebook_updated()