Esc

    The API is under testing and may change before release.

    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.

    Rust
    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(())
    }
    Python
    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 II observed ℓ\ell months before a date, a year-on-year coupon over [ts,te][t_s, t_e] with gearing gg and margin mm pays

    N τ(g(I(te−ℓ)I(ts−ℓ)−1)+m).N\,\tau \left( g \left( \frac{I(t_e - \ell)}{I(t_s - \ell)} - 1 \right) + m \right).

    A CPI coupon at real rate KK on base level I0I_0 pays N K τ I(te−ℓ)/I0N\, K\, \tau\, I(t_e - \ell) / I_0, and a CPI redemption paying only growth pays N (I(T−ℓ)/I0−1)N\,\bigl(I(T - \ell)/I_0 - 1\bigr). Why a contract observes the index months back is in how cashflows work.

    API

    Symbol Meaning Rust Python
    I(⋅)I(\cdot) the index-level curve PiecewiseInflation::new, held as Inflation curves.InflationCurve
    pinned II the curve under the index's code MarketContext::with_inflation_curve MarketContext.with_inflation_curve
    ℓ\ell 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: gg and mm above
    fixed_rate float required (CPI) the real rate KK
    base_index float required (CPI) the base level I0I_0
    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_months overrides 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_curve under 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.