import pandas as pd
import numpy as np
from IPython.display import Markdown
from tulip.core import TulipLibrary
from tulip.plots import plot_lines, plot_line, switch_trace_to_secondary_axis
from tulip_mania.columns import columns
market_data_lib = TulipLibrary.market_data()
g = ["investor", "market"]
ex_ante_hedge_yields = market_data_lib.read("ex_ante_hedged_yields").data.set_index(
g, append=True
)
last_reading = ex_ante_hedge_yields.groupby(["investor", "market"]).last()
last_reading_rank = (
ex_ante_hedge_yields.groupby(g)
.rank(ascending=True, method="dense", pct=True)
.groupby(g)
.last()
)
import plotly.io as pio
pio.renderers.default = "svg" # or "png"
Hedged Yields¶
Ex-Ante Hedged Yields Matrix¶
How to read: Rows = Investor home currency | Columns = Bond market currency
Example: EUR row, USD column = Yield a EUR-based investor receives when buying USD bonds and hedging back to EUR
We calculate the total USD return of a foreign‑currency bond when you hedge the FX risk assuming 1YR rolling hedge. We start with the bond’s local‑currency return and add the FX‑forward return—computed as the one‑year USD forward rate divided by today’s USD spot rate minus 1—which captures the currency gain or cost from the forward hedge. This captures the cross-currency bias that could exist in the FX cross.
Hedged Return=Rbondforeign+(SFXUSD,spotFFXforwardUSD,1yr−1)
# ----------------------------------------------------------------------------
# Hedged Yields Matrix Visualization
# ----------------------------------------------------------------------------
# For a given tenor, display two side-by-side tables:
# - Left: Last Reading (yields) with market as columns, investor as rows
# - Right: Last Reading Rank (percentile) with same structure
# ----------------------------------------------------------------------------
def create_hedged_yield_tables(tenor="10Y"):
"""Create styled side-by-side tables for hedged yields by tenor."""
# Extract the tenor column and unstack to get market as columns, investor as rows
yield_matrix = last_reading[tenor].unstack("market")
rank_matrix = last_reading_rank[tenor].unstack("market")
# Style function for yields (green = higher yield is better)
def style_yields(df):
return (
df.style.format("{:.2%}")
.background_gradient(cmap="RdYlGn", axis=None)
.set_caption(
f"<b>{tenor} Hedged Yields</b><br><span style='font-size:11px;color:gray'>Last Reading</span>"
)
.set_table_styles(
[
{
"selector": "caption",
"props": [("font-size", "14px"), ("text-align", "center")],
},
{
"selector": "th",
"props": [("font-size", "11px"), ("text-align", "center")],
},
{
"selector": "td",
"props": [("font-size", "11px"), ("text-align", "center")],
},
]
)
)
# Style function for ranks (percentile coloring)
def style_ranks(df):
return (
df.style.format("{:.0%}")
.background_gradient(cmap="RdYlGn", axis=None, vmin=0, vmax=1)
.set_caption(
f"<b>{tenor} Historical Rank</b><br><span style='font-size:11px;color:gray'>Percentile (higher = relatively high yield)</span>"
)
.set_table_styles(
[
{
"selector": "caption",
"props": [("font-size", "14px"), ("text-align", "center")],
},
{
"selector": "th",
"props": [("font-size", "11px"), ("text-align", "center")],
},
{
"selector": "td",
"props": [("font-size", "11px"), ("text-align", "center")],
},
]
)
)
return style_yields(yield_matrix), style_ranks(rank_matrix)
def display_hedged_yield_dashboard(tenor="10Y"):
"""Display the hedged yield dashboard with side-by-side tables."""
from IPython.display import HTML, display
yield_styled, rank_styled = create_hedged_yield_tables(tenor)
# Create side-by-side HTML layout
html = f"""
<div style="display: flex; gap: 30px; justify-content: center; align-items: flex-start; flex-wrap: wrap;">
<div style="flex: 1; min-width: 350px;">
{yield_styled.to_html()}
</div>
<div style="flex: 1; min-width: 350px;">
{rank_styled.to_html()}
</div>
</div>
"""
display(HTML(html))
def analyze_and_plot_hedged_vs_local_yields(
investor: str,
market: str,
tenor: str = "10Y",
plot_source: str = "Kate Capital",
**kwargs,
) -> None:
"""
Analyzes and plots the difference between a hedged yield and a local domestic yield.
Uses the ex_ante_hedge_yields DataFrame structure with multi-index (date, investor, market).
Args:
investor (str): The investor's home currency (e.g., 'EUR', 'JPY', 'USD').
market (str): The bond market currency (e.g., 'USD', 'EUR', 'JPY').
tenor (str): The yield maturity tenor (e.g., '10Y', '5Y', '3Y', '1Y'). Defaults to '10Y'.
domestic_yield_ticker (str): Optional Bloomberg ticker for domestic yield.
If None, uses the investor's own market yield from the data.
plot_source (str): Source to display on the plot. Defaults to 'Kate Capital'.
Returns:
None: Displays the plot directly.
"""
# Extract the hedged yield series for the given investor/market/tenor
hedged_yield_series = ex_ante_hedge_yields.xs(investor, level="investor").xs(
market, level="market"
)[tenor]
hedged_yield_name = f"{market} {tenor} Hedged to {investor}"
hedged_yield_series = hedged_yield_series.rename(hedged_yield_name)
# Get domestic yield - use the investor's own market yield from the data
domestic_yield_series = (
ex_ante_hedge_yields.xs(investor, level="investor").xs(
investor, level="market"
)[tenor] # investor buying their own market = domestic
)
domestic_yield_name = f"{investor} {tenor} Domestic Yield"
domestic_yield_series = domestic_yield_series.rename(domestic_yield_name)
# Calculate the difference (hedged foreign yield - domestic yield)
diff_series_name = "Hedged vs. Local"
diff_for_yield = hedged_yield_series.sub(domestic_yield_series).rename(
diff_series_name
)
# Concatenate into a DataFrame
df = pd.concat(
[domestic_yield_series, hedged_yield_series, diff_for_yield], axis=1
).dropna()
# Create the plot title dynamically
plot_title = f"<b>{market}</b> vs <b>Local {tenor} Yields</b> for a <b>{investor}</b> Investor"
# Generate the initial plot
fig = plot_lines(
df,
title=plot_title,
source=plot_source,
show_0=True,
tick_format="0.0%",
**kwargs,
)
# Switch the difference trace to a secondary axis
switch_trace_to_secondary_axis(
fig, diff_series_name, as_area=True, show_0=True, tick_format="0.1%"
)
# Display the plot
fig.show()display_hedged_yield_dashboard("10Y")display_hedged_yield_dashboard("5Y")display_hedged_yield_dashboard("3Y")display_hedged_yield_dashboard("1Y")upper_ranges = ex_ante_hedge_yields.groupby("investor").max().max(axis=1) + 0.01
lower_ranges = ex_ante_hedge_yields.groupby("investor").min().min(axis=1) - 0.01
investor = "EUR"
yield_plots = {}
for tenor in ["1Y", "3Y", "5Y", "10Y"]:
yield_plots[tenor] = plot_lines(
ex_ante_hedge_yields.xs(investor, level="investor")[tenor].unstack("market"),
title=f"Ex-Ante {tenor} Hedged Yields",
yaxis_title="Yield",
legend_title="Investor",
top_margin=120,
show_0=True,
tick_format="0.0%",
default_y_range=(
lower_ranges[investor],
upper_ranges[investor],
),
figsize=(400, 500),
)
display(Markdown(f"##### Ex-Ante Hedged Yields for a {investor} Investor"))
with columns(4, gap="30px", vertical_alignment="center") as cols:
cols[0].plotly(yield_plots["10Y"])
cols[1].plotly(yield_plots["5Y"])
cols[2].plotly(yield_plots["3Y"])
cols[3].plotly(yield_plots["1Y"])analyze_and_plot_hedged_vs_local_yields(
investor=investor,
market="USD",
tenor="5Y",
years_limit=2,
)
investor = "JPY"
yield_plots = {}
for tenor in ["1Y", "3Y", "5Y", "10Y"]:
yield_plots[tenor] = plot_lines(
ex_ante_hedge_yields.xs(investor, level="investor")[tenor].unstack("market"),
title=f"Ex-Ante {tenor} Hedged Yields",
yaxis_title="Yield",
legend_title="Investor",
top_margin=120,
show_0=True,
tick_format="0.0%",
default_y_range=(
lower_ranges[investor],
upper_ranges[investor],
),
figsize=(400, 500),
)
display(Markdown(f"##### Ex-Ante Hedged Yields for a {investor} Investor"))
with columns(4, gap="30px", vertical_alignment="center") as cols:
cols[0].plotly(yield_plots["10Y"])
cols[1].plotly(yield_plots["5Y"])
cols[2].plotly(yield_plots["3Y"])
cols[3].plotly(yield_plots["1Y"])analyze_and_plot_hedged_vs_local_yields(
investor=investor,
market="USD",
tenor="5Y",
years_limit=2,
)investor = "GBP"
yield_plots = {}
for tenor in ["1Y", "3Y", "5Y", "10Y"]:
yield_plots[tenor] = plot_lines(
ex_ante_hedge_yields.xs(investor, level="investor")[tenor].unstack("market"),
title=f"Ex-Ante {tenor} Hedged Yields",
yaxis_title="Yield",
legend_title="Investor",
top_margin=120,
show_0=True,
tick_format="0.0%",
default_y_range=(
lower_ranges[investor],
upper_ranges[investor],
),
figsize=(400, 500),
)
display(Markdown(f"##### Ex-Ante Hedged Yields for a {investor} Investor"))
with columns(4, gap="30px", vertical_alignment="center") as cols:
cols[0].plotly(yield_plots["10Y"])
cols[1].plotly(yield_plots["5Y"])
cols[2].plotly(yield_plots["3Y"])
cols[3].plotly(yield_plots["1Y"])analyze_and_plot_hedged_vs_local_yields(
investor=investor,
market="USD",
tenor="5Y",
years_limit=2,
)from tulip_mania.notebook_related import notebook_updated
notebook_updated()