Esc

    The API is under testing and may change before release.

    Build a curve from your own rungs

    You'll build a curve from instruments you pick yourself, such as a year-end turn that no shipped recipe lists, and check that it reprices every one of them.

    Before you start

    • helixio installed: see installing.
    • One quote per instrument you want the curve to fit, and the template that dates each one.

    Steps

    1. Make one rung per quote with the helpers in calibration.helpers: deposit, meeting, swap (by tenor, or over an explicit window), turn and future.
    2. Solve them with MarketContext.with_curve_helpers(rungs). Rungs are grouped by the curve they solve; here all of them are SOFR, so they build one curve.
    3. Read the curve, for example its forwards across the turn.

    The comments in the program match these steps.

    Rust
    use chrono::NaiveDate;
    use helixrs::{
        FutureContract, MarketContext, MeetingLocator, Period, YieldCurve, deposit_rung, future_rung,
        meeting_rung, par_rungs, template, turn_rung, window_rung,
    };
    
    type Outcome<T> = Result<T, Box<dyn std::error::Error>>;
    
    const SOFR_CLOSE_DATE: (i32, u32, u32) = (2026, 10, 1);
    
    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 close = date(SOFR_CLOSE_DATE);
        let (depo, meeting, ois, future) = (
            template("USD-SOFR-DEPO")?,
            template("USD-SOFR-MeetingOIS")?,
            template("USD-SOFR-OIS")?,
            template("USD-SOFR-3M-FUT")?,
        );
        let swaps = |tenor: &str, rate: f64| -> Outcome<_> {
            Ok(par_rungs(("USD-SOFR-OIS", &ois), close, &[tenor.parse::<Period>()?], &[rate], 1.0)?
                .remove(0))
        };
    
        // One rung per quote, each on the template that dates its instrument: a
        // one-week deposit, the October meeting, a swap over the December window that
        // ends where the turn starts, the turn itself, two futures, and three swaps.
        let rungs = vec![
            deposit_rung("USD-SOFR-DEPO", &depo, close, "1W".parse()?, 0.0389, 1.0)?,
            meeting_rung(
                "USD-SOFR-MeetingOIS",
                &meeting,
                close,
                MeetingLocator::Meeting(date((2026, 10, 28))),
                0.0388,
                1.0,
                None,
            )?,
            window_rung("USD-SOFR-OIS", &ois, date((2026, 12, 10)), date((2026, 12, 31)), 0.0380, 1.0)?,
            turn_rung("USD-SOFR-MeetingOIS", &meeting, date((2026, 12, 31)), date((2027, 1, 4)), 0.0475, 1.0)?,
            future_rung("USD-SOFR-3M-FUT", &future, FutureContract::parse("MAR27")?, 95.65, 0.0, 1.0)?,
            future_rung("USD-SOFR-3M-FUT", &future, FutureContract::parse("JUN27")?, 95.55, 0.0, 1.0)?,
            swaps("2Y", 0.0459)?,
            swaps("5Y", 0.0471)?,
            swaps("10Y", 0.0483)?,
        ];
        for rung in &rungs {
            println!("{}  {}", rung.pillar, rung.instrument.product_name());
        }
    
        // Solve. Every rung is on SOFR, so they build one curve that discounts USD and
        // projects SOFR.
        let market = MarketContext::as_of(close).with_curve_helpers(&rungs)?;
        let (curve, _) = market.curve(&"discount(USD)".parse()?).expect("the USD curve");
        for (role, key) in market.contents() {
            println!("{role:<11} {key}");
        }
        println!("parameters  {}", curve.parameter_count());
        // Every rung prices back to nothing on the curve solved from it.
        let mut largest: f64 = 0.0;
        for rung in &rungs {
            largest = largest.max(rung.instrument.npv(&market)?.abs());
        }
        println!("largest rung value  {largest:.2}");
    
        // Read the curve across the turn: the forward steps up into the year end and
        // back down after it. A forward reads continuously compounded on ACT/365, and
        // the turn is quoted simple on ACT/360, so its 0.0475 reads as 0.048147.
        let view = curve.view::<f64>();
        let before = view.forward_rate(date((2026, 12, 24)), date((2026, 12, 31)))?;
        let across = view.forward_rate(date((2026, 12, 31)), date((2027, 1, 4)))?;
        let after = view.forward_rate(date((2027, 1, 4)), date((2027, 1, 11)))?;
        println!("forward 24 Dec - 31 Dec  {before:.6}");
        println!("forward 31 Dec -  4 Jan  {across:.6}");
        println!("forward  4 Jan - 11 Jan  {after:.6}");
    
        Ok(())
    }
    Python
    import datetime as dt
    
    import helixio as hlx
    
    SOFR_CLOSE_DATE = dt.date(2026, 10, 1)
    helpers = hlx.calibration.helpers
    
    # One rung per quote, each on the template that dates its instrument: a
    # one-week deposit, the October meeting, a swap over the December window that
    # ends where the turn starts, the turn itself, two futures, and three swaps.
    rungs = [
        helpers.deposit("USD-SOFR-DEPO", SOFR_CLOSE_DATE, "1W", 0.0389),
        helpers.meeting("USD-SOFR-MeetingOIS", SOFR_CLOSE_DATE, dt.date(2026, 10, 28), 0.0388),
        helpers.swap("USD-SOFR-OIS", SOFR_CLOSE_DATE, "2026-12-10 - 2026-12-31", 0.0380),
        helpers.turn("USD-SOFR-MeetingOIS", dt.date(2026, 12, 31), dt.date(2027, 1, 4), 0.0475),
        helpers.future("USD-SOFR-3M-FUT", "MAR27", 95.65),
        helpers.future("USD-SOFR-3M-FUT", "JUN27", 95.55),
        helpers.swap("USD-SOFR-OIS", SOFR_CLOSE_DATE, "2Y", 0.0459),
        helpers.swap("USD-SOFR-OIS", SOFR_CLOSE_DATE, "5Y", 0.0471),
        helpers.swap("USD-SOFR-OIS", SOFR_CLOSE_DATE, "10Y", 0.0483),
    ]
    for rung in rungs:
        print(f"{rung.pillar()}  {rung.instrument().product()}")
    
    # Solve. Every rung is on SOFR, so they build one curve that discounts USD and
    # projects SOFR.
    market = hlx.MarketContext(SOFR_CLOSE_DATE).with_curve_helpers(rungs)
    curve = market.curve("discount(USD)")
    for role, key in market.contents():
        print(f"{role:<11} {key}")
    print(f"parameters  {curve.parameter_count()}")
    # Every rung prices back to nothing on the curve solved from it.
    print(f"largest rung value  {max(abs(rung.instrument().npv(market)) for rung in rungs):.2f}")
    
    # Read the curve across the turn: the forward steps up into the year end and
    # back down after it. A forward reads continuously compounded on ACT/365, and
    # the turn is quoted simple on ACT/360, so its 0.0475 reads as 0.048147.
    before = curve.forward_rate(dt.date(2026, 12, 24), dt.date(2026, 12, 31))
    across = curve.forward_rate(dt.date(2026, 12, 31), dt.date(2027, 1, 4))
    after = curve.forward_rate(dt.date(2027, 1, 4), dt.date(2027, 1, 11))
    print(f"forward 24 Dec - 31 Dec  {before:.6f}")
    print(f"forward 31 Dec -  4 Jan  {across:.6f}")
    print(f"forward  4 Jan - 11 Jan  {after:.6f}")
    2026-10-08  MoneyMarketDeposit
    2026-12-10  OvernightIndexedSwap
    2026-12-31  OvernightIndexedSwap
    2027-01-04  OvernightIndexedSwap
    2027-06-16  RateFuture
    2027-09-15  RateFuture
    2028-10-10  OvernightIndexedSwap
    2031-10-08  OvernightIndexedSwap
    2036-10-08  OvernightIndexedSwap
    discount    USD
    projection  USD-SOFR
    parameters  9
    largest rung value  0.00
    forward 24 Dec - 31 Dec  0.038485
    forward 31 Dec -  4 Jan  0.048147
    forward  4 Jan - 11 Jan  0.043863

    Check it worked

    • The curve has one pillar per rung.
    • Every rung's instrument is worth nothing on the solved curve.
    • A turn quoted above its neighbours shows as a forward that steps up into the year end and back down after it.
    • Curve helpers, the reference for every rung kind.
    • Reading a curve, for the forwards and discount factors.

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