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
- Make one rung per quote with the helpers in
calibration.helpers:deposit,meeting,swap(by tenor, or over an explicit window),turnandfuture. - 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. - 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.
Related
- 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.