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.
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(())
}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 rungs, the solve finds the discount factors at which every rung is worth nothing:
Between two pillars the curve interpolates linearly in time, so the continuously compounded forward is flat between them:
with the ACT/365 year fraction. With pillars at a turn's start and end , the forward over 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 |
|---|---|---|---|
| , deposit | a deposit rung | deposit_rung |
calibration.helpers.deposit |
| , meeting | a meeting rung | meeting_rung with MeetingLocator::Meeting |
calibration.helpers.meeting |
| , window | a swap over a window | window_rung |
calibration.helpers.swap with a window locator |
| , turn | a turn rung | turn_rung |
calibration.helpers.turn |
| , future | a futures rung at a price | future_rung with FutureContract::parse |
calibration.helpers.future |
| , swap | a par swap rung | par_rungs |
calibration.helpers.swap with a tenor |
| the solved curve, pinned under its roles | MarketContext::with_curve_helpers |
MarketContext.with_curve_helpers |
|
| 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.