Discount curves
This page lists what you need to build a discount curve yourself: from discount factors at dated pillars, or from a single rate. It covers the constructor's arguments, how the curve reads between and past its pillars, and what happens at the edges.
Quick example
This builds a curve from four discount factors, reads it between pillars and past the last one, builds a flat curve from one rate, and prices a swap on the first curve. The example reads its quotes from the desk sheets; load them once and it runs as shown.
use chrono::NaiveDate;
use helixio_docs_sheets::SOFR_CLOSE_DATE;
use helixrs::{
CollateralSpec, Curve, DayCountFraction, MarketContext, PiecewiseDiscount, Termination, YieldCurve,
template,
};
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 reference = date(SOFR_CLOSE_DATE);
// Four dated discount factors. At a pillar the curve returns its factor
// exactly; between pillars it interpolates.
let pillars = [
(date((2027, 10, 1)), 0.9580),
(date((2028, 10, 2)), 0.9150),
(date((2031, 10, 1)), 0.7920),
(date((2036, 10, 1)), 0.6180),
];
let piecewise = PiecewiseDiscount::from_discount_factors(reference, &pillars)?;
let scheme = piecewise.interpolation().code();
let curve = Curve::Piecewise(piecewise);
let view = curve.view::<f64>();
println!("parameters {}", curve.parameter_count());
println!("interpolation {scheme}");
println!("at a pillar {:.10}", view.discount(pillars[2].0)?);
println!("between {:.10}", view.discount(date((2030, 4, 1)))?);
// Past the last pillar the curve holds its last segment's forward, so the
// factor decays from the last pillar at that one rate.
let (last, previous) = (pillars[3].0, pillars[2].0);
let forward = view.forward_rate(previous, last)?;
let beyond = date((2041, 10, 1));
let expected = pillars[3].1 * (-forward * (beyond - last).num_days() as f64 / 365.0).exp();
println!("last forward {forward:.10}");
println!("factor 2041 {:.10}", view.discount(beyond)?);
println!("by the forward {expected:.10}");
// A flat curve is one continuously compounded zero rate on ACT/365: the factor
// to a date is exp(-r t).
let flat = Curve::flat(reference, 0.04, DayCountFraction::Act365Fixed)?;
let five = date((2031, 10, 1));
let flat_view = flat.view::<f64>();
println!("flat factor 5Y {:.10}", flat_view.discount(five)?);
// One curve that discounts dollars and projects SOFR prices a SOFR swap. A
// swap struck at the curve's par rate is worth nothing on it.
let market = MarketContext::as_of(reference)
.with_discount(CollateralSpec::Currency("USD".parse()?), curve.clone())
.with_projection("USD-SOFR", curve.clone());
let draft = template("USD-SOFR-OIS")?
.swap(reference)?
.with_maturity("5Y".parse::<Termination>()?)?
.with_notional(10_000_000.0);
let fair = draft.at_par(&market)?;
let par = fair.par_rate(&market)?;
println!("5Y par rate {par:.8}");
println!("npv at par {:.2}", fair.npv(&market)?.abs());
Ok(())
}import datetime as dt
import math
import helixio as hlx
from desk_sheets import SOFR_CLOSE_DATE
REFERENCE = SOFR_CLOSE_DATE
# Four dated discount factors. At a pillar the curve returns its factor
# exactly; between pillars it interpolates.
PILLARS = [
(dt.date(2027, 10, 1), 0.9580),
(dt.date(2028, 10, 2), 0.9150),
(dt.date(2031, 10, 1), 0.7920),
(dt.date(2036, 10, 1), 0.6180),
]
curve = hlx.curves.DiscountCurve(REFERENCE, PILLARS)
print(f"parameters {curve.parameter_count()}")
print(f"interpolation {curve.interpolation()}")
print(f"at a pillar {curve.discount(PILLARS[2][0]):.10f}")
print(f"between {curve.discount(dt.date(2030, 4, 1)):.10f}")
# Past the last pillar the curve holds its last segment's forward, so the
# factor decays from the last pillar at that one rate.
last, previous = PILLARS[-1][0], PILLARS[-2][0]
forward = curve.forward_rate(previous, last)
beyond = dt.date(2041, 10, 1)
expected = PILLARS[-1][1] * math.exp(-forward * (beyond - last).days / 365.0)
print(f"last forward {forward:.10f}")
print(f"factor 2041 {curve.discount(beyond):.10f}")
print(f"by the forward {expected:.10f}")
# A flat curve is one continuously compounded zero rate on ACT/365: the factor
# to a date is exp(-r t).
flat = hlx.curves.DiscountCurve.flat(REFERENCE, 0.04)
five = dt.date(2031, 10, 1)
print(f"flat factor 5Y {flat.discount(five):.10f}")
# One curve that discounts dollars and projects SOFR prices a SOFR swap. A
# swap struck at the curve's par rate is worth nothing on it.
market = (
hlx.MarketContext(REFERENCE)
.with_discount("USD", curve)
.with_projection("USD-SOFR", curve)
)
draft = hlx.templates.get("USD-SOFR-OIS").swap(REFERENCE).with_maturity("5Y").with_notional(10_000_000.0)
fair = draft.at_par(market)
par = fair.par_rate(market)
print(f"5Y par rate {par:.8f}")
print(f"npv at par {abs(fair.npv(market)):.2f}")parameters 4
interpolation loglinear
at a pillar 0.7920000000
between 0.8513937284
last forward 0.0495602742
factor 2041 0.4822927548
by the forward 0.4822927548
flat factor 5Y 0.8186410341
5Y par rate 0.04695436
npv at par 0.00
At each pillar the curve returns its factor exactly. Past the last pillar it carries on at its last segment's forward rate.
How it's priced
A curve holds discount factors at its pillars and at its reference date . Between pillars it reads under its interpolation; log-linear is the default. Past it holds the forward rate of its last segment:
with on ACT/365. A flat curve is one continuously compounded zero rate on a day count:
Why curves pass between systems as discount factors is covered in how curves work.
API
| Symbol | Meaning | Rust | Python |
|---|---|---|---|
| factors at pillars | PiecewiseDiscount::from_discount_factors, wrapped in Curve::Piecewise |
curves.DiscountCurve(reference, pillars) |
|
| the factor to a date | YieldCurve::discount on Curve::view |
curves.DiscountCurve.discount |
|
| the last segment's forward | YieldCurve::forward_rate |
curves.DiscountCurve.forward_rate |
|
| a flat curve | Curve::flat |
curves.DiscountCurve.flat |
|
| — | a curve in a market's roles | MarketContext::with_discount, MarketContext::with_projection |
MarketContext.with_discount, MarketContext.with_projection |
curves.DiscountCurve(...):
| Argument | Type | Default | Meaning |
|---|---|---|---|
reference |
date |
required | the curve's reference date, where the factor is 1 |
pillars |
list[tuple[date, float]] |
required | (date, discount factor) pairs; a first pillar of exactly 1.0 on the reference date is dropped, any other factor there is refused |
interpolation |
Interpolation or str |
"loglinear" |
the scheme between pillars; in Rust, .with_interpolation(...) |
tension |
float or None |
None |
the tension for a tension spline |
end_slopes |
tuple[float, float] or None |
None |
the end slopes for a clamped cubic |
long_interpolation |
Interpolation, str or None |
None |
a second scheme the curve switches to past changeover |
changeover |
date or None |
None |
the pillar where long_interpolation takes over |
rate_day_count |
str or DayCountFraction |
"ACT/365.FIXED" |
the day count zero_rate and forward_rate are formed on; it moves no factor |
The schemes interpolation= accepts are the members of curves.Interpolation; in Rust, the
variants of Interpolation. Interpolation explains how they differ.
| Scheme | Parameters |
|---|---|
LogLinear (the default), BackwardFlat, ForwardFlat, LinearZero |
none |
NaturalCubic, NotAKnotCubic, MonotoneCubic, MonotoneConvex, Akima, Kruger |
none |
MonotonicNaturalCubic (a natural spline with its slopes filtered to stay monotone), KrugerZero (Kruger on the zero rate) |
none |
anchored_linear_zero(anchor) |
the zero rate at the reference date, where LinearZero's first segment starts |
tension_spline(sigma) |
the tension sigma |
clamped_cubic(left, right) |
the two end slopes |
curves.DiscountCurve.flat(...):
| Argument | Type | Default | Meaning |
|---|---|---|---|
reference |
date |
required | the reference date |
rate |
float |
required | the continuously compounded zero rate |
day_count |
str or DayCountFraction |
"ACT/365.FIXED" |
the day count the rate accrues on |
Defaults and limits
- Extrapolation: past the last pillar the curve extrapolates at the last segment's forward,
silently rather than refusing. A date before the reference date raises
ValueError. - Interpolation: log-linear unless you choose another; see interpolation.
- A flat curve has no pillars and no free parameters, so it reports no risk of its own.
- Marked from quotes: to solve a curve from swap and futures quotes instead of factors, see quote sheets and recipes.
See also
- Source:
crates/helixrs/src/curves/discount.rs,crates/helixrs/src/curves/flat.rs - Related pages:
- Reading a curve, for the rates a curve answers.
- Interpolation, for the schemes between pillars.
- How curves work, for why the factors are the curve.
From the 0.0 line at revision bf7f2e2. Every printed number is that revision's own output.