Inflation calibration
This page lists what you need to solve an inflation curve from zero-coupon inflation swaps quoted at breakeven rates: the ladder, its lagged pillars, the base prints, and what the solve hands back.
Quick example
This solves a US CPI curve from three breakevens, files it in the market and prices each swap back to nothing. The example reads its rates from the desk sheets; load them once and it runs as shown.
use chrono::NaiveDate;
use helixio_docs_sheets::{SOFR_CLOSE_DATE, USD_SOFR_CLOSE};
use helixrs::{
AccrualCompounding, AnyInstrument, CollateralSpec, Currency, DayCountFraction, FixingKey,
FixingStore, FxRates, InflationCurve, LevelInterpolation, MarketContext, QuoteSheet, Rung,
Tolerance, ZeroCouponInflationSwap, calibrate_inflation, lagged_observation,
};
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")
}
fn main() -> Outcome<()> {
let close = date(SOFR_CLOSE_DATE);
let usd: Currency = "USD".parse()?;
let act365 = DayCountFraction::Act365Fixed;
// Two published prints of the index, and the discount curve from the sheet.
let prints = vec![
(FixingKey::Typed("US-CPI-U".to_owned()), date((2026, 6, 1)), 324.10),
(FixingKey::Typed("US-CPI-U".to_owned()), date((2026, 7, 1)), 324.90),
];
let fixings = FixingStore::from_observations_checked(&prints)?;
let sheet = QuoteSheet::from_rows(USD_SOFR_CLOSE.iter().copied())?;
let market = MarketContext::as_of(close)
.with_fixings(fixings.clone())
.with_recipe_quotes(&CollateralSpec::Currency(usd), &sheet)?;
let (discount, _) = market.curve(&"discount(USD)".parse()?).expect("the USD curve");
// Zero-coupon inflation swaps at breakeven rates, each pillar the maturity read
// three months back.
let mut swaps = Vec::new();
for (years, breakeven) in [(2, 0.0245), (5, 0.0238), (10, 0.0241)] {
let maturity = date((2026 + years, 10, 1));
let swap = ZeroCouponInflationSwap::new(usd, "US-CPI-U", close, maturity, 1.0, breakeven, 3)
.accruing(act365, AccrualCompounding::Compounded);
swaps.push(Rung::par(
lagged_observation(maturity, 3),
AnyInstrument::ZcInflationSwap(Box::new(swap)),
breakeven,
));
}
let fx = FxRates::default();
let inflation = calibrate_inflation(close, &discount, &swaps, &fixings, &fx, Tolerance::default(), LevelInterpolation::Linear, act365)?;
// Filed under its index, the curve prices every swap back to nothing.
let curve = inflation.curve.clone();
let priced = market.with_inflation_curve("US-CPI-U", inflation);
let level = |at: NaiveDate| curve.view::<f64>().index_level(at);
for rung in &swaps {
println!(
"{} breakeven {:.4} level {:.4} value {:.6}",
rung.pillar,
rung.quote,
level(rung.pillar)?,
rung.instrument.npv(&priced)?.abs()
);
}
Ok(())
}import datetime as dt
import helixio as hlx
from desk_sheets import SOFR_CLOSE_DATE, USD_SOFR_CLOSE
# Two published prints of the index, and the discount curve from the sheet.
prints = [("US-CPI-U", dt.date(2026, 6, 1), 324.10), ("US-CPI-U", dt.date(2026, 7, 1), 324.90)]
market = hlx.MarketContext(SOFR_CLOSE_DATE).with_fixings(prints).with_recipe_quotes("USD", USD_SOFR_CLOSE)
discount = market.curve("discount(USD)")
# Zero-coupon inflation swaps at breakeven rates, each pillar the maturity read
# three months back.
swaps = []
for years, breakeven in [(2, 0.0245), (5, 0.0238), (10, 0.0241)]:
maturity = dt.date(2026 + years, 10, 1)
swap = hlx.products.zero_coupon_inflation_swap(
"USD", "US-CPI-U", SOFR_CLOSE_DATE, maturity, notional=1.0, fixed_rate=breakeven
)
swaps.append((hlx.dates.lagged_observation(maturity, 3), swap, breakeven))
inflation = hlx.calibration.inflation(SOFR_CLOSE_DATE, discount, swaps, market=market)
# Filed under its index, the curve prices every swap back to nothing.
priced = market.with_inflation_curve("US-CPI-U", inflation.curve())
for pillar, swap, breakeven in swaps:
print(f"{pillar} breakeven {breakeven:.4f} level {inflation.curve().index_level(pillar):.4f} value {abs(swap.npv(priced)):.6f}")2028-07-01 breakeven 0.0245 level 341.0377 value 0.000000
2031-07-01 breakeven 0.0238 level 365.4713 value 0.000000
2036-07-01 breakeven 0.0241 level 412.3428 value 0.000000
Each pillar is the maturity read three months back, the index's lag, so a swap maturing in October 2028 pins the level for July 2028.
How it's priced
A zero-coupon inflation swap with base level , breakeven and year fraction is worth nothing when
so each quote fixes one index level at its lagged pillar, and the curve reads levels between pillars linearly (or log-linearly, if you ask). Why the index is read with a lag is in how calibration works.
API
| Symbol | Meaning | Rust | Python |
|---|---|---|---|
| rung | a zero-coupon inflation swap at its breakeven | ZeroCouponInflationSwap::new, Rung::par |
products.zero_coupon_inflation_swap |
| lagged pillar | the observation date of a maturity | lagged_observation |
dates.lagged_observation |
| the inflation solve | calibrate_inflation |
calibration.inflation |
|
| filed curve | the curve under its index | MarketContext::with_inflation_curve |
MarketContext.with_inflation_curve |
| level | the index level at a date | InflationCurve::index_level |
curves.InflationCurve.index_level |
| Argument | Type | Default | Meaning |
|---|---|---|---|
reference |
date |
required | the curve's reference date |
discount |
DiscountCurve or None |
required | the nominal curve; may be None when every rung is zero-coupon |
rungs |
list[Rung] or list[tuple[date, Instrument, float]] |
required | one swap per lagged pillar with its breakeven |
market |
MarketContext or None |
None |
where the base prints are read |
level_interpolation |
LevelInterpolation or str |
"linear" |
how levels are read between pillars: "linear" or "loglinear" |
day_count |
str or DayCountFraction |
"ACT/365.FIXED" |
the day count the curve measures time on |
tolerance |
Tolerance or None |
the defaults | how hard the solve tries; see bootstrapping |
The result, a CalibratedInflation, has curve(), rungs(), solves() and jacobian(), how far
each level moves per unit move in each breakeven.
Defaults and limits
- Base prints: the market must hold the index prints the swaps' base dates read; a missing print is refused with an error naming it.
- Discount curve: a zero-coupon swap's one payment cancels its discount factor in the fair rate, so any nominal curve solves the same levels. A year-on-year rung needs the real curve.
- Pillars: must rise strictly, each at a swap's lagged observation.
See also
- Source:
crates/helixrs/src/calibration.rs,crates/helixrs/src/curves/inflation.rs - Related pages:
- Inflation curves, for reading an inflation curve.
- Inflation coupons, the coupons the curve projects.
- Credit calibration, the same idea for survival.
From the 0.0 line at revision bf7f2e2. Every printed number is that revision's own output.