Esc

    The API is under testing and may change before release.

    Curve helpers

    This page lists the curve helpers, or rungs, you can build yourself: each quoted instrument a curve is solved to reprice, what it is quoted as, where it puts its pillar, and the arguments each takes. Build rungs yourself when a curve needs an instrument the shipped recipes don't list, such as a year-end turn.

    Quick example

    The example reads its quotes from the desk sheets; load them once and it runs as shown. It builds nine rungs covering every kind (a deposit, a meeting, a window swap, a year-end turn, futures and par swaps), prints each rung's pillar, and solves them into one curve.

    Rust
    use chrono::NaiveDate;
    use helixio_docs_sheets::SOFR_CLOSE_DATE;
    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>>;
    
    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 says how the instrument is
        // dated: a one-week deposit, the October policy meeting, a swap over the
        // December window that ends where the turn starts, the turn itself, two futures
        // quoted as prices, 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());
        }
    
        // The helpers are grouped by the curve each one solves. Every rung here is on
        // SOFR, so they solve 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());
        // A swap or deposit is worth nothing per unit notional; a future's value is in
        // price points, so its round-off is a hundred times larger in the same units.
        let mut worst: f64 = 0.0;
        for rung in &rungs {
            worst = worst.max(rung.instrument.npv(&market)?.abs());
        }
    
        Ok(())
    }
    Python
    import datetime as dt
    
    import helixio as hlx
    from desk_sheets import SOFR_CLOSE_DATE
    
    helpers = hlx.calibration.helpers
    
    # One rung per quote, each on the template that says how the instrument is
    # dated: a one-week deposit, the October policy meeting, a swap over the
    # December window that ends where the turn starts, the turn itself, two futures
    # quoted as prices, 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()}")
    
    # The helpers are grouped by the curve each one solves. Every rung here is on
    # SOFR, so they solve 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()}")
    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

    Each rung adds one pillar, so nine rungs solve a curve of nine parameters. Reading the forward across the turn, and checking every rung prices back to nothing, is the how-to Build a curve from your own rungs.

    How it's priced

    Within a group of nn rungs, the solve finds the discount factors D(T1),…,D(Tn)D(T_1), \dots, D(T_n) at which every rung is worth nothing:

    Pj ⁣(qj; D)=0,j=1,…,n.P_j\!\left(q_j;\ D\right) = 0, \qquad j = 1, \dots, n.

    Between two pillars the curve interpolates ln⁡D\ln D linearly in time, so the continuously compounded forward is flat between them:

    f(t1,t2)=ln⁡D(t1)−ln⁡D(t2)τ(t1,t2),f(t_1, t_2) = \frac{\ln D(t_1) - \ln D(t_2)}{\tau(t_1, t_2)},

    with τ\tau the ACT/365 year fraction. With pillars at a turn's start tst_s and end tet_e, the forward over [ts,te][t_s, t_e] is set by the turn rung alone. Why desks quote a turn, and why it needs its own two pillars, is covered in how a market is built.

    API

    Symbol Meaning Rust Python
    PjP_j, deposit a deposit rung deposit_rung calibration.helpers.deposit
    PjP_j, meeting a meeting rung meeting_rung with MeetingLocator::Meeting calibration.helpers.meeting
    PjP_j, window a swap over a window window_rung calibration.helpers.swap with a window locator
    PjP_j, turn a turn rung turn_rung calibration.helpers.turn
    PjP_j, future a futures rung at a price future_rung with FutureContract::parse calibration.helpers.future
    PjP_j, swap a par swap rung par_rungs calibration.helpers.swap with a tenor
    DD the solved curve, pinned under its roles MarketContext::with_curve_helpers MarketContext.with_curve_helpers
    f(t1,t2)f(t_1, t_2) the forward between two dates YieldCurve::forward_rate on the curve's view curves.DiscountCurve.forward_rate

    The rungs in calibration.helpers:

    Argument Type Default Meaning
    deposit(template, valuation, tenor, rate) str, date, str, float required a deposit lent on valuation, maturing tenor later; noded at its maturity
    meeting(template, valuation, locator, rate) str, date, date | str, float required the average rate between one policy decision and the next; noded at the next decision's window end
    meeting(..., schedule=) MeetingSchedule | None None the meeting schedule to date it on; None uses the template's
    swap(template, valuation, locator, rate) str, date, str, float required a par swap to a tenor ("5Y"), or over an explicit window ("2026-12-10 - 2026-12-31")
    turn(template, start, end, segment_rate) str, date, date, float required the rate over a short window spanning a year end; noded at the window's end
    future(template, contract, price) str, str, float required a listed future quoted at its price (95.90), noded at the end of its reference quarter
    future(..., convexity=, contracts=) float | None, float None, 1.0 a convexity adjustment to apply, and the number of contracts
    notional= (every rung but future) float 1.0 the notional the rung is built on

    MarketContext.with_curve_helpers(rungs, ...) takes the rungs plus the same notional, interpolation (default "loglinear"), tension, end_slopes and tolerance arguments as with_recipe_quotes.

    Defaults and limits

    • Grouping: rungs are grouped by the curve each solves. A rung on an overnight index solves the currency's discount curve and that index's projection as one curve, and a rung on a term index solves that index's projection over the discount the market holds.
    • One rung per pillar: two rungs on the same pillar raise ValueError, naming both.
    • Missing curves: a rung that needs a curve the market doesn't hold raises MissingMarketInputError, naming it.
    • Turns: a turn needs a rung ending where it starts (here a December window swap), so the turn's window carries its own rate and nothing else.

    See also

    • Source: crates/helixrs/src/products.rs, crates/helixrs/src/calibration.rs, crates/helixrs/src/statics/meetings.rs
    • Related pages:
      • Build a curve from your own rungs, the task step by step.
      • Quote sheets and recipes, for the same rungs bound from a sheet.
      • How a market is built, for turns and grouping.

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