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
- Mark the swap curve from its quotes with
MarketContext.with_recipe_quotes. - State the government curve with
curves.DiscountCurve, from discount factors you build from its zero rates. No recipe builds a government curve. - Describe the bond with
products.fixed_rate_bond, and work out its dirty price. - Find the z-spread to the swap curve with
bonds.z_spread, passing the clean quote. - Find the G-spread and the I-spread with
bonds.spread_over_curveon 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.
Related
- 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.