Inflation coupons
This page lists the coupons that pay on a price index rather than an interest rate: the year-on-year coupon and its strip, the CPI coupon and the CPI redemption. It covers their arguments, the observation lag, and the index-level curve they read.
Quick example
This joins a euro-area HICP ex-tobacco curve to a EUR market, then values a year-on-year coupon, a 1% CPI coupon and a CPI redemption over October 2027 to October 2028. The example states its own quotes and its index levels, chosen so the index grows exactly 2% a year at the dates the coupons observe; it takes only the close date from the desk sheets.
use chrono::{Datelike, NaiveDate};
use helixio_docs_sheets::SOFR_CLOSE_DATE;
use helixrs::{
AnyCoupon, AnyIndex, CollateralSpec, CpiCoupon, CpiRedemption, DayCountFraction, Direction,
Inflation, InflationCurve, Leg, MarketContext, PiecewiseInflation, QuoteSheet, YieldCurve,
YoyInflationCoupon, index, leg_cashflow_records,
};
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 EUR_CLOSE: [(&str, &str, f64); 4] = [
("EUR-ESTR-OIS", "1Y", 0.0195),
("EUR-ESTR-OIS", "2Y", 0.0200),
("EUR-ESTR-OIS", "5Y", 0.0215),
("EUR-ESTR-OIS", "10Y", 0.0240),
];
fn main() -> Outcome<()> {
let close = date(SOFR_CLOSE_DATE);
let eur = CollateralSpec::Currency("EUR".parse()?);
let base = 128.0;
let (start, end) = (date((2027, 10, 1)), date((2028, 10, 1)));
let thirty: DayCountFraction = "30/360".parse()?;
let Some(AnyIndex::Price(hicp_index)) = index("EUR-HICPXT") else {
panic!("a registered price index");
};
// Curves from the sheet; the euro-area HICP ex-tobacco level curve, which no
// recipe builds, joined with the market's verb. Its nodes grow 2% a year on
// each 1 July: the dates a coupon accruing from 1 October observes, three
// months back.
let mut nodes = vec![(close, 127.0)];
for k in 0..5 {
nodes.push((date((2027 + k, 7, 1)), base * 1.02_f64.powi(k)));
}
let hicp = Inflation::Piecewise(PiecewiseInflation::new(
close,
&nodes,
"linear".parse()?,
"ACT/365.FIXED".parse()?,
)?);
let sheet = QuoteSheet::from_rows(EUR_CLOSE)?;
let market = MarketContext::as_of(close)
.with_recipe_quotes(&eur, &sheet)?
.with_inflation_curve("EUR-HICPXT", hicp.clone());
println!("lag {} months", hicp_index.observation_lag_months);
let row = |coupon: AnyCoupon| -> Outcome<(f64, f64, f64)> {
let leg = Leg::new(vec![coupon], Direction::Receive);
let projections = leg.projections();
let table = market.priced(&leg.risk_factors(&eur, &projections), |view| {
Ok(leg_cashflow_records(&leg, &view.disc, view, &view.fixings, close)?)
})?;
let npv = leg.npv_on(&market, &eur)?;
Ok((
table["rate"].numbers().expect("numbers")[0],
table["amount"].numbers().expect("numbers")[0],
npv,
))
};
let (yoy_rate, yoy_amount, yoy_npv) = row(AnyCoupon::YoyInflation(YoyInflationCoupon::new(
start,
end,
end,
10_000_000.0,
"EUR".parse()?,
hicp_index.code(),
hicp_index.observation_lag_months,
thirty,
)))?;
println!("rate {yoy_rate:.8}");
println!("amount {yoy_amount:.2}");
println!("npv {yoy_npv:.2}");
// A 1% real coupon on a notional indexed from a base level of 128.
let level = hicp.view::<f64>().index_level(date((2028, 7, 1)))?;
let (_, cpi_amount, _) = row(AnyCoupon::Cpi(CpiCoupon {
accrual_start: start,
accrual_end: end,
payment_date: end,
notional: 10_000_000.0,
fixed_rate: 0.01,
day_count: thirty,
accrual: thirty.year_fraction(start, end),
payment_ordinal: end.num_days_from_ce(),
currency: "EUR".parse()?,
index: hicp_index.code(),
lag_months: hicp_index.observation_lag_months,
base_index: base,
}))?;
let (_, growth, _) = row(AnyCoupon::CpiRedemption(CpiRedemption {
payment_date: end,
payment_ordinal: end.num_days_from_ce(),
notional: 10_000_000.0,
currency: "EUR".parse()?,
index: hicp_index.code(),
lag_months: hicp_index.observation_lag_months,
base_index: base,
observation: end,
growth_only: true,
base_date: None,
payoff: None,
}))?;
println!("index level {level:.4}");
println!("cpi coupon {cpi_amount:.2}");
println!("redemption {growth:.2}");
Ok(())
}import datetime as dt
import helixio as hlx
from desk_sheets import SOFR_CLOSE_DATE
EUR_CLOSE = {"EUR-ESTR-OIS": {"1Y": 0.0195, "2Y": 0.0200, "5Y": 0.0215, "10Y": 0.0240}}
BASE = 128.0
START, END = dt.date(2027, 10, 1), dt.date(2028, 10, 1)
# Curves from the sheet; the euro-area HICP ex-tobacco level curve, which no
# recipe builds, joined with the market's verb. Its nodes grow 2% a year on
# each 1 July: the dates a coupon accruing from 1 October observes, three
# months back.
nodes = [(SOFR_CLOSE_DATE, 127.0)] + [(dt.date(2027 + k, 7, 1), BASE * 1.02**k) for k in range(5)]
hicp = hlx.curves.InflationCurve(SOFR_CLOSE_DATE, nodes)
market = (
hlx.MarketContext(SOFR_CLOSE_DATE)
.with_recipe_quotes("EUR", EUR_CLOSE)
.with_inflation_curve("EUR-HICPXT", hicp)
)
print(f"lag {hlx.Index('EUR-HICPXT').observation_lag_months()} months")
def row(coupon):
leg = hlx.Leg([coupon], "Receive", "EUR")
table = leg.cashflow_records(market).to_dict()
return table["rate"][0], table["amount"][0], leg.npv(market)
yoy_rate, yoy_amount, yoy_npv = row(
hlx.legs.yoy_inflation_coupon(
"EUR", START, END, END, notional=10_000_000.0, day_count="30/360", index="EUR-HICPXT"
)
)
print(f"rate {yoy_rate:.8f}")
print(f"amount {yoy_amount:.2f}")
print(f"npv {yoy_npv:.2f}")
# A 1% real coupon on a notional indexed from a base level of 128.
level = hicp.index_level(dt.date(2028, 7, 1))
_, cpi_amount, _ = row(
hlx.legs.cpi_coupon(
"EUR", START, END, END, notional=10_000_000.0, fixed_rate=0.01, day_count="30/360",
index="EUR-HICPXT", base_index=BASE,
)
)
_, growth, _ = row(
hlx.legs.cpi_redemption("EUR", END, notional=10_000_000.0, index="EUR-HICPXT", base_index=BASE)
)
print(f"index level {level:.4f}")
print(f"cpi coupon {cpi_amount:.2f}")
print(f"redemption {growth:.2f}")lag 3 months
rate 0.02000000
amount 200000.00
npv 192118.81
index level 130.5600
cpi coupon 102000.00
redemption 200000.00
The year-on-year coupon pays the 2% growth, and the CPI coupon and redemption pay their formulas on the index level the curve gives three months before the payment date.
How it's priced
With the index observed months before a date, a year-on-year coupon over with gearing and margin pays
A CPI coupon at real rate on base level pays , and a CPI redemption paying only growth pays . Why a contract observes the index months back is in how cashflows work.
API
| Symbol | Meaning | Rust | Python |
|---|---|---|---|
| the index-level curve | PiecewiseInflation::new, held as Inflation |
curves.InflationCurve |
|
| pinned | the curve under the index's code | MarketContext::with_inflation_curve |
MarketContext.with_inflation_curve |
| the observation lag | PriceIndex::observation_lag_months |
Index.observation_lag_months |
|
| YoY coupon | year-on-year growth | YoyInflationCoupon::new |
legs.yoy_inflation_coupon |
| YoY strip | one per period | yoy_inflation_leg |
legs.yoy_inflation_leg |
| CPI coupon | a real coupon, indexed | CpiCoupon |
legs.cpi_coupon, strip legs.cpi_leg |
| CPI redemption | indexed growth of the notional | CpiRedemption |
legs.cpi_redemption |
legs.yoy_inflation_coupon(...) and legs.cpi_coupon(...):
| Argument | Type | Default | Meaning |
|---|---|---|---|
currency |
str |
required | the currency the coupon pays in |
accrual_start, accrual_end, payment_date |
date |
required | the coupon's dates |
notional |
float |
required | the size |
day_count |
str |
required | the accrual day count |
index |
str |
required | the price index, such as "EUR-HICPXT" |
gearing, spread |
float |
1.0, 0.0 |
YoY only: and above |
fixed_rate |
float |
required (CPI) | the real rate |
base_index |
float |
required (CPI) | the base level |
lag_months |
int or None |
None |
the observation lag; None takes the index's own |
legs.cpi_redemption(...):
| Argument | Type | Default | Meaning |
|---|---|---|---|
currency, payment_date |
str, date |
required | where and when it pays |
notional, index, base_index |
required | as on the CPI coupon | |
observation |
date or None |
None |
the date the index is observed from; None is the payment date |
lag_months |
int or None |
None |
as above |
growth_only |
bool |
True |
True pays the growth only; False pays the whole indexed notional |
base_date |
date or None |
None |
the date the base level stands for; a collar on the notional is struck in annualised rates from it |
legs.yoy_inflation_leg(currency, schedule, payment_dates, ...) and
legs.cpi_leg(schedule, payment_dates, currency, ...) take the same terms as their coupon, over a
schedule.
Defaults and limits
- Lag: most indices are observed three months back; the index carries its own lag, and
lag_monthsoverrides it. - The curve: no recipe builds an index-level curve from a quote sheet. Build one directly, as on
inflation curves, or solve one from zero-coupon inflation
swaps, as on inflation calibration. Then join it with
with_inflation_curveunder the index's code. - Options: caps and floors on year-on-year coupons are on inflation caps and floors.
See also
- Source:
crates/helixrs/src/cashflows/coupons.rs,crates/helixrs/src/curves/inflation.rs - Related pages:
- Inflation swaps, the traded products.
- Inflation curves, the curve these coupons read.
From the 0.0 line at revision bf7f2e2. Every printed number is that revision's own output.