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.
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(())
}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 discounts each flow to the dirty price; the G- and I-spreads are the bond's yield over a curve's yield at maturity, in the same convention; spread duration is the flows' present-value weighted mean time at the spread:
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 |
|---|---|---|---|
| z-spread from a quote | z_spread_with |
bonds.z_spread |
|
| the bond's yield | bond.yield_(price, s, basis, convention, None) |
FixedRateBond.yield_ |
|
| the curve's yield at maturity | curve_yield_at |
curves.yield_at |
|
| spread | G- or I-spread | spread_over_curve |
bonds.spread_over_curve |
| 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_spreadreads the bond's own discount curve from the market;spread_over_curvetakes 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.