Inflation swaps
This page lists what you need to value zero-coupon and year-on-year inflation swaps: the booking calls and their arguments, the payoffs, and the defaults you get. An inflation swap exchanges a fixed rate for the growth of a price index.
Quick example
The example reads its quotes from the desk sheets; load them once and it runs as shown.
This values a five-year zero-coupon swap on US CPI paying 2.00% fixed, reads its breakeven and the five-year year-on-year par rate, and sets a one-year year-on-year swap beside a one-year zero-coupon swap.
use chrono::NaiveDate;
use helixio_docs_sheets::{SOFR_CLOSE_DATE, USD_SOFR_CLOSE};
use helixrs::{
AccrualCompounding, AnyInstrument, CollateralSpec, Inflation, MarketContext,
PiecewiseInflation, QuoteSheet, YoyInflationSwap, ZeroCouponInflationSwap,
};
type Outcome<T> = Result<T, Box<dyn std::error::Error>>;
fn date((year, month, day): (i32, u32, u32)) -> NaiveDate {
NaiveDate::from_ymd_opt(year, month, day).expect("a real calendar date")
}
const LEVELS: [((i32, u32, u32), f64); 6] = [
((2026, 7, 1), 321.0),
((2027, 7, 1), 329.0),
((2028, 7, 1), 337.2),
((2029, 7, 1), 345.5),
((2031, 7, 1), 362.6),
((2036, 7, 1), 407.0),
];
fn cpi_curve(close: NaiveDate) -> Outcome<Inflation> {
let nodes: Vec<(NaiveDate, f64)> = LEVELS.iter().map(|(d, level)| (date(*d), *level)).collect();
Ok(Inflation::Piecewise(PiecewiseInflation::new(
close,
&nodes,
"linear".parse()?,
"ACT/365.FIXED".parse()?,
)?))
}
fn zero_coupon(
close: NaiveDate,
rate: f64,
maturity: NaiveDate,
receiver: bool,
) -> Outcome<AnyInstrument> {
let mut swap = ZeroCouponInflationSwap::new(
"USD".parse()?,
"US-CPI-U",
close,
maturity,
10_000_000.0,
rate,
3,
)
.accruing("ACT/365.FIXED".parse()?, AccrualCompounding::Compounded);
if receiver {
swap = swap.receiver();
}
Ok(AnyInstrument::ZcInflationSwap(Box::new(swap)))
}
fn year_on_year(close: NaiveDate, rate: f64, maturity: NaiveDate) -> Outcome<AnyInstrument> {
let swap = YoyInflationSwap::new(
"USD".parse()?,
"US-CPI-U",
close,
maturity,
10_000_000.0,
rate,
3,
)
.accruing("ACT/365.FIXED".parse()?);
Ok(AnyInstrument::YoyInflationSwap(Box::new(swap)))
}
fn main() -> Outcome<()> {
let close = date(SOFR_CLOSE_DATE);
let market = MarketContext::as_of(close)
.with_recipe_quotes(
&CollateralSpec::Currency("USD".parse()?),
&QuoteSheet::from_rows(USD_SOFR_CLOSE.iter().copied())?,
)?
.with_inflation_curve("US-CPI-U", cpi_curve(close)?);
let (five_years, one_year) = (date((2031, 10, 1)), date((2027, 10, 1)));
// Pay a fixed rate compounded over five years, receive CPI growth.
let breakeven = zero_coupon(close, 0.0, five_years, false)?.par_rate(&market)?;
let payer = zero_coupon(close, 0.0200, five_years, false)?.npv(&market)?;
println!("5Y breakeven {breakeven:.8}");
println!("pay 2.00% fixed {payer:.2}");
// The year-on-year swap pays each year's growth against a fixed coupon.
let yoy = year_on_year(close, 0.0, five_years)?.par_rate(&market)?;
println!("5Y year-on-year par {yoy:.8}");
// Over one year the two shapes are one payment of the same growth.
let one_yoy = year_on_year(close, 0.0250, one_year)?.npv(&market)?;
let one_zc = zero_coupon(close, 0.0250, one_year, false)?.npv(&market)?;
println!("1Y yoy npv {one_yoy:.2}");
println!("1Y zero-coupon npv {one_zc:.2}");
Ok(())
}import datetime as dt
import helixio as hlx
from desk_sheets import SOFR_CLOSE_DATE, USD_SOFR_CLOSE
LEVELS = [
(dt.date(2026, 7, 1), 321.0),
(dt.date(2027, 7, 1), 329.0),
(dt.date(2028, 7, 1), 337.2),
(dt.date(2029, 7, 1), 345.5),
(dt.date(2031, 7, 1), 362.6),
(dt.date(2036, 7, 1), 407.0),
]
market = (
hlx.MarketContext(SOFR_CLOSE_DATE)
.with_recipe_quotes("USD", USD_SOFR_CLOSE)
.with_inflation_curve("US-CPI-U", hlx.curves.InflationCurve(SOFR_CLOSE_DATE, LEVELS))
)
FIVE_YEARS, ONE_YEAR = dt.date(2031, 10, 1), dt.date(2027, 10, 1)
def zero_coupon(rate, maturity=FIVE_YEARS, receiver=False):
return hlx.products.zero_coupon_inflation_swap(
"USD", "US-CPI-U", SOFR_CLOSE_DATE, maturity, notional=10_000_000.0, fixed_rate=rate,
receiver=receiver,
)
def year_on_year(rate, maturity=FIVE_YEARS):
return hlx.products.yoy_inflation_swap(
"USD", "US-CPI-U", SOFR_CLOSE_DATE, maturity, notional=10_000_000.0, fixed_rate=rate
)
# Pay a fixed rate compounded over five years, receive CPI growth.
breakeven = zero_coupon(0.0).par_rate(market)
payer = zero_coupon(0.0200).npv(market)
print(f"5Y breakeven {breakeven:.8f}")
print(f"pay 2.00% fixed {payer:.2f}")
# The year-on-year swap pays each year's growth against a fixed coupon.
yoy = year_on_year(0.0).par_rate(market)
print(f"5Y year-on-year par {yoy:.8f}")
# Over one year the two shapes are one payment of the same growth.
one_yoy = year_on_year(0.0250, ONE_YEAR).npv(market)
one_zc = zero_coupon(0.0250, ONE_YEAR).npv(market)
print(f"1Y yoy npv {one_yoy:.2f}")
print(f"1Y zero-coupon npv {one_zc:.2f}")5Y breakeven 0.02465758
pay 2.00% fixed 201511.31
5Y year-on-year par 0.02468764
1Y yoy npv -745.67
1Y zero-coupon npv -745.67
Over a single year the two shapes pay the same growth once, so they agree.
How it's priced
With the index level, the zero-coupon swap pays at maturity , observed with lag ,
to the side receiving inflation. The breakeven makes it worth nothing. A year-on-year swap pays each year. Why the zero-coupon swap is the benchmark, and why the index is observed with a lag, is covered in how rates products work.
API
| Symbol | Meaning | Rust | Python |
|---|---|---|---|
| the index level curve | PiecewiseInflation::new, MarketContext::with_inflation_curve |
curves.InflationCurve, MarketContext.with_inflation_curve |
|
| zero-coupon | the swap | ZeroCouponInflationSwap::new |
products.zero_coupon_inflation_swap |
| year-on-year | the swap | YoyInflationSwap::new |
products.yoy_inflation_swap |
| the breakeven | AnyInstrument::par_rate |
Instrument.par_rate |
products.zero_coupon_inflation_swap and products.yoy_inflation_swap take:
| Argument | Type | Default | Meaning |
|---|---|---|---|
currency |
str |
required | the currency both legs pay in |
index |
str |
required | the price index, such as "US-CPI-U" |
base_date |
date |
required | the start; the index is observed lag_months before it |
maturity |
date |
required | the end; the last observation is lagged the same way |
notional |
float |
required | the notional |
fixed_rate |
float |
required | , the fixed rate |
lag_months |
int |
3 |
the observation lag , in months |
receives |
str or None |
"inflation" |
the leg you receive: "inflation" or "fixed" |
receiver |
bool or None |
None |
the older spelling of receives="fixed"; don't give both |
day_count |
str or DayCountFraction |
"ACT/365.FIXED" |
the fixed side's accrual |
accrual_compounding |
str |
"Compounded" |
zero-coupon only: how accrues over the life |
base_index |
float or None |
None |
zero-coupon only: an agreed base index level in place of the observed one |
index_interpolation |
str or None |
None |
zero-coupon only: "Month" or "Interpolated", where it differs from the index's own rule |
In Rust, ZeroCouponInflationSwap::new and YoyInflationSwap::new take currency, index, base_date, maturity, notional, fixed_rate, lag_months positionally.
Defaults and limits
- The curve: the index level curve is added to the market with its own verb,
with_inflation_curve; see inflation curves. - Coupons: each swap is made of inflation coupons, built and read on inflation coupons.
See also
- Source:
crates/helixrs/src/instruments/rates.rs - Related pages:
- Inflation curves, for the index levels.
- Inflation caps and floors, for options on the growth.
- Inflation-linked bonds, for the cash instrument.
From the 0.0 line at revision bf7f2e2. Every printed number is that revision's own output.