Esc

    The API is under testing and may change before release.

    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

    1. Mark a EUR market from the €STR quotes with with_recipe_quotes, and give it the index's prints with with_fixings.
    2. 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 with dates.lagged_observation, because that is the date the swap's final index level is observed.
    3. Solve the index curve with calibration.inflation on the €STR discount curve, and file it under the index with with_inflation_curve.
    4. 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.
    • 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.