Price an inflation swap from quotes
You'll mark an inflation index curve from zero-coupon inflation swap quotes, file it in a EUR market, and price a swap on it, checking that a swap struck at a quoted breakeven is worth nothing.
Before you start
- helixio installed: see installing.
- The index's latest published prints, since the curve starts from them.
- Breakeven quotes for zero-coupon inflation swaps on the index, by tenor.
- A discount curve in the swaps' currency. This guide marks one from six €STR swap rates.
Steps
- Mark a EUR market from the €STR quotes with
with_recipe_quotes, and give it the index's prints withwith_fixings. - Book one zero-coupon inflation swap per quote with
products.zero_coupon_inflation_swap, struck at its breakeven. Pair each with its pillar: the maturity read three months back withdates.lagged_observation, because that is the date the swap's final index level is observed. - Solve the index curve with
calibration.inflationon the €STR discount curve, and file it under the index withwith_inflation_curve. - Price your trade on the market, and read its par rate.
The comments in the program match these steps.
Rust
use chrono::NaiveDate;
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>>;
const INDEX: &str = "EUR-HICPXT";
fn date((year, month, day): (i32, u32, u32)) -> NaiveDate {
NaiveDate::from_ymd_opt(year, month, day).expect("a real calendar date")
}
fn zero_coupon(eur: Currency, close: NaiveDate, maturity: NaiveDate, notional: f64, rate: f64) -> AnyInstrument {
let swap = ZeroCouponInflationSwap::new(eur, INDEX, close, maturity, notional, rate, 3)
.accruing(DayCountFraction::Act365Fixed, AccrualCompounding::Compounded);
AnyInstrument::ZcInflationSwap(Box::new(swap))
}
fn main() -> Outcome<()> {
let close = date((2026, 10, 1));
let eur: Currency = "EUR".parse()?;
// Step 1: a EUR market from €STR swap quotes, holding the index's last two prints.
let estr = [
("EUR-ESTR-OIS", "1Y", 0.0195),
("EUR-ESTR-OIS", "2Y", 0.0200),
("EUR-ESTR-OIS", "5Y", 0.0215),
("EUR-ESTR-OIS", "10Y", 0.0240),
("EUR-ESTR-OIS", "20Y", 0.0262),
("EUR-ESTR-OIS", "30Y", 0.0265),
];
let prints = vec![
(FixingKey::Typed(INDEX.to_owned()), date((2026, 6, 1)), 128.40),
(FixingKey::Typed(INDEX.to_owned()), date((2026, 7, 1)), 128.65),
];
let fixings = FixingStore::from_observations_checked(&prints)?;
let sheet = QuoteSheet::from_rows(estr.iter().copied())?;
let market = MarketContext::as_of(close)
.with_fixings(fixings.clone())
.with_recipe_quotes(&CollateralSpec::Currency(eur), &sheet)?;
// Step 2: one zero-coupon inflation swap per breakeven quote, each paired with its
// pillar: the maturity read three months back, as the index is observed.
let quotes = [(2, 0.0212), (5, 0.0205), (10, 0.0209)];
let mut rungs = Vec::new();
for (years, breakeven) in quotes {
let maturity = date((2026 + years, 10, 1));
rungs.push(Rung::par(
lagged_observation(maturity, 3),
zero_coupon(eur, close, maturity, 1.0, breakeven),
breakeven,
));
}
// Step 3: solve the index curve on the €STR discount curve, and file it under the index.
let (discount, _) = market.curve(&"discount(EUR)".parse()?).expect("the EUR curve");
let solved = calibrate_inflation(
close,
&discount,
&rungs,
&fixings,
&FxRates::default(),
Tolerance::default(),
LevelInterpolation::Linear,
DayCountFraction::Act365Fixed,
)?;
let curve = solved.curve.clone();
let market = market.with_inflation_curve(INDEX, solved);
for rung in &rungs {
let level = curve.view::<f64>().index_level(rung.pillar)?;
println!("{} breakeven {:.4} level {level:.4}", rung.pillar, rung.quote);
}
// Step 4: price your trade, five years on 10 million at the 5Y breakeven.
let trade = zero_coupon(eur, close, date((2031, 10, 1)), 10_000_000.0, 0.0205);
let npv = trade.npv(&market)?;
let par = trade.par_rate(&market)?;
println!("npv {:.2}", npv.abs());
println!("par rate {par:.6}");
Ok(())
}Python
import datetime as dt
import helixio as hlx
CLOSE = dt.date(2026, 10, 1)
INDEX = "EUR-HICPXT"
# Step 1: a EUR market from €STR swap quotes, holding the index's last two prints.
estr = {"EUR-ESTR-OIS": {"1Y": 0.0195, "2Y": 0.0200, "5Y": 0.0215, "10Y": 0.0240, "20Y": 0.0262, "30Y": 0.0265}}
prints = [(INDEX, dt.date(2026, 6, 1), 128.40), (INDEX, dt.date(2026, 7, 1), 128.65)]
market = hlx.MarketContext(CLOSE).with_fixings(prints).with_recipe_quotes("EUR", estr)
# Step 2: one zero-coupon inflation swap per breakeven quote, each paired with its
# pillar: the maturity read three months back, as the index is observed.
quotes = [(2, 0.0212), (5, 0.0205), (10, 0.0209)]
rungs = []
for years, breakeven in quotes:
maturity = dt.date(2026 + years, 10, 1)
swap = hlx.products.zero_coupon_inflation_swap("EUR", INDEX, CLOSE, maturity, notional=1.0, fixed_rate=breakeven)
rungs.append((hlx.dates.lagged_observation(maturity, 3), swap, breakeven))
# Step 3: solve the index curve on the €STR discount curve, and file it under the index.
solved = hlx.calibration.inflation(CLOSE, market.curve("discount(EUR)"), rungs, market=market)
market = market.with_inflation_curve(INDEX, solved.curve())
for pillar, _, breakeven in rungs:
print(f"{pillar} breakeven {breakeven:.4f} level {solved.curve().index_level(pillar):.4f}")
# Step 4: price your trade, five years on 10 million at the 5Y breakeven.
trade = hlx.products.zero_coupon_inflation_swap(
"EUR", INDEX, CLOSE, dt.date(2031, 10, 1), notional=10_000_000.0, fixed_rate=0.0205
)
npv = trade.npv(market)
par = trade.par_rate(market)
print(f"npv {abs(npv):.2f}")
print(f"par rate {par:.6f}")2028-07-01 breakeven 0.0212 level 134.1703
2031-07-01 breakeven 0.0205 level 142.3964
2036-07-01 breakeven 0.0209 level 158.2398
npv 0.00
par rate 0.020500
Check it worked
- Each pillar date is three months before its swap's maturity, and the index levels rise from the last print at the breakeven rates.
- A trade struck at a quoted breakeven on a quoted tenor is worth nothing, and its par rate is that breakeven.
- If the solve refuses, check that the market holds a print for the base the swaps read: the index's level three months before the trade date.
Related
- Inflation calibration, the reference for the solve.
- Inflation swaps, the reference for the contract.
- Inflation curves, for reading levels off the curve.
From the 0.0 line at revision bf7f2e2. Every printed number is that revision's own output.