Spread curves
This page lists what you need to add a security's spread on top of its currency's discounting: a flat spread or a dated one, the arguments each takes, and how either is pinned under a security's key in a market.
Quick example
This builds a flat spread and a dated one, prints each beside its definition, and pins the dated one under a security's key over the SOFR curve. The example reads its quotes from the desk sheets; load them once and it runs as shown.
use chrono::NaiveDate;
use helixio_docs_sheets::{SOFR_CLOSE_DATE, USD_SOFR_CLOSE};
use helixrs::{
CollateralSpec, Curve, FlatSpread, MarketContext, PiecewiseSpread, QuoteSheet, YieldCurve,
};
type Outcome<T> = Result<T, Box<dyn std::error::Error>>;
fn date((year, month, day): (i32, u32, u32)) -> NaiveDate {
NaiveDate::from_ymd_opt(year, month, day).expect("a real calendar date")
}
fn main() -> Outcome<()> {
let reference = date(SOFR_CLOSE_DATE);
let (one, five, mid) = (date((2027, 10, 1)), date((2031, 10, 1)), date((2029, 10, 1)));
let years = |d: NaiveDate| (d - reference).num_days() as f64 / 365.0;
// One spread for every date: 150 basis points, no pillars, no risk of its own.
let flat = FlatSpread::new(reference, 0.0150)?;
let flat_factor = flat.view::<f64>().discount(five)?;
println!("pillars {}", flat.parameters().len());
println!("factor 5Y {flat_factor:.10}");
println!("exp(-s t) {:.10}", (-0.0150 * years(five)).exp());
// A spread of 100 basis points at one year and 200 at five, linear in time
// between them and flat outside.
let dated = PiecewiseSpread::new(reference, &[(one, 0.0100), (five, 0.0200)])?;
let weight = (years(mid) - years(one)) / (years(five) - years(one));
let spread = dated.spread_at(mid)?;
let factor = dated.view::<f64>().discount(mid)?;
println!("spread 3Y {spread:.8}");
println!("by the line {:.8}", 0.0100 + weight * 0.0100);
println!("factor 3Y {factor:.10}");
// Pinned under a security's key, the spread multiplies the factor of the curve
// the security's currency discounts on.
let sheet = QuoteSheet::from_rows(USD_SOFR_CLOSE.iter().copied())?;
let market = MarketContext::as_of(reference)
.with_recipe_quotes(&CollateralSpec::Currency("USD".parse()?), &sheet)?
.with_security_spread("ACME-2031", Curve::PiecewiseSpread(dated.clone()));
for (role, key) in market.contents() {
println!("{role:<16} {key}");
}
// A bond naming the security discounts on this product; here it is formed by the formula.
let (curve, _) = market.curve(&"discount(USD)".parse()?).expect("the USD curve");
let base = curve.view::<f64>().discount(mid)?;
println!("risky 3Y, by the formula {:.10}", base * factor);
Ok(())
}import datetime as dt
import math
import helixio as hlx
from desk_sheets import SOFR_CLOSE_DATE, USD_SOFR_CLOSE
REFERENCE = SOFR_CLOSE_DATE
ONE, FIVE, MID = dt.date(2027, 10, 1), dt.date(2031, 10, 1), dt.date(2029, 10, 1)
def years(d: dt.date) -> float:
return (d - REFERENCE).days / 365.0
# One spread for every date: 150 basis points, no pillars, no risk of its own.
flat = hlx.curves.SpreadCurve.flat(REFERENCE, 0.0150)
print(f"pillars {len(flat.pillars())}")
print(f"factor 5Y {flat.discount(FIVE):.10f}")
print(f"exp(-s t) {math.exp(-0.0150 * years(FIVE)):.10f}")
# A spread of 100 basis points at one year and 200 at five, linear in time
# between them and flat outside.
dated = hlx.curves.SpreadCurve(REFERENCE, [(ONE, 0.0100), (FIVE, 0.0200)])
weight = (years(MID) - years(ONE)) / (years(FIVE) - years(ONE))
print(f"spread 3Y {dated.spread(MID):.8f}")
print(f"by the line {0.0100 + weight * 0.0100:.8f}")
print(f"factor 3Y {dated.discount(MID):.10f}")
# Pinned under a security's key, the spread multiplies the factor of the curve
# the security's currency discounts on.
market = (
hlx.MarketContext(REFERENCE)
.with_recipe_quotes("USD", USD_SOFR_CLOSE)
.with_security_spread("ACME-2031", dated)
)
for role, key in market.contents():
print(f"{role:<16} {key}")
# A bond naming the security discounts on this product; here it is formed by the formula.
base = market.curve("discount(USD)").discount(MID)
print(f"risky 3Y, by the formula {base * dated.discount(MID):.10f}")pillars 0
factor 5Y 0.9277053607
exp(-s t) 0.9277053607
spread 3Y 0.01500342
by the line 0.01500342
factor 3Y 0.9559483714
discount USD
projection USD-SOFR
security-spread ACME-2031
risky 3Y, by the formula 0.8316227076
A positive spread lowers every factor the security discounts at.
How it's priced
A spread contributes the factor
with on ACT/365. A dated spread is linear in between pillars, and flat before the first pillar and after the last. On top of a base curve , the security discounts at
Why a bond's holder discounts at more than its currency's curve is covered in how curves work.
API
| Symbol | Meaning | Rust | Python |
|---|---|---|---|
| a flat spread | FlatSpread::new |
curves.SpreadCurve.flat |
|
| a dated spread | PiecewiseSpread::new, read with PiecewiseSpread::spread_at |
curves.SpreadCurve(reference, pillars), read with curves.SpreadCurve.spread |
|
| the spread's factor | YieldCurve::discount on the spread's view |
curves.SpreadCurve.discount |
|
| — | pinned under a security | MarketContext::with_security_spread |
MarketContext.with_security_spread |
| Argument | Type | Default | Meaning |
|---|---|---|---|
reference |
date |
required | the spread's reference date |
pillars |
list[tuple[date, float]] |
required | (date, spread) pairs, for SpreadCurve(reference, pillars) |
spread |
float |
required | the constant spread, for SpreadCurve.flat(reference, spread) |
Defaults and limits
!!! warning "Only a dated spread carries risk"
A flat spread is declared data: it reports no risk of its own. A dated spread's pillars are
free parameters, and report risk on the security_spread axis. Use a dated spread when you
want sensitivity to the spread.
- Outside the pillars: a dated spread is flat before its first pillar and after its last.
- Reach: a spread pinned under a security's key reaches every product that names that security.
See also
- Source:
crates/helixrs/src/curves/spread.rs,crates/helixrs/src/market/store.rs - Related pages:
- Repo spread curves, for a spread in simple-rate space.
- Survival curves, for default risk stated as a probability.
From the 0.0 line at revision bf7f2e2. Every printed number is that revision's own output.