Esc

    The API is under testing and may change before release.

    Parametric fits

    This page lists what you need to fit a smooth parametric curve to a universe of bonds: the forms the library offers, the fit's arguments and weights, how to read the natural parameters back, and how to read each bond's misfit as rich or cheap.

    Quick example

    This fits a Nelson-Siegel curve to eight bonds, reads back its natural parameters, prints the curve beside the formula, and lists each bond's misfit. 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;
    use helixrs::{
        Curve, DayCountFraction, FitWeights, FittedForm, FixedRateBond, Frequency, YieldCurve,
        fit_bond_curve,
    };
    
    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);
        // Eight Treasury-style bullets, semiannual on ACT/ACT ICMA, at their clean
        // prices: issue, maturity, coupon, price.
        let universe = [
            ((2025, 11, 15), (2027, 11, 15), 0.0400, 99.80),
            ((2025, 8, 15), (2028, 8, 15), 0.0375, 99.10),
            ((2024, 11, 15), (2029, 11, 15), 0.0425, 99.95),
            ((2026, 2, 15), (2031, 2, 15), 0.0400, 98.70),
            ((2026, 5, 15), (2033, 5, 15), 0.0425, 99.00),
            ((2025, 11, 15), (2035, 11, 15), 0.0450, 99.60),
            ((2026, 8, 15), (2046, 8, 15), 0.0475, 98.40),
            ((2026, 5, 15), (2056, 5, 15), 0.0475, 96.90),
        ];
    
        let mut quotes = Vec::new();
        for (issue, maturity, coupon, price) in universe {
            let bond = FixedRateBond::bullet(
                "USD".parse()?,
                date(issue),
                date(maturity),
                coupon,
                Frequency::SemiAnnual,
                DayCountFraction::ActActIcma,
                100.0,
            )?;
            quotes.push((bond, price));
        }
        let fitted = fit_bond_curve(reference, &quotes, FittedForm::NelsonSiegel, FitWeights::InverseDuration)?;
        let Curve::NelsonSiegel(form) = &fitted.curve else { unreachable!("a Nelson-Siegel fit") };
        let (level, slope, hump, scale) = form.natural();
        println!("parameters {}", fitted.curve.parameter_count());
        println!("level {level:.8}  slope {slope:.8}  hump {hump:.8}  scale {scale:.6}");
    
        // The curve's ten-year zero rate is the Nelson-Siegel formula at those numbers.
        let ten = date((2036, 10, 1));
        let x = (ten - reference).num_days() as f64 / 365.0 / scale;
        let loading = (1.0 - (-x).exp()) / x;
        let formula = level + slope * loading + hump * (loading - (-x).exp());
        let zero = fitted.curve.view::<f64>().zero_rate(ten)?;
        println!("10Y zero  {zero:.10}");
        println!("formula   {formula:.10}");
    
        // How far each bond sits from the curve, as a yield in basis points.
        for row in &fitted.rows {
            println!(
                "{}  market {:.3}  model {:.3}  misfit {:+.2}bp",
                row.maturity,
                row.market_clean,
                row.model_clean,
                row.yield_misfit * 1e4
            );
        }
    
        Ok(())
    }
    Python
    import datetime as dt
    import math
    
    import helixio as hlx
    from desk_sheets import SOFR_CLOSE_DATE
    
    REFERENCE = SOFR_CLOSE_DATE
    # Eight Treasury-style bullets, semiannual on ACT/ACT ICMA, at their clean
    # prices: issue, maturity, coupon, price.
    UNIVERSE = [
        (dt.date(2025, 11, 15), dt.date(2027, 11, 15), 0.0400, 99.80),
        (dt.date(2025, 8, 15), dt.date(2028, 8, 15), 0.0375, 99.10),
        (dt.date(2024, 11, 15), dt.date(2029, 11, 15), 0.0425, 99.95),
        (dt.date(2026, 2, 15), dt.date(2031, 2, 15), 0.0400, 98.70),
        (dt.date(2026, 5, 15), dt.date(2033, 5, 15), 0.0425, 99.00),
        (dt.date(2025, 11, 15), dt.date(2035, 11, 15), 0.0450, 99.60),
        (dt.date(2026, 8, 15), dt.date(2046, 8, 15), 0.0475, 98.40),
        (dt.date(2026, 5, 15), dt.date(2056, 5, 15), 0.0475, 96.90),
    ]
    
    quotes = [
        (hlx.products.fixed_rate_bond("USD", issue, maturity, coupon=coupon, frequency="6M", day_count="ACT/ACT.ICMA"), price)
        for issue, maturity, coupon, price in UNIVERSE
    ]
    curve, rows = hlx.curves.fit_bond_curve(REFERENCE, quotes, form="nelson_siegel")
    level, slope, hump, scale = curve.natural()
    print(f"parameters {curve.parameter_count()}")
    print(f"level {level:.8f}  slope {slope:.8f}  hump {hump:.8f}  scale {scale:.6f}")
    
    # The curve's ten-year zero rate is the Nelson-Siegel formula at those numbers.
    ten = dt.date(2036, 10, 1)
    x = (ten - REFERENCE).days / 365.0 / scale
    loading = (1.0 - math.exp(-x)) / x
    formula = level + slope * loading + hump * (loading - math.exp(-x))
    print(f"10Y zero  {curve.zero_rate(ten):.10f}")
    print(f"formula   {formula:.10f}")
    
    # How far each bond sits from the curve, as a yield in basis points.
    for row in rows:
        print(f"{row.maturity}  market {row.market_clean:.3f}  model {row.model_clean:.3f}  misfit {row.yield_misfit * 1e4:+.2f}bp")
    parameters 4
    level 0.05393351  slope -0.01248426  hump -0.01298477  scale 3.934506
    10Y zero  0.0457279409
    formula   0.0457279409
    2027-11-15  market 99.800  model 99.778  misfit +1.99bp
    2028-08-15  market 99.100  model 99.157  misfit -3.22bp
    2029-11-15  market 99.950  model 99.938  misfit +0.41bp
    2031-02-15  market 98.700  model 98.695  misfit +0.12bp
    2033-05-15  market 99.000  model 98.931  misfit +1.21bp
    2035-11-15  market 99.600  model 99.598  misfit +0.03bp
    2046-08-15  market 98.400  model 98.654  misfit -2.03bp
    2056-05-15  market 96.900  model 96.689  misfit +1.40bp

    A positive misfit is a bond whose market price sits above the curve's model price: rich.

    How it's priced

    The Nelson-Siegel zero rate at maturity tt in years is

    z(t)=β0+β1 L(x)+β2(L(x)−e−x),L(x)=1−e−xx,x=tλ.z(t) = \beta_0 + \beta_1\, L(x) + \beta_2 \big(L(x) - e^{-x}\big), \qquad L(x) = \frac{1 - e^{-x}}{x}, \quad x = \frac{t}{\lambda}.

    The loading LL falls from one at t=0t = 0 towards zero, so β0+β1\beta_0 + \beta_1 is the short rate and β0\beta_0 the long-run level; β2\beta_2 adds a hump at maturities near λ\lambda. Both loadings are at most 1/x1/x, so far out

    ∣z(t)−β0∣≤(∣β1∣+∣β2∣) λt.|z(t) - \beta_0| \le \big(|\beta_1| + |\beta_2|\big)\, \frac{\lambda}{t}.

    Why a government curve is fitted rather than bootstrapped is covered in how curves work.

    API

    Symbol Meaning Rust Python
    — a fit to bonds at clean prices fit_bond_curve(reference, &quotes, FittedForm::NelsonSiegel, FitWeights::InverseDuration) curves.fit_bond_curve(reference, quotes, form="nelson_siegel")
    β0,β1,β2,λ\beta_0, \beta_1, \beta_2, \lambda the natural parameters NelsonSiegel::natural on Curve::NelsonSiegel curves.DiscountCurve.natural
    z(t)z(t) the fitted curve's zero rate YieldCurve::zero_rate curves.DiscountCurve.zero_rate
    misfit each bond against the curve BondFitRow fields curves.BondFitRow properties
    — a bond in the universe FixedRateBond::bullet products.fixed_rate_bond

    curves.fit_bond_curve(...) returns the fitted curve and one row per bond:

    Argument Type Default Meaning
    reference date required the curve's reference date
    quotes list[tuple[FixedRateBond, float]] required each bond with its clean price
    form str "nelson_siegel" "nelson_siegel" (or "ns"), "svensson" (or "nss"), or "exponential_splines" (or "splines"); in Rust, a FittedForm
    weights str "inverse_duration" "inverse_duration" (or "duration") or "equal"; in Rust, a FitWeights
    basis int 3 the number of exponentials, read by the splines form only

    Defaults and limits

    • The forms: Nelson-Siegel has four natural parameters (β0,β1,β2,λ\beta_0, \beta_1, \beta_2, \lambda), Svensson six (a second hump and decay), and exponential splines a basis of basis decaying exponentials. natural() reads them back in that order.
    • Weights: by default each bond's price error is weighted by the inverse of its duration, so the fit matches yields rather than prices. "equal" weights prices equally, and the answer changes.
    • A smoother, not a repricer: a handful of parameters can't reproduce every price, and the rows say how far each bond misses.
    • Failure: the fit raises CalibrationError if the universe can't determine the form or the solve doesn't converge.

    See also

    • Source: crates/helixrs/src/curves/fit.rs, crates/helixrs/src/curves/parametric.rs
    • Related pages:
      • Bootstrapping, for a curve that reprices every quote.
      • Reading a curve, for the zero rate.

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