Esc

    The API is under testing and may change before release.

    Interpolation

    You'll see why two curves built from the same quotes can price a forward-starting trade differently: the interpolation scheme decides what a curve answers between its pillars, and so what its forward rates look like.

    A choice with a visible cost

    Log-linear interpolation in the discount factor holds the forward flat within each segment. It is local, stable and easy to hedge, but its forwards jump at every pillar. A spline smooths the forwards across pillars, which a forward-starting trade notices. In exchange, moving one quote moves the curve between other pillars too. Monotone schemes keep a smooth shape while refusing to overshoot.

    A scheme is a value you pass to a curve when you build it. The full list of schemes and their parameters is on the discount curves reference page.

    What a scheme reads

    A curve stores its pillars as year fractions τi\tau_i on ACT/365 and log factors yi=ln⁡D(Ti)y_i = \ln D(T_i), with the anchor (0,0)(0, 0) implicit. A scheme reads y(τ)y(\tau) from those knots and passes through every one of them.

    Under log-linear interpolation yy is linear in τ\tau between neighbouring knots, so between pillars TiT_i and Ti+1T_{i+1}

    D(t)=D(Ti) 1−w D(Ti+1) w,w=τ(t)−τiτi+1−τi,D(t) = D(T_i)^{\,1 - w}\, D(T_{i+1})^{\,w}, \qquad w = \frac{\tau(t) - \tau_i}{\tau_{i+1} - \tau_i},

    and the forward − dy/dτ-\,dy/d\tau is constant within the segment. A cubic spline makes yy a cubic in τ\tau with continuous second derivative, so the forward varies within a segment.

    Five schemes on the same pillars

    The same four pillars under five schemes. Each reproduces every pillar and reads its own value between them; log-linear's forward is one number within a segment, the natural cubic's moves. The program prints the pillar check and log-linear's formula beside the curve's own value. It takes its close date from the desk sheets.

    Rust
    use std::collections::BTreeMap;
    
    use chrono::NaiveDate;
    use helixio_docs_sheets::SOFR_CLOSE_DATE;
    use helixrs::{Curve, Interpolation, PiecewiseDiscount, 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 pillars: [(NaiveDate, f64); 4] = [
            (date((2027, 10, 1)), 0.9580),
            (date((2028, 10, 2)), 0.9150),
            (date((2031, 10, 1)), 0.7920),
            (date((2036, 10, 1)), 0.6180),
        ];
        let schemes = [
            Interpolation::LogLinear,
            Interpolation::MonotoneConvex,
            Interpolation::NaturalCubic,
            Interpolation::TensionSpline { sigma: 2.0 },
            Interpolation::Kruger,
        ];
        let between = date((2030, 4, 1));
    
        // The same four pillars under five schemes. Each reproduces every pillar; each
        // reads its own value between them.
        let mut curves = BTreeMap::new();
        let mut on_pillars = true;
        for scheme in schemes {
            let curve = Curve::Piecewise(
                PiecewiseDiscount::from_discount_factors(reference, &pillars)?.with_interpolation(scheme)?,
            );
            let view = curve.view::<f64>();
            println!("{:<15} {:.10}", scheme.code(), view.discount(between)?);
            for (pillar, factor) in pillars {
                on_pillars &= (view.discount(pillar)? - factor).abs() < 1e-15;
            }
            curves.insert(scheme.code(), curve);
        }
        println!("every scheme reproduces every pillar: {}", if on_pillars { "yes" } else { "no" });
        // Log-linear reads the log factor on a straight line in time between pillars.
        let ((t1, f1), (t2, f2)) = (pillars[1], pillars[2]);
        let w = (between - t1).num_days() as f64 / (t2 - t1).num_days() as f64;
        println!("loglinear by its formula {:.10}", f1.powf(1.0 - w) * f2.powf(w));
    
        // Within the 2028-2031 segment, log-linear's forward is one number; the
        // natural cubic's moves.
        let early = (date((2029, 1, 2)), date((2029, 7, 2)));
        let late = (date((2030, 7, 1)), date((2031, 1, 2)));
        let forwards = |code: &str| -> Outcome<(f64, f64)> {
            let view = curves[code].view::<f64>();
            Ok((view.forward_rate(early.0, early.1)?, view.forward_rate(late.0, late.1)?))
        };
        println!("forward         2029 H1    2030 H2");
        for code in ["loglinear", "naturalcubic"] {
            let (first, second) = forwards(code)?;
            println!("{code:<15} {first:.8} {second:.8}");
        }
    
        Ok(())
    }
    Python
    import datetime as dt
    
    import helixio as hlx
    from desk_sheets import SOFR_CLOSE_DATE
    
    Interpolation = hlx.curves.Interpolation
    PILLARS = [
        (dt.date(2027, 10, 1), 0.9580),
        (dt.date(2028, 10, 2), 0.9150),
        (dt.date(2031, 10, 1), 0.7920),
        (dt.date(2036, 10, 1), 0.6180),
    ]
    SCHEMES = [
        Interpolation.LogLinear,
        Interpolation.MonotoneConvex,
        Interpolation.NaturalCubic,
        Interpolation.tension_spline(2.0),
        Interpolation.Kruger,
    ]
    BETWEEN = dt.date(2030, 4, 1)
    
    # The same four pillars under five schemes. Each reproduces every pillar; each
    # reads its own value between them.
    curves = {s.code(): hlx.curves.DiscountCurve(SOFR_CLOSE_DATE, PILLARS, interpolation=s) for s in SCHEMES}
    for code, curve in curves.items():
        print(f"{code:<15} {curve.discount(BETWEEN):.10f}")
    on_pillars = all(
        abs(curve.discount(pillar) - factor) < 1e-15 for curve in curves.values() for pillar, factor in PILLARS
    )
    print(f"every scheme reproduces every pillar: {'yes' if on_pillars else 'no'}")
    # Log-linear reads the log factor on a straight line in time between pillars.
    (t1, f1), (t2, f2) = PILLARS[1], PILLARS[2]
    w = (BETWEEN - t1).days / (t2 - t1).days
    print(f"loglinear by its formula {f1 ** (1.0 - w) * f2**w:.10f}")
    
    # Within the 2028-2031 segment, log-linear's forward is one number; the
    # natural cubic's moves.
    early = (dt.date(2029, 1, 2), dt.date(2029, 7, 2))
    late = (dt.date(2030, 7, 1), dt.date(2031, 1, 2))
    print("forward         2029 H1    2030 H2")
    for code in ("loglinear", "naturalcubic"):
        curve = curves[code]
        first, second = curve.forward_rate(*early), curve.forward_rate(*late)
        print(f"{code:<15} {first:.8f} {second:.8f}")
    loglinear       0.8513937284
    monotoneconvex  0.8520436306
    naturalcubic    0.8521424884
    tensionspline   0.8517848283
    kruger          0.8520215867
    every scheme reproduces every pillar: yes
    loglinear by its formula 0.8513937284
    forward         2029 H1    2030 H2
    loglinear       0.04816488 0.04816488
    naturalcubic    0.04733858 0.04863567

    The scheme read from its knots

    Underneath, each scheme is a function of the knots. This rebuilds a curve's value between pillars from read_log_df on the stored knots, and prints it beside the curve's own value.

    Rust
    use chrono::NaiveDate;
    use helixio_docs_sheets::SOFR_CLOSE_DATE;
    use helixrs::{Curve, Interpolation, PiecewiseDiscount, 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 pillars: [(NaiveDate, f64); 4] = [
            (date((2027, 10, 1)), 0.9580),
            (date((2028, 10, 2)), 0.9150),
            (date((2031, 10, 1)), 0.7920),
            (date((2036, 10, 1)), 0.6180),
        ];
        let between = date((2030, 4, 1));
        let years = |d: NaiveDate| (d - reference).num_days() as f64 / 365.0;
    
        let scheme = Interpolation::NaturalCubic;
        let taus: Vec<f64> = pillars.iter().map(|(d, _)| years(*d)).collect();
        let logs: Vec<f64> = pillars.iter().map(|(_, f)| f.ln()).collect();
        let by_hand = scheme.read_log_df(&taus, &logs, years(between))?.exp();
        let curve = Curve::Piecewise(
            PiecewiseDiscount::from_discount_factors(reference, &pillars)?.with_interpolation(scheme)?,
        );
        let read = curve.view::<f64>().discount(between)?;
        println!("by hand  {by_hand:.10}");
        println!("curve    {read:.10}");
    
        Ok(())
    }
    Python
    import datetime as dt
    import math
    
    import helixio as hlx
    from desk_sheets import SOFR_CLOSE_DATE
    
    PILLARS = [
        (dt.date(2027, 10, 1), 0.9580),
        (dt.date(2028, 10, 2), 0.9150),
        (dt.date(2031, 10, 1), 0.7920),
        (dt.date(2036, 10, 1), 0.6180),
    ]
    BETWEEN = dt.date(2030, 4, 1)
    
    def years(d: dt.date) -> float:
        return (d - SOFR_CLOSE_DATE).days / 365.0
    
    scheme = hlx.curves.Interpolation.NaturalCubic
    taus = [years(d) for d, _ in PILLARS]
    logs = [math.log(f) for _, f in PILLARS]
    by_hand = math.exp(scheme.read_log_df(taus, logs, years(BETWEEN)))
    curve = hlx.curves.DiscountCurve(SOFR_CLOSE_DATE, PILLARS, interpolation=scheme)
    print(f"by hand  {by_hand:.10f}")
    print(f"curve    {curve.discount(BETWEEN):.10f}")
    by hand  0.8521424884
    curve    0.8521424884

    From formula to API

    Symbol Meaning Rust Python
    D(t)D(t) a curve under a scheme PiecewiseDiscount::with_interpolation curves.DiscountCurve(…, interpolation=…)
    y(τ)y(\tau) the scheme's reading from knots Interpolation::read_log_df curves.Interpolation.read_log_df
    ww, τi\tau_i the knot weight and year fractions computed inside read_log_df computed inside read_log_df

    See also

    • Source: crates/helixrs/src/curves/interp.rs, crates/helixrs/src/curves/discount.rs
    • Related pages:
      • Discount curves, for the pillars a scheme reads between.
      • Step curves, for flat forwards placed on decision dates.
      • Bootstrapping, for a scheme chosen at calibration.

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