Esc

    The API is under testing and may change before release.

    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.

    Rust
    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(())
    }
    Python
    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 s(t)s(t) contributes the factor

    Ds(t)=e−s(t) τ(t0,t),D_s(t) = e^{-s(t)\, \tau(t_0, t)},

    with τ\tau on ACT/365. A dated spread is linear in τ\tau between pillars, and flat before the first pillar and after the last. On top of a base curve DD, the security discounts at

    Drisky(t)=D(t) Ds(t).D_{\text{risky}}(t) = D(t)\, D_s(t).

    Why a bond's holder discounts at more than its currency's curve is covered in how curves work.

    API

    Symbol Meaning Rust Python
    ss a flat spread FlatSpread::new curves.SpreadCurve.flat
    s(t)s(t) a dated spread PiecewiseSpread::new, read with PiecewiseSpread::spread_at curves.SpreadCurve(reference, pillars), read with curves.SpreadCurve.spread
    Ds(t)D_s(t) 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.