import pandas as pd
from IPython.display import Markdown
from tulip.data.bloomberg import BloombergClient as bb
from tulip.data.haver import get_series
from tulip.plots import plot_line, plot_lines, plot_bar
import numpy as np
from inflection import titleize
from tulip.libraries.gadgets.nairu import calculate_nairu_estimate
from tulip.analysis.bond_related.analytics import analyze_yield_series
from tulip.data.haver import (
get_cpi,
get_inflation_target,
get_urate,
get_slack,
get_pot_gdp,
get_rgdp,
get_rstar_fs,
get_rstar_hlw,
)
import plotly.offline as pyo
pyo.init_notebook_mode(connected=True)
idx = pd.IndexSliceCanada¶
# Country
ctry_2 = "CA"
ctry_3 = "CAN"
ccy = "CAD"
ctry_name = "Canada"
ccy_2 = "CD"
# Inflation and Unemployment
inflation = get_cpi(ctry_3)
intlation_target = get_inflation_target(ctry_3)
inflation_6m_ann = (inflation.ts.pct_change(6) * 2) * 100
inflation_6m_ann.name = "inflation_6m_ann"
# intlation_swap_1y = bb.bdh(f"{ccy_2}SWIT1 Curncy")
# intlation_swap_5y = bb.bdh(f"{ccy_2}SWIT5 Curncy")
# intlation_swap_10y = bb.bdh(f"{ccy_2}SWIT10 Curncy")
# inflation_swap_5y5y = intlation_swap_10y.iloc[:,0].rename('10y').mul(2).sub(intlation_swap_5y.iloc[:,0].rename('5y')).rename('inflation_swap_5y5y')
inflation_bei_5y = bb.bdh("CDGGBE05 Index")
inflation_bei_10y = bb.bdh("CDGGBE10 Index")
urate = get_urate(ctry_3)
nairu_estimate = calculate_nairu_estimate(inflation, urate, intlation_target)
# nairu_cb = hv.get_series("NAIRUQ@USECON")
# Grwoth Conditions
slack = get_slack(ctry_3)
rgdp = get_rgdp(ctry_3)
potential_gdp = get_pot_gdp(ctry_3) # esto esta mal
try:
potential_gdp_YoY = potential_gdp.ts.pct_change(4).loc["2025"].mean()
except:
potential_gdp_YoY = potential_gdp.ts.pct_change(4).iloc[-4:].mean()
# Interest Rates
interest_rates = bb.get_govt_yields(ccy)
overnight = (
bb.get_collected_item(collection="OVERNIGHT_RATES", key=ctry_2)
.iloc[:, 0]
.rename("O/N Rate")
)
bill_rate = (
bb.get_collected_item(collection="BILL_RATES", key=ctry_2)
.iloc[:, 0]
.rename("3M rate")
)
term_premium = (
interest_rates.loc[:, pd.IndexSlice[:, :, "10Y"]].squeeze()
- interest_rates.loc[:, pd.IndexSlice[:, :, "2Y"]].squeeze()
)
term_premium.name = "term_premium"
real_bill_rate = (
bill_rate.resample("ME")
.last()
.sub(inflation_6m_ann)
.rename("T-Bill minus inflation")
)
real_overnight = (
overnight.resample("ME")
.last()
.sub(inflation_6m_ann)
.rename("Overnight rate minus inflation")
)
short_rate_by_2025 = bb.bdp("CA0BFR DEC2025 Index")["px_last"].squeeze()
# R-Star Data
rstar_hlw = get_rstar_hlw(ctry_3)
rstar_fs = get_rstar_fs(ctry_3)
The R-Star of Market Participants is included in the survey of market participants here.
1. Macro situation¶
conditions = pd.Series(
{
"inflation": inflation_6m_ann.iloc[-1],
"target": intlation_target.ts.iloc[-1],
"inflation_gap": inflation_6m_ann.iloc[-1] - intlation_target.ts.iloc[-1],
"unemployment": urate.ts.iloc[-1],
"nairu": nairu_estimate,
"unemployment_gap": urate.ts.iloc[-1] - nairu_estimate,
"slack": slack.ts.iloc[-1],
}
)conditions.rename({k: titleize(k) for k in conditions.index}).to_frame().T.style.format(
precision=2
).hide(axis="index").set_caption(f"Current Macro Conditions for the {ctry_name}")inflation_target_fig = plot_lines(
[inflation_6m_ann, intlation_target.ts.rename("Inflation Target")],
show_0=True,
years_limit=20,
title=f"<b>{ctry_name} Inflation vs Target</b>",
source="Haver, Kate Capital",
tick_suffix="%",
)
slack_fig = plot_line(
blue=slack.ts.rename("Slack"),
red=urate.ts.rename("Unemployment Rate"),
show_0=True,
invert_red=True,
years_limit=20,
title=f"<b>{ctry_name} Unemployment Rate vs Slack</b>",
source="Haver, Kate Capital",
tick_suffix="%",
)
inflation_target_fig.show()
slack_fig.show()# display_two_charts(inflation_target_fig, slack_fig)2. Defining the Neutral Rate¶
rstar_hlw_ts = rstar_hlw.ts.rename("R-Star HLW")
rstar_fs_ts = rstar_fs.ts.rename("R-Star FS")
rstar_survey_ts = (
pd.Series([2.75], index=["04-28-2025"]).sub(2).dropna().rename("R-Star Survey")
) # from https://www.bankofcanada.ca/2025/04/market-participants-survey-first-quarter-of-2025/#23-What-is-your-estimate-of-the-longterm-nominal-neutral-rate-in-Canada
# rstar_survey_ts = rstar_survey.ts.resample('ME').last().ffill().sub(2).dropna().rename('R-Star Survey')
inflation_6m_ann = (inflation.ts.pct_change(6) * 2) * 100
inflation_6m_ann.name = "inflation_6m_ann"
real_bill_rate = (
bill_rate.resample("ME")
.last()
.sub(inflation_6m_ann)
.rename("T-Bill minus inflation")
)
real_overnight = (
overnight.resample("ME")
.last()
.sub(inflation_6m_ann)
.rename("Overnight rate minus inflation")
)
hlw_fig = plot_lines(
[real_bill_rate, real_overnight, rstar_hlw_ts],
show_0=True,
years_limit=40,
title="<b>US Real Rates vs R-Star HLW (Holston-Laubach-Williams)</b>",
source="Bloomberg, FED, Kate Capital",
tick_suffix="%",
)
hlw_fig.show()
fs_fig = plot_lines(
[real_bill_rate, real_overnight, rstar_fs_ts],
show_0=True,
years_limit=5,
title=f"<b>{ctry_name} Real Rates vs R-Star FS (Ferreira and Shousha) </b>",
source="Bloomberg, FED, Kate Capital",
tick_suffix="%",
)
fs_fig.show()
survey_fig = plot_lines(
[real_bill_rate, real_overnight, rstar_survey_ts],
show_0=True,
years_limit=5,
title=f"<b>{ctry_name} Real Rates vs Survey </b>",
source="Bloomberg, RBA, Kate Capital",
tick_suffix="%",
)
survey_fig.show()
Latest R-Star Estimates:
current_rstar = pd.Series(
{
"HLW Estimate": rstar_hlw_ts.iloc[-1].squeeze(),
"FS Estimate": rstar_fs_ts.iloc[-1].squeeze(),
"Survey Estimate": rstar_survey_ts.iloc[-1].squeeze(),
# "Real GDP YoY": real_gdp_2.iloc[-1].squeeze(),
# # "Real GDP YoY Median": real_gdp.rolling(4*5).median().ffill().iloc[-1],
}
)
current_rstar["Average of three"] = current_rstar.mean()
fig = plot_bar(
current_rstar,
colors=["#348ABD", "#348ABD", "#348ABD", "#1A455E"],
title=f"{ctry_name} Real Neutral Rate Estimates",
default_y_range=(0.5, 2),
# figsize=(400, 400),
)
fig.show()3. Current Short Rate vs. A Neutral Stance¶
# Create a comparison of current short rate vs neutral rate estimates
short_rate_comparison = pd.Series(
{
"R-Star + Inflation Target": current_rstar["Average of three"]
+ intlation_target.ts.iloc[-1],
"R-Star + 5Y BEI": current_rstar["Average of three"]
+ inflation_bei_5y.iloc[-1, 0],
"R-Star + 10Y BEI": current_rstar["Average of three"]
+ inflation_bei_10y.iloc[-1, 0],
}
)
short_rate_comparison["Average of three"] = short_rate_comparison.mean()
short_rate_comparison["Short Rate by 2025"] = short_rate_by_2025
short_rate_comparison["Current Short Rate"] = overnight.dropna().iloc[-1]
colors = [
"#348ABD",
"#348ABD",
"#348ABD",
"#1A455E",
"#093F5E",
"#952a2a",
]
fig = plot_bar(
short_rate_comparison,
colors=colors,
title=f"{ctry_name} Short Rates - Actual vs. Estimates",
default_y_range=(0, 4.5),
)
fig.show()4. Average Steepness¶
ir_10y = interest_rates.loc[:, pd.IndexSlice[:, :, "10Y"]].squeeze()
ir_2y = interest_rates.loc[:, pd.IndexSlice[:, :, "2Y"]].squeeze()
ir_3m = bill_rate
ir_10y2y = ir_10y - ir_2y
ir_10y3m = ir_10y - ir_3m
fig = plot_lines(
[ir_10y2y.rename("10Y-2Y"), ir_10y3m.rename("10Y-3M")],
show_0=True,
years_limit=20,
title=f"{ctry_name} Yield Curve Steepness",
source="Bloomberg, FED, Kate Capital",
tick_suffix="%",
)
median_10y2y = ir_10y2y.median()
median_10y3m = ir_10y3m.median()
fig.add_hline(y=median_10y2y, line_color="#4275b1", opacity=1, line_width=1)
fig.add_hline(y=median_10y3m, line_color="#ea862a", opacity=1, line_width=1)
fig.show()yield_steepnes = analyze_yield_series(ir_10y3m)
yield_steepnes.to_frame().T.style.format(precision=2).hide(axis="index").set_caption(
"US Yield Curve Steepness"
)5. Long Run Neutral Rate¶
long_rate_comparison = pd.Series(
{
"Short Rate Estimate + FH Term Premium": short_rate_comparison[
"Average of three"
]
+ yield_steepnes["Full History Avg"],
"Short Rate Estimate + Since 2009 Avg TP": short_rate_comparison[
"Average of three"
]
+ yield_steepnes["Since 2009 Avg"],
"Short Rate Estimate + Since 2022 Avg TP": short_rate_comparison[
"Average of three"
]
+ yield_steepnes["Since 2022 Avg"],
# "Current Long Rate": ir_10y.dropna().iloc[-1],
},
name="Long Rate",
)
long_rate_comparison["Average of three"] = long_rate_comparison.mean()
long_rate_comparison["Current Long Rate"] = ir_10y.dropna().iloc[-1]
colors = [
"#348ABD",
"#348ABD",
"#348ABD",
"#1A455E",
"#952a2a",
] # ["#8bb068", "#E24A33", "#348ABD", "#988ED5", "#FFB000"]
fig = plot_bar(
long_rate_comparison,
title=f"{ctry_name} Long Rates - Actual vs. Estimates",
colors=colors,
default_y_range=(2, 6),
figsize=(800, 600),
)
fig.show()Notes/Links/Bibliography¶
from tulip_mania.notebook_related import notebook_updated
notebook_updated()