Esc

    The API is under testing and may change before release.

    Inflation-linked bond

    This page lists what you need to price an inflation-linked bond: the constructor and its options, the index ratio, the value off an inflation curve, the real price and yield, the settlement invoice, and examples you can run. A linker pays a fixed real coupon on a principal that grows with a consumer price index.

    Quick example

    The example reads its swap quotes from the desk sheets; load them once and it runs as shown.

    This takes a ten-year TIPS-style linker paying a 1.875% real coupon semiannually, indexed to US CPI-U with a three-month lag from a base of 320, on 2 March 2026. The December and January prints and a rising CPI curve are stated in the file. It reads the index ratio today and in July, values the linker, moves between its real price and real yield, and works out the settlement invoice for a real quote.

    Rust
    use chrono::{Datelike, NaiveDate};
    use helixio_docs_sheets::USD_CLOSE_MARCH;
    use helixrs::LinkerConvention;
    use helixrs::{
        AnyInstrument, CollateralSpec, Currency, DayCountFraction, FixedRateBond, FixingKey,
        FixingStore, Frequency, Inflation, InflationCurve, InflationLinkedBond, LevelInterpolation,
        MarketContext, PiecewiseInflation, PriceBasis, PricingRiskFactors, QuoteSheet, YieldBasis,
        YieldCurve, dirty_price, invoice, real_price, street_index_ratio,
    };
    
    fn date(year: i32, month: u32, day: u32) -> NaiveDate {
        NaiveDate::from_ymd_opt(year, month, day).expect("a real calendar date")
    }
    
    const INDEX: &str = "US-CPI-U";
    const BASE: f64 = 320.0;
    const REAL_COUPON: f64 = 0.01875;
    const DECEMBER: f64 = 324.0;
    const JANUARY: f64 = 325.2;
    
    fn inflation_on(reference: NaiveDate, nodes: &[(NaiveDate, f64)]) -> Inflation {
        Inflation::Piecewise(
            PiecewiseInflation::new(
                reference,
                nodes,
                LevelInterpolation::Linear,
                DayCountFraction::Act365Fixed,
            )
            .expect("well-formed nodes"),
        )
    }
    
    fn linker(floored: bool) -> InflationLinkedBond {
        InflationLinkedBond::new(
            Currency::new("USD").expect("a registered currency"),
            INDEX,
            BASE,
            REAL_COUPON,
            3,
            Frequency::SemiAnnual,
            DayCountFraction::ActActIcma,
            100.0,
            date(2025, 7, 15),
            date(2035, 7, 15),
        )
        .expect("a well-formed linker")
        .with_redemption_floor(floored)
    }
    
    fn market_on(nominal_market: &MarketContext, cpi: Inflation) -> MarketContext {
        let fixings = FixingStore::from_observations_checked(&[
            (FixingKey::from(INDEX), date(2025, 12, 1), DECEMBER),
            (FixingKey::from(INDEX), date(2026, 1, 1), JANUARY),
        ])
        .expect("a registered index");
        nominal_market
            .clone()
            .with_inflation_curve(INDEX, cpi)
            .with_fixings(fixings)
    }
    
    fn npv(bond: &InflationLinkedBond, market: &MarketContext) -> f64 {
        let lifted = market.lift::<f64>();
        let view = market.view(&lifted).expect("a market that binds");
        view.npv(&AnyInstrument::InflationLinkedBond(Box::new(bond.clone())))
            .expect("a priced linker")
    }
    
    /// The pricing index ratio at `at`, read through the market the bond resolves.
    fn index_ratio(bond: &InflationLinkedBond, market: &MarketContext, at: NaiveDate) -> f64 {
        let lifted = market.lift::<f64>();
        let view = market.view(&lifted).expect("a market that binds");
        let resolved = view
            .resolve(&bond.risk_factors().expect("a linker declares its curves"))
            .expect("the market carries its curves");
        bond.index_ratio(&resolved, at)
            .expect("an observable index")
    }
    
    fn main() -> Result<(), Box<dyn std::error::Error>> {
        let valuation = date(2026, 3, 2);
        // The nominal market, marked from the morning's SOFR swaps. The CPI curve
        // joins it with the market's own verb.
        let usd = Currency::new("USD").expect("a registered currency");
        let sheet = QuoteSheet::from_rows(USD_CLOSE_MARCH.iter().copied()).expect("a sheet");
        let nominal_market = MarketContext::as_of(valuation)
            .with_recipe_quotes(&CollateralSpec::Currency(usd), &sheet)
            .expect("a marked market");
        let rising = inflation_on(
            valuation,
            &[
                (date(2026, 4, 1), 326.0),
                (date(2031, 4, 1), 360.0),
                (date(2036, 4, 1), 398.0),
            ],
        );
        let market = market_on(&nominal_market, rising.clone());
        let bond = linker(true);
        let tips: LinkerConvention = "tips".parse().expect("a linker street");
    
        // The index ratio on a day in March reads December and January, three
        // months back, interpolated by the day of the month. A flow in July reads
        // April and May, which aren't published yet, so the inflation curve
        // supplies them.
        let ratio = index_ratio(&bond, &market, valuation);
        let july = date(2026, 7, 15);
        let july_ratio = index_ratio(&bond, &market, july);
        println!("index ratio   {valuation}  {ratio:.6}");
        println!("index ratio   {july}  {july_ratio:.6}");
    
        // Each real coupon and the real redemption is scaled by the ratio at its
        // date and discounted on the nominal curve.
        let value = npv(&bond, &market);
        println!("value         {value:.6}");
    
        // A TIPS quote is a real price at a real yield on the uninflated flows,
        // compounded semiannually. The index never enters it.
        let real_yield = 0.0180;
        let real_clean = real_price(&bond, tips, real_yield, valuation, PriceBasis::Clean)?;
        let real_dirty = real_price(&bond, tips, real_yield, valuation, PriceBasis::Dirty)?;
        let yield_back = helixrs::real_yield(&bond, tips, real_clean, valuation)?;
        println!("real yield    {:.4}%", real_yield * 100.0);
        println!("real clean    {real_clean:.6}");
        println!("real dirty    {real_dirty:.6}");
        println!("yield back    {:.10}%", yield_back * 100.0);
    
        // Settlement inflates the real quote by the street index ratio: TIPS
        // rounds the reference index and the ratio to five decimals.
        let lifted = market.lift::<f64>();
        let view = market.view(&lifted)?;
        let resolved = view.resolve(&bond.risk_factors()?)?;
        let curve = resolved.inflation.as_ref().expect("an inflation curve");
        let invoiced = invoice(&bond, tips, real_clean, valuation, &resolved.fixings, curve)?;
        let dirty = invoiced.dirty;
        println!("street ratio  {:.5}", invoiced.index_ratio);
        println!("clean         {:.6}", invoiced.clean);
        println!("accrued       {:.6}", invoiced.accrued);
        println!("dirty         {dirty:.6}");
    
        Ok(())
    }
    Python
    import datetime as dt
    
    import helixio as hlx
    from desk_sheets import USD_CLOSE_MARCH
    
    VALUATION = dt.date(2026, 3, 2)
    INDEX = "US-CPI-U"
    BASE = 320.0
    REAL_COUPON = 0.01875
    ISSUE = dt.date(2025, 7, 15)
    MATURITY = dt.date(2035, 7, 15)
    DECEMBER, JANUARY = 324.0, 325.2
    
    # The nominal market, marked from the morning's SOFR swaps. The CPI curve joins
    # it with the market's own verb, below.
    NOMINAL = hlx.MarketContext(VALUATION).with_recipe_quotes("USD", USD_CLOSE_MARCH)
    
    def linker(floored=True, outstanding=None):
        return hlx.products.inflation_linked_bond(
            "USD", INDEX, base_index=BASE, real_coupon=REAL_COUPON, lag_months=3, frequency=hlx.dates.Frequency.SemiAnnual,
            day_count=hlx.dates.DayCountFraction.ActActIcma, face=100.0, issue=ISSUE, maturity=MATURITY,
            redemption_floor=floored, outstanding=outstanding,
        )
    
    def market_on(cpi):
        return (
            NOMINAL
            .with_inflation_curve(INDEX, cpi)
            .with_fixings([(INDEX, dt.date(2025, 12, 1), DECEMBER), (INDEX, dt.date(2026, 1, 1), JANUARY)])
        )
    
    RISING = hlx.curves.InflationCurve(
        VALUATION, [(dt.date(2026, 4, 1), 326.0), (dt.date(2031, 4, 1), 360.0), (dt.date(2036, 4, 1), 398.0)]
    )
    MARKET = market_on(RISING)
    BOND = linker()
    
    # The index ratio on a day in March reads December and January, three months
    # back, interpolated by the day of the month. A flow in July reads April and
    # May, which aren't published yet, so the inflation curve supplies them.
    ratio = hlx.linker.index_ratio(BOND, MARKET, VALUATION)
    july = dt.date(2026, 7, 15)
    july_ratio = hlx.linker.index_ratio(BOND, MARKET, july)
    print(f"index ratio   {VALUATION}  {ratio:.6f}")
    print(f"index ratio   {july}  {july_ratio:.6f}")
    
    # Each real coupon and the real redemption is scaled by the ratio at its date
    # and discounted on the nominal curve.
    value = BOND.npv(MARKET)
    print(f"value         {value:.6f}")
    
    # A TIPS quote is a real price at a real yield on the uninflated flows,
    # compounded semiannually. The index never enters it.
    real_yield = 0.0180
    real_clean = hlx.linker.real_price(BOND, "tips", real_yield, VALUATION)
    real_dirty = hlx.linker.real_price(BOND, "tips", real_yield, VALUATION, hlx.bonds.PriceBasis.Dirty)
    yield_back = hlx.linker.real_yield(BOND, "tips", real_clean, VALUATION)
    print(f"real yield    {real_yield * 100:.4f}%")
    print(f"real clean    {real_clean:.6f}")
    print(f"real dirty    {real_dirty:.6f}")
    print(f"yield back    {yield_back * 100:.10f}%")
    
    # Settlement inflates the real quote by the street index ratio: TIPS rounds
    # the reference index and the ratio to five decimals.
    invoice_ratio, clean, accrued, dirty = hlx.linker.invoice(BOND, MARKET, "tips", real_clean, VALUATION)
    print(f"street ratio  {invoice_ratio:.5f}")
    print(f"clean         {clean:.6f}")
    print(f"accrued       {accrued:.6f}")
    print(f"dirty         {dirty:.6f}")
    index ratio   2026-03-02  1.012621
    index ratio   2026-07-15  1.019538
    value         99.056828
    real yield    1.8000%
    real clean    100.643416
    real dirty    100.881675
    yield back    1.8000000000%
    street ratio  1.01262
    clean         101.913536
    accrued       0.241267
    dirty         102.154802

    Today's ratio interpolates the December and January prints; July's reads April and May off the curve, since they aren't published yet. The real price and yield never touch the index. The invoice inflates the real clean price and the real accrued by the street ratio, which TIPS rounds to five decimals.

    How it's priced

    With I0I_0 and I1I_1 the two monthly levels a date dd sits between, after the lag, and each real flow aka_k:

    R(d)=I0+(I1−I0) day(d)−1DIbase,V=∑kak R(Tk) DF(Tk),Pclean=Pcleanreal Rs.R(d) = \frac{I_0 + (I_1 - I_0)\,\frac{\text{day}(d) - 1}{D}}{I_{\text{base}}}, \qquad V = \sum_k a_k\,R(T_k)\,DF(T_k), \qquad P_{\text{clean}} = P^{\text{real}}_{\text{clean}}\,R_s.

    With a floor the redemption is 100 max⁡(R(Tn),1)100\,\max(R(T_n), 1). Why the quote is real and the cash nominal, and how the lag works, is covered in how bonds work.

    API

    Argument Type Default Meaning
    currency, index str, str required the bond's currency and its price index, such as "US-CPI-U"
    base_index float required the index level the ratio divides by
    real_coupon float required the real coupon rate
    lag_months int required the observation lag, three for TIPS
    frequency, day_count dates.Frequency, dates.DayCountFraction required the coupon schedule and accrual
    face, issue, maturity float, date, date required the face and the dates
    redemption_floor bool None, floored repays at least the face, even after deflation; False drops the floor on the redemption and on every paydown
    outstanding list[float] None, a bullet the fraction of face alive in each period
    index_interpolation str the index's registered convention "Month" reads the lagged month alone; "Interpolated" blends it with the next by the day of the month
    issuer, security_spread str None the issuer's credit, or a spread key on the discounting
    payment_calendar, payment_convention str None, "following" how payment dates roll
    accrual_calendar, business_day_convention str None, "following" how accrual dates roll

    products.inflation_linked_bond(currency, index, *, base_index, real_coupon, ...) books it; in Rust, InflationLinkedBond::new(..)?, with .with_redemption_floor, .with_amortisation and .with_interpolation.

    Symbol Meaning Rust Python
    R(d)R(d) index ratio InflationLinkedBond::index_ratio(&resolved, d) linker.index_ratio(bond, market, d)
    VV value against the market MarketView::npv on AnyInstrument::InflationLinkedBond Instrument.npv
    PrealP^{\text{real}} real price at a real yield real_price(&bond, street, y_r, s, basis)? linker.real_price(bond, "tips", y_r, s, basis)
    yry_r real yield from a real clean price real_yield(&bond, street, clean, s)? linker.real_yield(bond, "tips", clean, s)
    RsR_s street index ratio street_index_ratio(&bond, street, s, &fixings, curve)? linker.street_index_ratio(bond, market, "tips", s)
    the invoice street ratio, clean, accrued, dirty invoice(&bond, street, clean, s, &fixings, curve)? linker.invoice(bond, market, "tips", clean, s)

    Defaults and limits

    • Market inputs: the currency's discount curve, an inflation curve for the index (MarketContext.with_inflation_curve) for months not yet published, and the published prints as fixings (MarketContext.with_fixings).
    • Streets: "tips" is the US street; each market's rounding of the reference index and the ratio is in bond conventions.
    • The floor is valued at its intrinsic amount on the projected index.
    • Amortising linkers: each coupon is indexed on the face alive and each principal slice at its own date's ratio, floored at par. The UK money price and yield, the Brazilian price and the Australian price are defined on a bullet, and are refused for a linker that amortises.

    More examples

    The same linker under a falling CPI curve with and without its floor, repaying half its face in July 2030, and observing the index by month or interpolated.

    Rust
    use helixrs::statics::indices::price::IndexInterpolation;
    use chrono::{Datelike, NaiveDate};
    use helixio_docs_sheets::USD_CLOSE_MARCH;
    use helixrs::LinkerConvention;
    use helixrs::{
        AnyInstrument, CollateralSpec, Currency, DayCountFraction, FixedRateBond, FixingKey,
        FixingStore, Frequency, Inflation, InflationCurve, InflationLinkedBond, LevelInterpolation,
        MarketContext, PiecewiseInflation, PriceBasis, PricingRiskFactors, QuoteSheet, YieldBasis,
        YieldCurve, dirty_price, invoice, real_price, street_index_ratio,
    };
    
    fn date(year: i32, month: u32, day: u32) -> NaiveDate {
        NaiveDate::from_ymd_opt(year, month, day).expect("a real calendar date")
    }
    
    const INDEX: &str = "US-CPI-U";
    const BASE: f64 = 320.0;
    const REAL_COUPON: f64 = 0.01875;
    const DECEMBER: f64 = 324.0;
    const JANUARY: f64 = 325.2;
    
    fn inflation_on(reference: NaiveDate, nodes: &[(NaiveDate, f64)]) -> Inflation {
        Inflation::Piecewise(
            PiecewiseInflation::new(
                reference,
                nodes,
                LevelInterpolation::Linear,
                DayCountFraction::Act365Fixed,
            )
            .expect("well-formed nodes"),
        )
    }
    
    fn linker(floored: bool) -> InflationLinkedBond {
        InflationLinkedBond::new(
            Currency::new("USD").expect("a registered currency"),
            INDEX,
            BASE,
            REAL_COUPON,
            3,
            Frequency::SemiAnnual,
            DayCountFraction::ActActIcma,
            100.0,
            date(2025, 7, 15),
            date(2035, 7, 15),
        )
        .expect("a well-formed linker")
        .with_redemption_floor(floored)
    }
    
    fn market_on(nominal_market: &MarketContext, cpi: Inflation) -> MarketContext {
        let fixings = FixingStore::from_observations_checked(&[
            (FixingKey::from(INDEX), date(2025, 12, 1), DECEMBER),
            (FixingKey::from(INDEX), date(2026, 1, 1), JANUARY),
        ])
        .expect("a registered index");
        nominal_market
            .clone()
            .with_inflation_curve(INDEX, cpi)
            .with_fixings(fixings)
    }
    
    fn npv(bond: &InflationLinkedBond, market: &MarketContext) -> f64 {
        let lifted = market.lift::<f64>();
        let view = market.view(&lifted).expect("a market that binds");
        view.npv(&AnyInstrument::InflationLinkedBond(Box::new(bond.clone())))
            .expect("a priced linker")
    }
    
    /// The pricing index ratio at `at`, read through the market the bond resolves.
    fn index_ratio(bond: &InflationLinkedBond, market: &MarketContext, at: NaiveDate) -> f64 {
        let lifted = market.lift::<f64>();
        let view = market.view(&lifted).expect("a market that binds");
        let resolved = view
            .resolve(&bond.risk_factors().expect("a linker declares its curves"))
            .expect("the market carries its curves");
        bond.index_ratio(&resolved, at)
            .expect("an observable index")
    }
    
    fn main() -> Result<(), Box<dyn std::error::Error>> {
        let valuation = date(2026, 3, 2);
        let maturity = date(2035, 7, 15);
        // The nominal market, marked from the morning's SOFR swaps. The CPI curve
        // joins it with the market's own verb.
        let usd = Currency::new("USD").expect("a registered currency");
        let sheet = QuoteSheet::from_rows(USD_CLOSE_MARCH.iter().copied()).expect("a sheet");
        let nominal_market = MarketContext::as_of(valuation)
            .with_recipe_quotes(&CollateralSpec::Currency(usd), &sheet)
            .expect("a marked market");
        let rising = inflation_on(
            valuation,
            &[
                (date(2026, 4, 1), 326.0),
                (date(2031, 4, 1), 360.0),
                (date(2036, 4, 1), 398.0),
            ],
        );
        let market = market_on(&nominal_market, rising.clone());
        let bond = linker(true);
    
        // If prices fall below the base, the floor still repays the face at par.
        let falling = inflation_on(valuation, &[(date(2026, 4, 1), 326.0), (date(2036, 4, 1), 300.0)]);
        let deflating = market_on(&nominal_market, falling);
        let floored = npv(&linker(true), &deflating);
        let unfloored = npv(&linker(false), &deflating);
        let final_ratio = index_ratio(&bond, &deflating, maturity);
        println!("final ratio   {final_ratio:.6}");
        println!("floored       {floored:.6}");
        println!("unfloored     {unfloored:.6}");
        println!("floor worth   {:.6}", floored - unfloored);
    
        // The same linker repaying half its face on 15 July 2030. Each coupon is
        // indexed on the face alive in its period, and each principal payment at
        // its own date's ratio, floored at par.
        let mut profile = vec![1.0; 10];
        profile.extend(vec![0.5; 10]);
        let sinking = linker(true).with_amortisation(profile.clone())?;
        let sinking_value = npv(&sinking, &market);
        let sinking_deflating = npv(&sinking, &deflating);
        println!("rising prices   {sinking_value:.6}");
        println!("falling prices  {sinking_deflating:.6}");
    
        // How each index ratio observes the index inside its month: "Month" reads
        // the lagged reference month's print alone; "Interpolated" blends it with
        // the next month by the day of the month. US CPI's registered convention
        // is interpolated, so stating it changes nothing.
        let flat = npv(&linker(true).with_interpolation(IndexInterpolation::Month), &market);
        let linear = npv(&linker(true).with_interpolation(IndexInterpolation::Interpolated), &market);
        println!("Month         {flat:.6}");
        println!("Interpolated  {linear:.6}");
    
        Ok(())
    }
    Python
    import datetime as dt
    
    import helixio as hlx
    from desk_sheets import USD_CLOSE_MARCH
    
    VALUATION = dt.date(2026, 3, 2)
    INDEX = "US-CPI-U"
    BASE = 320.0
    REAL_COUPON = 0.01875
    ISSUE = dt.date(2025, 7, 15)
    MATURITY = dt.date(2035, 7, 15)
    DECEMBER, JANUARY = 324.0, 325.2
    
    # The nominal market, marked from the morning's SOFR swaps. The CPI curve joins
    # it with the market's own verb, below.
    NOMINAL = hlx.MarketContext(VALUATION).with_recipe_quotes("USD", USD_CLOSE_MARCH)
    
    def linker(floored=True, outstanding=None):
        return hlx.products.inflation_linked_bond(
            "USD", INDEX, base_index=BASE, real_coupon=REAL_COUPON, lag_months=3, frequency=hlx.dates.Frequency.SemiAnnual,
            day_count=hlx.dates.DayCountFraction.ActActIcma, face=100.0, issue=ISSUE, maturity=MATURITY,
            redemption_floor=floored, outstanding=outstanding,
        )
    
    def market_on(cpi):
        return (
            NOMINAL
            .with_inflation_curve(INDEX, cpi)
            .with_fixings([(INDEX, dt.date(2025, 12, 1), DECEMBER), (INDEX, dt.date(2026, 1, 1), JANUARY)])
        )
    
    RISING = hlx.curves.InflationCurve(
        VALUATION, [(dt.date(2026, 4, 1), 326.0), (dt.date(2031, 4, 1), 360.0), (dt.date(2036, 4, 1), 398.0)]
    )
    MARKET = market_on(RISING)
    BOND = linker()
    
    # If prices fall below the base, the floor still repays the face at par.
    falling = hlx.curves.InflationCurve(
        VALUATION, [(dt.date(2026, 4, 1), 326.0), (dt.date(2036, 4, 1), 300.0)]
    )
    deflating = market_on(falling)
    floored = linker(True).npv(deflating)
    unfloored = linker(False).npv(deflating)
    final_ratio = hlx.linker.index_ratio(BOND, deflating, MATURITY)
    print(f"final ratio   {final_ratio:.6f}")
    print(f"floored       {floored:.6f}")
    print(f"unfloored     {unfloored:.6f}")
    print(f"floor worth   {floored - unfloored:.6f}")
    
    # The same linker repaying half its face on 15 July 2030. Each coupon is
    # indexed on the face alive in its period, and each principal payment at its
    # own date's ratio, floored at par.
    PROFILE = [1.0] * 10 + [0.5] * 10
    sinking = linker(True, PROFILE)
    sinking_value = sinking.npv(MARKET)
    sinking_deflating = sinking.npv(deflating)
    print(f"rising prices   {sinking_value:.6f}")
    print(f"falling prices  {sinking_deflating:.6f}")
    
    # How each index ratio observes the index inside its month: "Month" reads the
    # lagged reference month's print alone; "Interpolated" blends it with the next
    # month by the day of the month. US CPI's registered convention is
    # interpolated, so stating it changes nothing.
    def observed(spelling):
        return hlx.products.inflation_linked_bond(
            "USD", INDEX, base_index=BASE, real_coupon=REAL_COUPON, lag_months=3, frequency=hlx.dates.Frequency.SemiAnnual,
            day_count=hlx.dates.DayCountFraction.ActActIcma, face=100.0, issue=ISSUE, maturity=MATURITY,
            redemption_floor=True, index_interpolation=spelling,
        ).npv(MARKET)
    
    flat, linear = observed("Month"), observed("Interpolated")
    print(f"Month         {flat:.6f}")
    print(f"Interpolated  {linear:.6f}")
    final ratio   0.945339
    floored       82.237179
    unfloored     78.534814
    floor worth   3.702365
    rising prices   100.143061
    falling prices  87.105630
    Month         98.984796
    Interpolated  99.056828

    Under deflation the final ratio is below one, and the floor is worth the shortfall on the face, discounted from maturity. The amortising linker is worth less as prices fall. US CPI's registered convention is interpolated, so stating it changes nothing, and reading by month gives a different value.

    See also

    • Source: crates/helixrs/src/instruments/bonds/linker.rs
    • Related pages:
      • How bonds work, for real quotes, the lag and the floor.
      • Inflation curves, for the curve that projects the index.
      • Inflation calibration, for solving one from swap quotes.
      • Inflation swaps, for the swaps on the same index.

    From the 0.0 line at revision bf7f2e2. Every printed number is that revision's own output.