Yield curves
Module: Shoals.Curves.
This module represents a yield curve as a set of pillar times and rates tagged with a curve kind, interpolates the rate at an arbitrary maturity by several methods, derives discount factors, bootstraps a zero curve from par yields in the single-curve case, and applies sensitivity shifts.
Types and curve kinds
Section titled “Types and curve kinds”type CurveKind = | Ois | Ibor | Sofr | Sonia | Estr | Custom { label: string }
type YieldCurve[n] = | YieldCurve { kind: CurveKind, times: tensor[n, f32], rates: tensor[n, f32] }A YieldCurve[n] carries its kind, a tensor of pillar times, and a tensor
of pillar rates, both of length n. The kind is one of the standard
overnight or term-rate families, or a Custom kind with a free-text label.
Constructors for each kind:
def ois() -> CurveKinddef ibor() -> CurveKinddef sofr() -> CurveKinddef sonia() -> CurveKinddef estr() -> CurveKinddef custom_curve(label: string) -> CurveKindBuilding a curve
Section titled “Building a curve”def yield_curve_from_pillars[n](times: tensor[n, f32], rates: tensor[n, f32]) -> YieldCurve[n]def yield_curve_tagged[n](kind: CurveKind, times: tensor[n, f32], rates: tensor[n, f32]) -> YieldCurve[n]def curve_kind[n](curve: YieldCurve[n]) -> CurveKindyield_curve_from_pillars builds an untagged curve (a Custom kind with
the label "untagged"). yield_curve_tagged builds a curve with an
explicit kind. curve_kind reads the kind back. From tests/curves.ch and
tests/curves_ops.ch:
curve = yield_curve_from_pillars( to_tensor([cast(1.0, f32), cast(2.0, f32), cast(3.0, f32)]), to_tensor([cast(0.03, f32), cast(0.04, f32), cast(0.045, f32)]))tagged = yield_curve_tagged(sofr(), to_tensor([cast(1.0, f32)]), to_tensor([cast(0.04, f32)]))Interpolation and discount factors
Section titled “Interpolation and discount factors”def rate_at[n](curve: YieldCurve[n], t: f32) -> f32def spline_rate_at[n](curve: YieldCurve[n], t: f32) -> f32def log_linear_rate_at[n](curve: YieldCurve[n], t: f32) -> f32def nss_rate(beta0: f32, beta1: f32, beta2: f32, beta3: f32, tau1: f32, tau2: f32, t: f32) -> f32def discount_factor[n](curve: YieldCurve[n], t: f32) -> f32rate_at interpolates the pillar rates linearly at maturity t (using
Nautilus.Interpolation.linear_interp_sorted). spline_rate_at uses a
cubic spline, and log_linear_rate_at interpolates in log-rate space and
exponentiates. nss_rate evaluates the Nelson-Siegel-Svensson functional
form directly from its six parameters, independent of any pillar set.
discount_factor returns exp(-rate_at(curve, t) * t).
From tests/curves.ch, linear interpolation at the midpoint and the
discount factor at a pillar:
r = rate_at(curve, cast(1.5, f32)) // r == 0.035d = discount_factor(curve, cast(2.0, f32)) // d == exp(-0.08)From tests/curves_ops.ch, the NSS rate tends to beta0 + beta1 as the
maturity goes to zero and to beta0 at long horizons:
r = nss_rate(cast(0.04, f32), cast(-0.02, f32), cast(0.01, f32), cast(0.0, f32), cast(1.0, f32), cast(2.0, f32), cast(0.0, f32))// r == 0.02 (beta0 + beta1)Bootstrapping
Section titled “Bootstrapping”def bootstrap_zero_from_par[n](times: tensor[n, f32], par_yields: tensor[n, f32]) -> YieldCurve[n]bootstrap_zero_from_par builds a zero curve from a set of par yields in
the single-curve case, with one coupon per pillar at integer-year spacing.
The resulting curve reprices the par bonds to par. From tests/curves.ch:
times = to_tensor([cast(1.0, f32), cast(2.0, f32)])pars = to_tensor([cast(0.05, f32), cast(0.06, f32)])curve = bootstrap_zero_from_par(times, pars)// the two-year par bond reprices to 1.0The multi-instrument bootstrap (deposits, FRAs, futures, and swaps in one joint solve) is not part of this surface; see Scope and limitations.
Sensitivity shifts
Section titled “Sensitivity shifts”def parallel_shift[n](curve: YieldCurve[n], delta: f32) -> YieldCurve[n]def key_rate_shift[n](curve: YieldCurve[n], pillar_index: int64, delta: f32) -> YieldCurve[n]def twist[n](curve: YieldCurve[n], short_delta: f32, long_delta: f32) -> YieldCurve[n]def butterfly[n](curve: YieldCurve[n], wing_delta: f32, body_delta: f32) -> YieldCurve[n]def scale_rates[n](curve: YieldCurve[n], factor: f32) -> YieldCurve[n]parallel_shift adds the same delta to every pillar rate.
key_rate_shift adds delta only to the pillar at pillar_index,
leaving the others fixed. twist interpolates a shift linearly in maturity
from short_delta at the shortest pillar to long_delta at the longest;
at the midpoint the applied shift is the average of the two. butterfly
applies wing_delta at the ends and body_delta in the middle, scaled by
distance from the midpoint. scale_rates multiplies every rate by factor.
From tests/curves_ops.ch, a parallel shift lifts every pillar by the same
amount and a key-rate shift moves only the chosen pillar:
shifted = parallel_shift(curve, cast(0.001, f32)) // every rate +10bpkr = key_rate_shift(curve, cast(1, int64), cast(0.005, f32)) // only the 2y pillar +50bp