Esc

    The API is under testing and may change before release.

    Measure a bond's spread to a curve

    You'll take a bond's quote and measure how far it trades over the swap curve and over a government curve, as a z-spread, a G-spread and an I-spread.

    Before you start

    • helixio installed: see installing.
    • The swap quotes for the bond's currency, and the government curve's zero rates, or any discount factors you have for it.
    • The bond's terms, the settlement date and its clean quote.

    Steps

    1. Mark the swap curve from its quotes with MarketContext.with_recipe_quotes.
    2. State the government curve with curves.DiscountCurve, from discount factors you build from its zero rates. No recipe builds a government curve.
    3. Describe the bond with products.fixed_rate_bond, and work out its dirty price.
    4. Find the z-spread to the swap curve with bonds.z_spread, passing the clean quote.
    5. Find the G-spread and the I-spread with bonds.spread_over_curve on each curve, passing the dirty price.

    The comments in the program match these steps.

    Rust
    use chrono::{Datelike, NaiveDate};
    use helixrs::{
        AnyInstrument, CallableBond, CollateralSpec, Currency, Curve, DayCountFraction, FixedRateBond,
        FlatSpread, Frequency, Interpolation, MarketContext, PiecewiseDiscount, PriceBasis, QuoteSheet,
        RiskFactors, YieldQuoteConvention, curve_yield_at, 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")
    }
    
    fn main() -> Result<(), Box<dyn std::error::Error>> {
        // 1. Mark the swap curve from the morning's SOFR swap quotes.
        let close = date(2026, 11, 3);
        let usd = Currency::new("USD").expect("a registered currency");
        let swaps = [
            ("USD-SOFR-OIS", "1Y", 0.0412),
            ("USD-SOFR-OIS", "2Y", 0.0405),
            ("USD-SOFR-OIS", "3Y", 0.0403),
            ("USD-SOFR-OIS", "5Y", 0.0408),
            ("USD-SOFR-OIS", "7Y", 0.0416),
            ("USD-SOFR-OIS", "10Y", 0.0428),
        ];
        let collateral = CollateralSpec::Currency(usd);
        let market = MarketContext::as_of(close)
            .with_recipe_quotes(&collateral, &QuoteSheet::from_rows(swaps.iter().copied())?)?;
        let (swap_curve, _) = market.curve(&"discount(USD)".parse()?).expect("the USD curve");
    
        // 2. State the government curve from its continuously compounded zero rates.
        let government_zeros = [(1, 0.0391), (2, 0.0384), (3, 0.0382), (5, 0.0387), (7, 0.0396), (10, 0.0409)];
        let mut pillars = Vec::new();
        for (years, rate) in government_zeros {
            let at = date(close.year() + years, close.month(), close.day());
            pillars.push((at, (-rate * (at - close).num_days() as f64 / 365.0).exp()));
        }
        let government = Curve::Piecewise(
            PiecewiseDiscount::from_discount_factors(close, &pillars)?
                .with_interpolation(Interpolation::LogLinear)?
                .with_rate_day_count(DayCountFraction::Act365Fixed),
        );
    
        // 3. Describe the bond and its quote: a 5.25% semiannual corporate on
        //    30/360, maturing on 1 June 2032, quoted at 101.40 clean.
        let bond = FixedRateBond::bullet(
            usd,
            date(2025, 6, 1),
            date(2032, 6, 1),
            0.0525,
            Frequency::SemiAnnual,
            DayCountFraction::Thirty360,
            100.0,
        )?;
        let clean = 101.40;
        let dirty = clean + bond.accrued(close);
    
        // 4. The z-spread to the swap curve, from the clean quote.
        let lifted = market.lift::<f64>();
        let view = market.view(&lifted)?;
        let z = z_spread_with(&bond, &view.resolve(&RiskFactors::discounted_by(&collateral))?, clean, PriceBasis::Clean)?;
        println!("z-spread to swaps   {:.2} bp", z * 10_000.0);
    
        // 5. The G-spread to the government curve and the I-spread to the swap
        //    curve, both on the street convention.
        let street = YieldQuoteConvention::UsStreet.basis(bond.frequency).expect("a compounding convention");
        let g_spread = spread_over_curve(&bond, &government.view::<f64>(), dirty, close, street)?;
        let i_spread = spread_over_curve(&bond, &swap_curve.view::<f64>(), dirty, close, street)?;
        println!("G-spread            {:.2} bp", g_spread * 10_000.0);
        println!("I-spread            {:.2} bp", i_spread * 10_000.0);
    
        Ok(())
    }
    Python
    import math
    from datetime import date
    
    import helixio as hlx
    
    # 1. Mark the swap curve from the morning's SOFR swap quotes.
    close = date(2026, 11, 3)
    swaps = {"USD-SOFR-OIS": {"1Y": 0.0412, "2Y": 0.0405, "3Y": 0.0403, "5Y": 0.0408, "7Y": 0.0416, "10Y": 0.0428}}
    market = hlx.MarketContext(close).with_recipe_quotes("USD", swaps)
    swap_curve = market.curve("discount(USD)")
    
    # 2. State the government curve from its continuously compounded zero rates.
    government_zeros = [(1, 0.0391), (2, 0.0384), (3, 0.0382), (5, 0.0387), (7, 0.0396), (10, 0.0409)]
    pillars = []
    for years, rate in government_zeros:
        at = date(close.year + years, close.month, close.day)
        pillars.append((at, math.exp(-rate * (at - close).days / 365.0)))
    government = hlx.curves.DiscountCurve(close, pillars)
    
    # 3. Describe the bond and its quote: a 5.25% semiannual corporate on 30/360,
    #    maturing on 1 June 2032, quoted at 101.40 clean.
    bond = hlx.products.fixed_rate_bond(
        "USD", date(2025, 6, 1), date(2032, 6, 1), coupon=0.0525,
        frequency=hlx.dates.Frequency.SemiAnnual, day_count=hlx.dates.DayCountFraction.Thirty360,
    )
    clean = 101.40
    dirty = clean + bond.accrued(close)
    
    # 4. The z-spread to the swap curve, from the clean quote.
    z = hlx.bonds.z_spread(bond, market, clean, hlx.bonds.PriceBasis.Clean)
    print(f"z-spread to swaps   {z * 10_000:.2f} bp")
    
    # 5. The G-spread to the government curve and the I-spread to the swap curve,
    #    both on the street convention.
    street = hlx.bonds.YieldConvention.UsStreet
    g_spread = hlx.bonds.spread_over_curve(bond, government, dirty, close, street)
    i_spread = hlx.bonds.spread_over_curve(bond, swap_curve, dirty, close, street)
    print(f"G-spread            {g_spread * 10_000:.2f} bp")
    print(f"I-spread            {i_spread * 10_000:.2f} bp")
    z-spread to swaps   81.59 bp
    G-spread            101.78 bp
    I-spread            83.06 bp

    Check it worked

    • The G-spread is the widest of the three: the government zeros sit below the swap curve, so the bond's yield is further above them.
    • The z-spread and the I-spread are close but not equal: one spreads every zero rate, the other compares two yields at maturity.
    • Spread measures, the reference for each spread and its arguments.
    • How bonds work, for what each spread measures.
    • Quote a bond from its yield, for the yield the G- and I-spreads start from.

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