Pin a curve node past the last quote
You'll hold a curve at a level you choose beyond its last swap, say a 40-year zero rate, and check that the swaps it was solved from still price to nothing.
Before you start
- helixio installed: see installing.
- Your swap quotes, and the value you want the curve to read at the far date: a zero rate or a discount factor.
Steps
- Make one par rung per swap quote with
calibration.par_rungs. - Make one more rung on the curve's own reading at the far date:
Rung.priced(date, products.curve_pin(currency, date, "zero_rate"), value, 1.0). Use"discount_factor"to pin a factor instead. - Solve the swaps and the pin together with
calibration.curve, and file the curve in a market. - Read the curve at the far date, and price the swaps back.
The comments in the program match these steps.
Rust
use chrono::NaiveDate;
use helixrs::{
AnyInstrument, CollateralSpec, CurvePin, CurveReading, FixingStore, FxRates, Interpolation,
MarketContext, Period, Rung, Tolerance, YieldCurve, calibrate, par_rungs,
};
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((2026, 10, 1));
let far = date((2066, 10, 1));
let usd = CollateralSpec::Currency("USD".parse()?);
// Step 1: one par rung per swap quote, here five SOFR swaps out to thirty years.
let tenors = ["1Y", "2Y", "5Y", "10Y", "30Y"].iter().map(|t| t.parse::<Period>()).collect::<Result<Vec<_>, _>>()?;
let rates = [0.0441, 0.0452, 0.0468, 0.0481, 0.0476];
let rungs = par_rungs("USD-SOFR-OIS", close, &tenors, &rates, 1.0)?;
// Step 2: a priced rung on the curve's own 40-year zero rate, worth the 4.60%
// you want the curve to read there.
let pin = AnyInstrument::CurvePin(CurvePin::new("USD".parse()?, far, CurveReading::ZeroRate));
let mut with_pin = rungs.clone();
with_pin.push(Rung::priced(far, pin, 0.0460, 1.0));
// Step 3: solve the swaps and the pin together, and file the curve.
let (fixings, fx) = (FixingStore::new(), FxRates::default());
let solved = calibrate(close, &with_pin, &fixings, &fx, Tolerance::default(), Interpolation::LogLinear)?;
let parameters = solved.curve_value().parameter_count();
let market = MarketContext::as_of(close)
.with_discount(usd, solved.clone())
.with_projection("USD-SOFR", solved);
// Step 4: read the curve at the last swap and at the pin, and price the swaps back.
let (curve, _) = market.curve(&"discount(USD)".parse()?).expect("the USD curve");
let view = curve.view::<f64>();
println!("pillars {parameters}");
let last = rungs[rungs.len() - 1].pillar;
println!("zero rate at {last} {:.6}", view.zero_rate(last)?);
println!("zero rate at {far} {:.6}", view.zero_rate(far)?);
let mut worst: f64 = 0.0;
for rung in &rungs {
worst = worst.max(rung.instrument.npv(&market)?.abs());
}
println!("largest swap value {worst:.2}");
Ok(())
}Python
import datetime as dt
import helixio as hlx
CLOSE = dt.date(2026, 10, 1)
FAR = dt.date(2066, 10, 1)
calibration = hlx.calibration
# Step 1: one par rung per swap quote, here five SOFR swaps out to thirty years.
tenors = ["1Y", "2Y", "5Y", "10Y", "30Y"]
rates = [0.0441, 0.0452, 0.0468, 0.0481, 0.0476]
rungs = calibration.par_rungs("USD-SOFR-OIS", CLOSE, list(zip(tenors, rates)))
# Step 2: a priced rung on the curve's own 40-year zero rate, worth the 4.60%
# you want the curve to read there.
pin = calibration.Rung.priced(FAR, hlx.products.curve_pin("USD", FAR, "zero_rate"), 0.0460, 1.0)
# Step 3: solve the swaps and the pin together, and file the curve.
solved = calibration.curve(CLOSE, list(rungs) + [pin])
market = hlx.MarketContext(CLOSE).with_discount("USD", solved.curve()).with_projection("USD-SOFR", solved.curve())
# Step 4: read the curve at the last swap and at the pin, and price the swaps back.
curve = market.curve("discount(USD)")
print(f"pillars {solved.curve().parameter_count()}")
print(f"zero rate at {rungs[-1].pillar()} {curve.zero_rate(rungs[-1].pillar()):.6f}")
print(f"zero rate at {FAR} {curve.zero_rate(FAR):.6f}")
worst = max(abs(rung.instrument().npv(market)) for rung in rungs)
print(f"largest swap value {worst:.2f}")pillars 6
zero rate at 2056-10-10 0.047044
zero rate at 2066-10-01 0.046000
largest swap value 0.00
Check it worked
- The curve reads exactly the pinned value at the far date.
- Every swap is still worth nothing: the pin sits past the last pillar, so it moves nothing before it.
- If the solve refuses the pin, its date isn't after the pillar of the rung listed before it: pillars must rise in the order you pass the rungs. List the pin in date order and it can sit between quotes as well as past them.
Related
- Bootstrapping, the reference for the solve, its rungs and its tolerance.
- Reading a curve, for zero rates and discount factors.
- Build a curve from your own rungs, for rungs no recipe lists.
From the 0.0 line at revision bf7f2e2. Every printed number is that revision's own output.