Esc

    The API is under testing and may change before release.

    Spread measures

    This page lists the spreads helixio measures between a bond's quote and a curve: the z-spread, the G-spread and the I-spread, how a spread is pinned on a bond, and spread duration, with an example you can run. Each turns one bond price into a number of basis points over a reference curve.

    Quick example

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

    This measures an eight-year USD corporate paying 4.75% semiannually on 30/360, quoted at 98.25 clean, against two curves: the SOFR swap curve marked from the morning's sheet, and a government curve stated from its zero rates in the file. It shows each call once: the z-spread from a clean and a dirty price, the spread pinned on the bond to reprice the quote, the G- and I-spreads beside the yields they compare, and spread duration. To measure your own bond's spreads step by step, see measure a bond's spread to a curve.

    Rust
    use chrono::{Datelike, NaiveDate};
    use helixio_docs_sheets::USD_CLOSE_MARCH;
    use helixrs::{
        AnyInstrument, CallableBond, CollateralSpec, Currency, Curve, DayCountFraction, FixedRateBond,
        FlatSpread, Frequency, Interpolation, MarketContext, PiecewiseDiscount, PriceBasis,
        QuoteSheet, RiskFactors, YieldCurve, YieldQuoteConvention, curve_yield_at, pv_weighted_time,
        spread_over_curve, z_spread_with,
    };
    
    fn date(year: i32, month: u32, day: u32) -> NaiveDate {
        NaiveDate::from_ymd_opt(year, month, day).expect("a real calendar date")
    }
    
    const COUPON: f64 = 0.0475;
    const QUOTE: f64 = 98.25;
    const KEY: &str = "ACME-2033";
    
    /// A discount curve through continuously compounded zero rates.
    fn curve_on(reference: NaiveDate, zero_rates: &[(i32, f64)]) -> Curve {
        let pillars: Vec<(NaiveDate, f64)> = zero_rates
            .iter()
            .map(|&(years, rate)| {
                let at = date(reference.year() + years, reference.month(), reference.day());
                let t = (at - reference).num_days() as f64 / 365.0;
                (at, (-rate * t).exp())
            })
            .collect();
        Curve::Piecewise(
            PiecewiseDiscount::from_discount_factors(reference, &pillars)
                .expect("well-formed pillars")
                .with_interpolation(Interpolation::LogLinear)
                .expect("a valid scheme")
                .with_rate_day_count(DayCountFraction::Act365Fixed),
        )
    }
    
    /// A flat security spread, as a curve a market can pin.
    fn flat_spread(reference: NaiveDate, spread: f64) -> Curve {
        Curve::FlatSpread(FlatSpread::new(reference, spread).expect("a finite spread"))
    }
    
    fn main() -> Result<(), Box<dyn std::error::Error>> {
        let settlement = date(2026, 3, 2);
        let maturity = date(2033, 5, 15);
        let usd = Currency::new("USD").expect("a registered currency");
        let bond = FixedRateBond::bullet(
            usd,
            date(2025, 5, 15),
            maturity,
            COUPON,
            Frequency::SemiAnnual,
            DayCountFraction::Thirty360,
            100.0,
        )
        .expect("a well-formed bond");
        let coupon_dates: Vec<NaiveDate> = (0..15)
            .map(|k| date(2026 + k / 2, if k % 2 == 0 { 5 } else { 11 }, 15))
            .collect();
        let flow_amount = |k: usize| {
            let redemption = if k == coupon_dates.len() - 1 {
                100.0
            } else {
                0.0
            };
            100.0 * COUPON / 2.0 + redemption
        };
        let ust = YieldQuoteConvention::UsStreet;
    
        let government = curve_on(
            settlement,
            &[
                (1, 0.0380),
                (2, 0.0370),
                (3, 0.0365),
                (5, 0.0370),
                (7, 0.0380),
                (10, 0.0395),
            ],
        );
        // The swap curve is the market's own, marked from the morning's SOFR swaps;
        // the government curve is a Treasury zero curve, which no recipe builds.
        let collateral = CollateralSpec::Currency(usd);
        let sheet = QuoteSheet::from_rows(USD_CLOSE_MARCH.iter().copied()).expect("a sheet");
        let market = MarketContext::as_of(settlement)
            .with_recipe_quotes(&collateral, &sheet)
            .expect("a marked market");
        let (swap, _) = market
            .curve(&"discount(USD)".parse().expect("a curve id"))
            .expect("the USD curve");
        let dirty_price = QUOTE + bond.accrued(settlement);
        let swap_reader = swap.view::<f64>();
        let government_reader = government.view::<f64>();
    
        // The z-spread as the binding reads it: the bond discounted on its
        // currency's curve, with its own spread key where one is named.
        let z_spread_on = |market: &MarketContext, price: f64, basis: PriceBasis, key: Option<&str>| {
            let lifted = market.lift::<f64>();
            let view = market.view(&lifted).expect("a market that binds");
            let mut factors = RiskFactors::discounted_by(&collateral);
            if let Some(key) = key {
                factors = factors.spread_by(key);
            }
            let resolved = view.resolve(&factors).expect("the curves are pinned");
            z_spread_with(&bond, &resolved, price, basis).expect("a bracketed spread")
        };
        let bond_on_spread = AnyInstrument::Callable(Box::new(
            CallableBond::new(bond.clone(), Vec::new()).spread_by(KEY),
        ));
        let value_on = |market: &MarketContext| {
            let lifted = market.lift::<f64>();
            let view = market.view(&lifted).expect("a market that binds");
            view.npv(&bond_on_spread).expect("a priced bond")
        };
    
        // The z-spread is the one continuous spread that, added to every zero rate
        // of the curve, discounts the flows to the dirty price. A clean quote and
        // its dirty price give the same spread.
        let z = z_spread_on(&market, QUOTE, PriceBasis::Clean, None);
        let z_from_dirty = z_spread_on(&market, dirty_price, PriceBasis::Dirty, None);
        println!("clean quote        {QUOTE:.6}");
        println!("dirty price        {dirty_price:.6}");
        println!("z-spread           {:.4} bp", z * 10_000.0);
        println!("from the dirty     {:.4} bp", z_from_dirty * 10_000.0);
    
        // Pin the z-spread as the bond's own security spread and value the bond on
        // it: the quote comes back, and nothing is left for a second z-spread.
        let pinned = market.clone().with_security_spread(KEY, flat_spread(settlement, z));
        let repriced = value_on(&pinned);
        let residual = z_spread_on(&pinned, QUOTE, PriceBasis::Clean, Some(KEY));
        println!("value on spread    {repriced:.6}");
        println!("residual spread    {:.6} bp", residual.abs() * 10_000.0);
    
        // A G-spread is the bond's yield over the government curve's yield at
        // maturity; an I-spread the same over the swap curve. One call, two curves.
        let basis = ust.basis(bond.frequency).expect("a compounding convention");
        let curve_basis = ust.basis(Frequency::Annual).expect("a compounding convention");
        let bond_ytm = bond
            .yield_(dirty_price, settlement, PriceBasis::Dirty, ust, None)
            .expect("a bracketed solve");
        let g_spread = spread_over_curve(&bond, &government_reader, dirty_price, settlement, basis)?;
        let i_spread = spread_over_curve(&bond, &swap_reader, dirty_price, settlement, basis)?;
        let government_yield = curve_yield_at(&government_reader, maturity, curve_basis)?;
        let swap_yield = curve_yield_at(&swap_reader, maturity, curve_basis)?;
        println!("bond yield         {:.6}%", bond_ytm * 100.0);
        println!("government at mat. {:.6}%", government_yield * 100.0);
        println!("swap at mat.       {:.6}%", swap_yield * 100.0);
        println!("G-spread           {:.4} bp", g_spread * 10_000.0);
        println!("I-spread           {:.4} bp", i_spread * 10_000.0);
    
        // Spread duration is the present-value weighted mean time of the flows,
        // discounted at the curve plus the spread. You pass the flows as (time,
        // value on the curve) pairs.
        let mut flows = Vec::new();
        for (k, paid) in coupon_dates.iter().enumerate() {
            let tau = (*paid - settlement).num_days() as f64 / 365.0;
            flows.push((tau, flow_amount(k) * swap_reader.discount(*paid)?));
        }
        let continuous = YieldQuoteConvention::Continuous
            .basis(Frequency::Annual)
            .expect("a compounding convention");
        let duration = pv_weighted_time(&flows, z, continuous);
        println!("spread duration    {duration:.6}");
    
        Ok(())
    }
    Python
    import datetime as dt
    import math
    
    import helixio as hlx
    from desk_sheets import USD_CLOSE_MARCH
    
    SETTLEMENT = dt.date(2026, 3, 2)
    MATURITY = dt.date(2033, 5, 15)
    COUPON = 0.0475
    BOND = hlx.products.fixed_rate_bond(
        "USD", dt.date(2025, 5, 15), MATURITY, coupon=COUPON, frequency=hlx.dates.Frequency.SemiAnnual,
        day_count=hlx.dates.DayCountFraction.Thirty360,
    )
    CLEAN = hlx.bonds.PriceBasis.Clean
    DIRTY = hlx.bonds.PriceBasis.Dirty
    UST = hlx.bonds.YieldConvention.UsStreet
    COUPON_DATES = [dt.date(2026 + k // 2, 5 if k % 2 == 0 else 11, 15) for k in range(15)]
    
    def curve_on(reference, zero_rates):
        """A discount curve through continuously compounded zero rates."""
        pillars = []
        for years, rate in zero_rates:
            at = dt.date(reference.year + years, reference.month, reference.day)
            t = (at - reference).days / 365.0
            pillars.append((at, math.exp(-rate * t)))
        return hlx.curves.DiscountCurve(reference, pillars)
    
    GOVERNMENT = curve_on(SETTLEMENT, [(1, 0.0380), (2, 0.0370), (3, 0.0365), (5, 0.0370), (7, 0.0380), (10, 0.0395)])
    # The swap curve is the market's own, marked from the morning's SOFR swaps; the
    # government curve is a Treasury zero curve, which no recipe builds.
    MARKET = hlx.MarketContext(SETTLEMENT).with_recipe_quotes("USD", USD_CLOSE_MARCH)
    SWAP = MARKET.curve("discount(USD)")
    QUOTE = 98.25
    DIRTY_PRICE = QUOTE + BOND.accrued(SETTLEMENT)
    
    def flow_amount(k):
        """The k-th remaining flow per 100 face: a coupon, and the face at the end."""
        return 100.0 * COUPON / 2.0 + (100.0 if k == len(COUPON_DATES) - 1 else 0.0)
    
    # The z-spread is the one continuous spread that, added to every zero rate of
    # the curve, discounts the flows to the dirty price. A clean quote and its
    # dirty price give the same spread.
    z = hlx.bonds.z_spread(BOND, MARKET, QUOTE, CLEAN)
    z_from_dirty = hlx.bonds.z_spread(BOND, MARKET, DIRTY_PRICE, DIRTY)
    print(f"clean quote        {QUOTE:.6f}")
    print(f"dirty price        {DIRTY_PRICE:.6f}")
    print(f"z-spread           {z * 10_000:.4f} bp")
    print(f"from the dirty     {z_from_dirty * 10_000:.4f} bp")
    
    # Pin the z-spread as the bond's own security spread and value the bond on
    # it: the quote comes back, and nothing is left for a second z-spread.
    pinned = MARKET.with_security_spread("ACME-2033", hlx.curves.SpreadCurve.flat(SETTLEMENT, z))
    bond_on_spread = hlx.products.callable_bond(BOND, [], security_spread="ACME-2033")
    repriced = bond_on_spread.npv(pinned)
    residual = hlx.bonds.z_spread(BOND, pinned, QUOTE, CLEAN, security_spread="ACME-2033")
    print(f"value on spread    {repriced:.6f}")
    print(f"residual spread    {abs(residual) * 10_000:.6f} bp")
    
    # A G-spread is the bond's yield over the government curve's yield at
    # maturity; an I-spread the same over the swap curve. One call, two curves.
    bond_ytm = BOND.yield_(DIRTY_PRICE, SETTLEMENT, DIRTY, UST)
    g_spread = hlx.bonds.spread_over_curve(BOND, GOVERNMENT, DIRTY_PRICE, SETTLEMENT, UST)
    i_spread = hlx.bonds.spread_over_curve(BOND, SWAP, DIRTY_PRICE, SETTLEMENT, UST)
    government_yield = hlx.curves.yield_at(GOVERNMENT, MATURITY, UST)
    swap_yield = hlx.curves.yield_at(SWAP, MATURITY, UST)
    print(f"bond yield         {bond_ytm * 100:.6f}%")
    print(f"government at mat. {government_yield * 100:.6f}%")
    print(f"swap at mat.       {swap_yield * 100:.6f}%")
    print(f"G-spread           {g_spread * 10_000:.4f} bp")
    print(f"I-spread           {i_spread * 10_000:.4f} bp")
    
    # Spread duration is the present-value weighted mean time of the flows,
    # discounted at the curve plus the spread. You pass the flows as (time, value
    # on the curve) pairs.
    flows = []
    for k, paid in enumerate(COUPON_DATES):
        tau = (paid - SETTLEMENT).days / 365.0
        flows.append((tau, flow_amount(k) * SWAP.discount(paid)))
    duration = hlx.bonds.pv_weighted_time(flows, z, hlx.bonds.YieldConvention.Continuous)
    print(f"spread duration    {duration:.6f}")
    clean quote        98.250000
    dirty price        99.661806
    z-spread           95.0205 bp
    from the dirty     95.0205 bp
    value on spread    99.661806
    residual spread    0.000000 bp
    bond yield         5.041563%
    government at mat. 3.850662%
    swap at mat.       4.079251%
    G-spread           119.0901 bp
    I-spread           96.2312 bp
    spread duration    6.089928

    The clean quote and its dirty price give the same z-spread. Pinned as the bond's security spread, it reprices the quote and leaves nothing for a second z-spread. The G-spread is wider than the I-spread, because the government curve sits below the swap curve at the bond's maturity.

    How it's priced

    The z-spread zz discounts each flow aka_k to the dirty price; the G- and I-spreads are the bond's yield yy over a curve's yield yc(T)y_c(T) at maturity, in the same convention; spread duration is the flows' present-value weighted mean time at the spread:

    Pdirty=∑kak DF(Tk) e−z τk,spread=y−yc(T),Ds=∑kτk ak DF(Tk) e−z τk∑kak DF(Tk) e−z τk.P_{\text{dirty}} = \sum_k a_k\,DF(T_k)\,e^{-z\,\tau_k}, \qquad \text{spread} = y - y_c(T), \qquad D_s = \frac{\sum_k \tau_k\,a_k\,DF(T_k)\,e^{-z\,\tau_k}}{\sum_k a_k\,DF(T_k)\,e^{-z\,\tau_k}}.

    What each spread is for, and why the G- and I-spreads differ by the swap spread, is covered in how bonds work.

    API

    Argument Type Default Meaning
    bonds.z_spread(bond, market, price, price_basis="clean", security_spread=None) FixedRateBond, MarketContext, float, bonds.PriceBasis or str, str price_basis="clean", security_spread=None the z-spread over the bond's discount curve that reprices price; with security_spread, over that spread as well
    bonds.spread_over_curve(bond, curve, dirty, settlement, convention) FixedRateBond, DiscountCurve, float, date, convention — the bond's yield over the curve's yield at maturity: a G-spread on a government curve, an I-spread on a swap curve
    curves.yield_at(curve, date, convention) DiscountCurve, date, convention — the curve's yield at a date, in the convention given
    bonds.pv_weighted_time(flows, spread, convention) list[(float, float)], float, convention — spread duration from (time, value on the curve) pairs
    MarketContext.with_security_spread(key, curve) str, SpreadCurve — pins a spread under a key; a bond built with security_spread=key discounts under it

    In Rust the same calls are z_spread_with(&bond, &resolved, price, basis), spread_over_curve(&bond, &curve, dirty, s, basis), curve_yield_at(&curve, T, basis) and pv_weighted_time(&flows, z, basis), with the convention passed as a compounding basis.

    Symbol Meaning Rust Python
    zz z-spread from a quote z_spread_with bonds.z_spread
    yy the bond's yield bond.yield_(price, s, basis, convention, None) FixedRateBond.yield_
    yc(T)y_c(T) the curve's yield at maturity curve_yield_at curves.yield_at
    spread G- or I-spread spread_over_curve bonds.spread_over_curve
    DsD_s spread duration pv_weighted_time with a continuous basis bonds.pv_weighted_time with YieldConvention.Continuous

    Defaults and limits

    • Clean or dirty: pass the quote with its basis; both give the same z-spread.
    • Which curve: z_spread reads the bond's own discount curve from the market; spread_over_curve takes any discount curve you hand it, so a government curve that no recipe builds can be stated from its zero rates.
    • Flows for spread duration: you build the (time, value on the curve) pairs, with times in years of 365 days.
    • A unique spread: the price falls strictly as the spread rises, so each quote has one z-spread.

    See also

    • Source: crates/helixrs/src/instruments/bonds.rs
    • Related pages:
      • How bonds work, for what each spread measures.
      • Measure a bond's spread to a curve, step by step.
      • Fixed-rate bond, for the bond and its yield.
      • Asset swap, for the spread a bond pays over a swap.
      • Spread curves, for the curve a security spread is stored as.
      • Bond conventions, for each market's yield convention.

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