TIPS#

Treasury Inflation Protected Securities (TIPS)

  • Treasury notes and bonds (no bills)

  • Semiannual coupon

  • Issued since 1997

Inflation protection#

TIPS provide a hedge against inflation.

  • Face value is scaled by CPI

  • Coupon rate is fixed

  • Fixed coupon rate multiplies the (CPI-adjusted) face-value, which leads to an inflation-adjusted coupon

import pandas as pd

import matplotlib.pyplot as plt
%matplotlib inline
plt.rcParams['figure.figsize'] = (10,6)
plt.rcParams['font.size'] = 15
plt.rcParams['legend.fontsize'] = 13

info_tips = pd.read_excel('../data/tips_data_bb.xlsx',sheet_name='info tips').set_index('security_name')
ts_tips = pd.read_excel('../data/tips_data_bb.xlsx',sheet_name='timeseries tips').set_index('date')
styler = (
    info_tips.T.style
     .format('{:%Y-%m-%d}', subset=pd.IndexSlice['issue_dt',  :])
     .format('{:%Y-%m-%d}', subset=pd.IndexSlice['maturity',  :])
     .format('{:.3f}',    subset=pd.IndexSlice['issue_px',  :])
     .format('{:.3f}',    subset=pd.IndexSlice['cpn',       :])
     .format('{:.3f}',    subset=pd.IndexSlice['base_cpi',  :])
     .format('{:.1e}',    subset=pd.IndexSlice['amt_issued',:])
)

styler
security_name TII 3 7/8 04/15/29
BB ID 912810FH@BGN Govt
issue_dt 1999-04-15
maturity 2029-04-15
cpn 3.875
amt_issued 2.0e+10
issue_px 103.628
base_cpi 164.393
(ts_tips*100).plot(title='Redemption Value of a specific TIPS issue');
../_images/7f5a7024ccdd6836a2a435c38412da97f61dfc949e336e5c0575623ff3f85989.png

QUOTE_DATE = '2024-04-30'

filepath_rawdata = f'../data/treasury_quotes_{QUOTE_DATE}.xlsx'
data = pd.read_excel(filepath_rawdata,sheet_name='quotes').set_index('KYTREASNO')
from cmds.bond_calcs import crsp_data_calculate_ytm
ytm_calcs = crsp_data_calculate_ytm(data)
data['ytm'] = ytm_calcs['ytm']
from cmds.plot_utils import scatter_by_type
from cmds.config import COLOR_MAP 

ax = scatter_by_type(
    data,
    x='ttm',
    y='ytm',
    color_map=COLOR_MAP,
    xlabel='Time to Maturity (ttm)',
    ylabel='Yield to Maturity (ytm)',
    title='YTM vs TTM by Type',
    alpha=0.8,
)
../_images/a0ebd69fe56517655b36d4dc12f57aa27633cf5b02c8026c0e57ab01edd7be36.png

DATE = '2025-04-30'
FILEIN_TS = f'../data/treasury_ts_crsp_{DATE}.xlsx'
df_ytm = pd.read_excel(FILEIN_TS,sheet_name='ytm').set_index('caldt')
df_ttm = pd.read_excel(FILEIN_TS,sheet_name='ttm').set_index('caldt')
df_info = pd.read_excel(FILEIN_TS,sheet_name='info').set_index('Unnamed: 0').T
ttm = df_ttm.iloc[:, 0]
df_plot = df_ytm.copy()

# grab the first index value (or, if you actually want a column, use info['itype'].iloc[0])
first = df_info['itype'][0]

# decide the new column order in one line
new_cols = (['nominal','TIPS'] if first in (1, 2)
            else ['TIPS','nominal'] if first in (11, 12)
            else None)

if new_cols is None:
    raise ValueError(f"Unexpected info index: {first!r}")

# apply the renaming
df_plot.columns = new_cols
/var/folders/zx/3v_qt0957xzg3nqtnkv007d00000gn/T/ipykernel_27098/2880877461.py:5: FutureWarning: Series.__getitem__ treating keys as positions is deprecated. In a future version, integer keys will always be treated as labels (consistent with DataFrame behavior). To access a value by position, use `ser.iloc[pos]`
  first = df_info['itype'][0]
from matplotlib.ticker import PercentFormatter
import numpy as np
import matplotlib.pyplot as plt

tol = 0.01
max_val = ttm.max()
multiples = np.arange(5, max_val + 5, 5)

fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True, figsize=(8, 9))

# ─── Top panel: your two curves ───
df_plot.plot(ax=ax1)
ymin1, ymax1 = ax1.get_ylim()
for m in multiples:
    mask = np.isclose(ttm.values, m, atol=tol)
    if not mask.any():
        continue
    loc = ttm.index[mask][0]
    ax1.vlines(loc, ymin1, ymax1, colors='black', linestyles='--', alpha=0.7)
    ax1.text(loc, ymin1, f"ttm={m}", rotation=0,
             va='top', ha='center', fontsize=12, alpha=0.8)

ax1.legend()
ax1.set_title("Yields")
ax1.yaxis.set_major_formatter(PercentFormatter(xmax=1, decimals=1))

# ─── Bottom panel: the breakeven ───
breakeven = df_plot.iloc[:, 0] - df_plot.iloc[:, 1]
breakeven.plot(ax=ax2, label='breakeven', color='tab:green')  # new color here
ymin2, ymax2 = ax2.get_ylim()
for m in multiples:
    mask = np.isclose(ttm.values, m, atol=tol)
    if not mask.any():
        continue
    loc = ttm.index[mask][0]
    ax2.vlines(loc, ymin2, ymax2, colors='black', linestyles='--', alpha=0.7)
    ax2.text(loc, ymin2, f"ttm={m}", rotation=0,
             va='top', ha='center', fontsize=12, alpha=0.8)

ax2.legend()
ax2.set_title("Market-Implied Breakeven Inflation")
ax2.yaxis.set_major_formatter(PercentFormatter(xmax=1, decimals=1))

plt.tight_layout()
plt.show()
../_images/8a5c5b487b6ff53b8cb4f0e559f82897ac007fe126ad24edf60c4f3768beeb65.png

Market Expectations#

SELECT = 3

ts = pd.read_excel('../data/tips_data_bb.xlsx',sheet_name='timeseries breakeven').set_index('date')

ticks = ts.columns.str.split().str[0]
labels_ticks = [f'{tick[-3:-1]}yr' for tick in ticks]
labels_ticks = ['Breakeven ' + tick for tick in labels_ticks]
ts.columns = labels_ticks

ts.iloc[:,0:SELECT].plot(xlim=('2008','2010'),ylim=(-8,5),title='TIPS Breakeven Inflation Rates')
plt.show()
../_images/f5313771afe1ac3191a32bc13403be7aa9c8edbabc0fc916b96fc2c3c17557e0.png
ts.iloc[:,0:SELECT].plot(xlim=('2018-01-01', '2025-05-31'),ylim=(-3,6.5),title='TIPS Breakeven Inflation Rates')
plt.show()
../_images/ce2dbf5699c3e34ce5c16ee35217cd6a1c35b086d588cadb272ec4a77515ad62.png

Expectations and Outcomes#

rawdata = pd.read_excel('../data/economic_data.xlsx',sheet_name='data').set_index('date')

FREQ = 4

if FREQ == 4:
    FREQcode = 'QE'
elif FREQ == 1:
    FREQcode = 'Y'
elif FREQ==12:
    FREQcode = 'M'

data = rawdata.resample(FREQcode).agg('last')
data.index = data.index - pd.tseries.offsets.BDay(1)

data_econ = data
inflation = (data['CPI-Core']/data['CPI-Core'].shift(1) -1 ) * FREQ
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.ticker import StrMethodFormatter

# --- prepare your data (as you already have) ---
inflation_expectations = pd.concat([
    inflation.rolling(FREQ*5).mean(),
    (data_econ['Breakeven 5yr']/100).shift(5*FREQ)
], axis=1)
inflation_expectations.rename(
    columns={'CPI-Core':'5yr avg CPI Inflation'},
    inplace=True
)

# --- make the wide, 2×1 figure ---
fig, (ax1, ax2) = plt.subplots(
    2, 1,
    figsize=(8, 8),     # wider than default
    sharex=False,        # each panel has its own x‐range
    gridspec_kw={'height_ratios': [1,1]}
)

# Top panel: realized vs lagged breakeven
inflation_expectations.plot(
    ax=ax1,
    xlim=('2007-01-01','2024-05-31'),
    ylim=(0, .04),
    xlabel='realized date',
    ylabel='inflation',
    title='Inflation vs (lagged) Breakeven Forecast'
)
ax1.yaxis.set_major_formatter(StrMethodFormatter('{x:.1%}'))

# Bottom panel: breakeven series
(data_econ[['Breakeven 5yr','Forward Breakeven 5yr']] / 100).plot(
    ax=ax2,
    xlim=('2017-01-01','2023-12-31'),
    xlabel='forecast date',
    ylabel='inflation',
    title='5-year Break-even'
)
ax2.yaxis.set_major_formatter(StrMethodFormatter('{x:.1%}'))

# tighten and show
plt.tight_layout()
plt.show()
../_images/10a3f17b7db4571b8a0ad465cd2da356e849aac6fde425b83ebeb3532c8ed437.png