Migration Guide
v6.x to v7.0
Fit.Bootstrap()supports onlySpline.Linear()(equivalentlySpline.PolynomialSpline(1)orSpline.BSpline(1)). Bootstrapping solves one quote at a time and requires that each new knot leave earlier curve segments unchanged. With quadratic, cubic, higher-order B-spline, PCHIP, or Akima interpolation a later knot reshapes earlier segments, so earlier coupon quotes silently stopped repricing: on uneven par quotes the residuals reached 0.40% (quadratic) and 0.11% (cubic). These strategies now throw anArgumentError. Migration: useSpline.Linear()withFit.Bootstrap(), or fit the smoother strategy to all quotes at once withFit.Loss(x -> x^2); fit a monotone convex curve withfit(Spline.MonotoneConvex(), quotes)(bootstrap previously switched to this loss fit silently). Zero-coupon quote sets were exact with every strategy, so their fitted curves change only if you switch to a loss fit.- Bootstrap validates its inputs up front: empty quote sets, non-finite or non-positive maturities, and duplicate maturities throw an
ArgumentError. After solving, every quote is repriced on the returned curve, and a residual beyond root-finder precision throws. - Full-curve
Fit.Lossspline fits start from slightly sloped rates near 5% instead of a flat 5%, which lets PCHIP and Akima fits converge. Converged fits of other strategies move by at most about 1e-9 in zero rate.
ZeroRateCurve returns the curve it builds
ZeroRateCurve is now a construction function rather than a type. It returns a Yield.MonotoneConvex for Spline.MonotoneConvex() (the default) and a Yield.Spline for every other method. Both are subtypes of the new Yield.AbstractInterpolatedZeroCurve, and so are the curves returned by spline fits and Fit.Bootstrap(). All of them share one interface:
| v6 | v7 |
|---|---|
zrc isa ZeroRateCurve, f(z::ZeroRateCurve) | zrc isa Yield.AbstractInterpolatedZeroCurve (or Yield.AbstractYieldModel when only discounting) |
zrc.rates, zrc.tenors, mc.times | knot_rates(curve), knot_tenors(curve) (read-only vectors) |
@set zrc.rates[2] = 0.031 | reconstruct(curve; rates = new_rates) (@set on rates, tenors, spline or extrapolation still works and calls reconstruct) |
ZeroRateCurve(dual_rates, zrc.tenors, zrc.spline) in a gradient | reconstruct(curve; rates = dual_rates) |
Yield.build_model(spline, tenors, rates; extrapolation) | ZeroRateCurve(rates, tenors, spline; extrapolation) (note the argument order) |
fit(Yield.MonotoneConvex(), quotes) | fit(Spline.MonotoneConvex(), quotes) |
mc.f, mc.fᵈ | Yield.instantaneous_forward(mc, t); the node forwards are internal |
Yield.Spline(fn) for a callable zero-rate function | Yield.Constant(0.0) + ((z, t) -> Continuous(fn(t))) |
Yield.build_modelis removed, and so are theYield.MonotoneConvex()placeholder and its callable form (#272).Spline.MonotoneConvex()is the only monotone convex selector: it works withZeroRateCurve, lossfits (whose default optimizer for it isLBFGS()), andFX.Forwards, and every interpolation method is built by the same code wherever the curve comes from.Yield.Splineaccepts the DataInterpolations methods only, soYield.Spline(Spline.MonotoneConvex(), …)is aMethodError, as in v6: build that curve withZeroRateCurveorYield.MonotoneConvex.- Every knot curve owns and validates its data. Inputs are copied (mutating the vectors you passed in no longer changes the curve) and promoted to one concrete floating-point type per vector (
Int→Float64;Float32+BigFloat→BigFloat;Float64+ForwardDiff.Dual→Dual; ranges and tuples are accepted). Construction throws anArgumentErrorfor a length mismatch, empty or non-finite inputs, negative, unsorted or duplicate tenors, or too few knots:Spline.PCHIP()andSpline.Akima()need 3; every other method accepts a single knot, which gives a flat curve. Direct construction,reconstruct,fit, and bootstrap raise the same errors. - Knot curves change only through
reconstruct. The knot vectors are read-only (indexed assignment,.=,sort!, and writes throughviewthrow).reconstructandAccessors.@setrevalidate and rebuild every derived cache, so two curves can no longer compare==yet price differently. Setting a derived cache (_f,_fn,_tail, …) throws. - Equality is structural for every knot curve.
==,isequal, andhashcompare the knot rates, knot tenors, interpolation method, and extrapolation policy, so independently built curves from equal inputs are equal and work asDictkeys.isequalkeeps the signed-zero distinction of the underlying rates. Curves with different methods are unequal even when they are numerically identical (Spline.Linear()andSpline.BSpline(1)). - Negative times throw a
DomainErrorfor every knot curve, as they already did forZeroRateCurve. showprints the construction call, for exampleZeroRateCurve([0.02, 0.03], [1.0, 2.0], PolynomialSpline(1); extrapolation = :flat_forward), which rebuilds an equal curve.- Spline descriptors validate their order:
Spline.PolynomialSpline(order)accepts only orders 1, 2 and 3 (previously any order above 3 silently built a cubic spline), andSpline.BSpline(d)requiresd ≥ 1. - One signature per form. The direct form is
ZeroRateCurve(rates, tenors, spline = Spline.MonotoneConvex(); extrapolation = :flat_forward), with the method positional as in v6; it has nosplinekeyword. The sampling form keeps its v6 keyword,ZeroRateCurve(curve, tenors; spline, extrapolation). - The sampling form
ZeroRateCurve(curve::AbstractYieldModel, tenors)still sorts its tenor grid, and samples throughzero(curve, t)instead of-log(discount(curve, t))/t, which is numerically stable at very small and very large tenors. The sampled grid is validated like any other knot grid. A tenor of0(previously rejected) takes the source curve's zero-rate limit there; a source curve without one gives a non-finite rate, which throws. - Refitting a knot curve is the spline fit on its knots.
fit(curve, quotes)works for any knot curve: it fits new knot rates at the curve's tenors with its interpolation method and extrapolation policy, by the same solve asfit(spline, quotes). It starts from the same rates (not the curve's own), builds one unchecked trial curve per optimizer candidate, uses the spline's default optimizer (Newton(), orLBFGS()forSpline.MonotoneConvex()), and differentiates through dual quotes when there is one knot per quote. It no longer acceptsvariables(aMethodError): that keyword never had a documented use for knot curves and was inherited from the generic optic-based fit, whose candidates went through the public constructor and made a flat PCHIP or Akima starting curve throw. AnFX.Forwardswhose foreign curve is a knot curve refits it through the implied foreign quotes, like a spline placeholder. fitvalidates the knot grid before optimising: loss fits andFit.Bootstrap()throw the constructionArgumentErrorfor duplicate or non-positive maturities, too few quotes for the interpolant, orextrapolation = :extensionwithSpline.MonotoneConvex(), before any solver runs. Optimizer trial curves are not validated (a non-finite candidate is a bad loss, not an exception). The returned curve is, so a fit that diverged to non-finite rates throws instead of returning a curve ofNaNs.- Unsuccessful optimizer fits throw
FitConvergenceErrorcarrying the solver'sretcode, instead of returning the unfitted starting model. Catch it to retry with a different seed model or optimizer, or with tighter or longer solver settings through the newsolve_kwargskeyword (passed toOptimization.solve). - PCHIP and Akima loss fits start from a different seed: a curve that is strictly increasing and concave in the maturities, away from the interpolants' formula switches. Fits that converged before reach the same curve up to the optimizer's tolerance.
Flat short end
DataInterpolations-backed curves (Spline.Linear(), Quadratic(), Cubic(), PCHIP(), Akima(), BSpline(d)) now hold the zero rate flat at the first knot's rate between t = 0 and the first knot, instead of extending the first interpolation piece back to t = 0. Values at and beyond the first knot do not change, and Spline.MonotoneConvex(), whose first interval from t = 0 is part of the Hagan-West construction, does not change.
For zero rates [0.02, 0.025, 0.03, 0.035, 0.04] at [1, 2, 5, 10, 30] with Spline.Linear(), the zero rate at 0.25 years moves from 1.625% (the 1-to-2-year line extended) to 2.0%, and at t = 0 from 1.5% to 2.0%. A loss fit is affected only through cash flows before its earliest quote maturity, such as early coupons of a longer bond when the first knot is later than them.
Fit.Bootstrap()knots are exactly the quote maturities. The returned curve used to carry an extra knot att = 0tied to the first zero rate; the flat short end now gives the same curve, so bootstrapped values do not change, butknot_tenors(curve)has one entry per quote andreconstruct(curve; rates = r)takes one rate per quote.
Long-end extrapolation
Every DataInterpolations-backed curve and fit now extrapolates with :flat_forward by default instead of continuing its final interpolation piece. This changes zero rates, discount factors and present values beyond the last knot for Yield.Spline, ZeroRateCurve and fit (loss fits and Fit.Bootstrap()) with Spline.Linear(), Quadratic(), Cubic(), PCHIP(), Akima() and BSpline(d). Values at and before the last knot do not change. Spline.MonotoneConvex(), the ZeroRateCurve default, keeps its v6.1 tail and does not change.
For example, a linear bootstrap of ZCBYield.([0.02, 0.025, 0.031, 0.036], [1, 2, 5, 10]) gives these continuously compounded zero rates:
| Maturity | v6 (final piece continued) | v7 default | Change |
|---|---|---|---|
| 10 (last knot) | 3.5367% | 3.5367% | none |
| 15 | 4.0205% | 3.6980% | −32 bp |
| 20 | 4.5043% | 3.7786% | −73 bp |
| 30 | 5.4719% | 3.8592% | −161 bp |
At 30 years the discount factor moves from 0.1937 to 0.3142, so the present value of a 30-year cashflow rises by 62%. The v7 tail holds the last discrete forward, 4.0205% (the average forward from 5 to 10 years); the v6 linear continuation kept raising the zero rate by about 10 bp a year.
To keep the previous values, pass extrapolation = :extension wherever the curve is built: Yield.Spline(spline, tenors, rates; extrapolation = :extension), ZeroRateCurve(rates, tenors, spline; extrapolation = :extension), or fit(spline, quotes, method; extrapolation = :extension).
- Knot curves take an
extrapolationpolicy for the long end.ZeroRateCurve,Yield.Spline,Yield.MonotoneConvex,reconstructand the splinefitmethods acceptextrapolation = :flat_forward(default),:flat_zero,:linear,:extension, orYield.FlatForwardAt(rate).:extensionrestores the v6 continuation of the final DataInterpolations piece and is unavailable forSpline.MonotoneConvex(). - The
:flat_forwardanchor. Beyond the last knot(tₙ, zₙ)the instantaneous forward is held at a constantfₙ, soz(t) = fₙ + (zₙ - fₙ)tₙ/tand discount factors are continuous attₙ. DataInterpolations-backed curves use the last discrete forwardfₙ = (zₙtₙ - zₙ₋₁tₙ₋₁)/(tₙ - tₙ₋₁)(for a single knot,z₁), so the forward can jump attₙ; the tail no longer depends on the interpolant's end piece, whose endpoint forward can be extreme or negative (about −6.8% forSpline.BSpline(3)on an upward-sloping 2% to 4% curve).Spline.MonotoneConvex()keeps its boundary instantaneous forward, so its forward curve stays continuous. See Interpolation Methods. Yield.FlatForwardAt(rate)supplies an independent terminal forward. It requires aFinanceCore.Ratesuch asContinuous(0.035)orPeriodic(0.035, 1); there is no method for a bare number (aMethodError), becauseYield.Constant(0.035)reads a bare number as annual effective. The rate is stored continuously compounded and stays fixed through fitting and Accessors updates. It preserves the final-knot discount factor and usually introduces a forward jump.- The policy is part of the curve. It is preserved by
fit,reconstruct, andAccessors.@set, compared by==, and readable ascurve.extrapolation. The type parameters ofYield.SplineandYield.MonotoneConvexare internal: dispatch on the type names orYield.AbstractInterpolatedZeroCurve. - MonotoneConvex supports the tail policies natively. Direct construction and loss-fitting
Spline.MonotoneConvex()return a nativeYield.MonotoneConvexfor:flat_forward,:flat_zero,:linear, andYield.FlatForwardAt(rate);Fit.Bootstrap()still rejects the descriptor. At the final knot,instantaneous_forwardreports the interior (left) value.
Quote conventions
OISYieldpays annually beyond one year. Maturities over one year now build annual-pay par swaps, matching SOFR, €STR, and SONIA overnight index swaps; they were quarterly. Maturities of one year or less still settle once. Bootstrapped OIS curves change slightly at maturities over one year.ParSwapYieldrequiresfrequency. The quarterly default was removed because fixed-leg conventions differ by market. Write, for example,ParSwapYield(r, t; frequency = 1)for OIS-style annual fixed legs orfrequency = 2for semiannual legs.InterestRateSwaprequiresfrequencyfor the same reason; both legs use it.InterestRateSwap(curve, 10)becomesInterestRateSwap(curve, 10; frequency = 4)to keep the former quarterly legs.ParYieldrejects a conflictingfrequency. APeriodicrate sets its own frequency; passing a differentfrequencynow throws anArgumentErrorinstead of being silently ignored. Convert the rate first, e.g.Periodic(1)(r).
CompositeYield accepts only + and -
Yield.CompositeYield(a, b, op) now requires op to be + or -, the operations behind a + b and a - b. Those compose the curves' factors: + multiplies the discount factors and - divides them, so interval factors and forward-starting curves compose as well. Another operation on the two zero rates throws a MethodError.
Migration: for a pointwise transformation of one curve's zero rates, use a TenorShift, for example a + ((z, t) -> max(z, Continuous(0.0))) to floor the zero rate. To combine two curves' zero rates in some other way, define a small curve type with its own zero method.
Interval factors that don't underflow
For the built-in curves, discount(curve, from, to), accumulation(curve, from, to) and forward(curve, from, to) are computed from the log-discount at each endpoint rather than as a ratio of discount factors. discount(curve, 0, t) is unchanged bit for bit, and so is every present_value of a contract. Intervals that start later can move by a few units in the last place, and intervals far in the tail are finite where they were NaN. Tests that compare such intervals exactly should allow for rounding. A custom curve that defines only discount keeps the ratio D(to)/D(from).
forward(curve, from, to) follows the same interval rule as discount(curve, from, to): on composite, scaled, ForwardStarting and Smith–Wilson curves it can move by a few units in the last place, and on a Smith–Wilson fit (or a custom curve) whose discount factors are negative at both ends it now returns the rate of the positive interval factor instead of throwing a DomainError.
Time derivatives at exactly t = 0 of Yield.MonotoneConvex, NelsonSiegel, NelsonSiegelSvensson and curves built on them are exact where they were NaN. MonotoneConvex now computes its log-discount directly, so some of its discount factors move in the last bit. For a curve without its own zero (Smith–Wilson, the short-rate models, ForwardStarting, custom curves) the zero rate at 0 is the 0/0 of L(t)/t, so its time derivative there, and that of a yield shift over such a curve, throws a DomainError.
Derivatives through fits and knot curves
Spline fits are now differentiable with ForwardDiff; see Sensitivities Through Calibration. The contract:
- Derivatives are first order; nested dual numbers throw.
- They are with respect to the quotes passed to
fit. Risk to another quote family computed from a fitted curve (for exampleimplied_quoteat its knots) is risk to that synthetic family. It equals market-quote risk only when the curve was fitted to that family at those tenors. - A differentiated loss fit must reprice its quotes (a Newton correction of at most
1e-6in every knot rate), or it throws.
Knot-rate derivatives at interpolation kinks changed:
Spline.MonotoneConvex(): where two adjacent discrete forwards are equal (every flat stretch of the curve), each knot partial is now the centered response to a bump of that knot; ForwardDiff previously returned a one-sided or fallback value. On a flat stretch these partials need not sum to the parallel-shift derivative, so do not aggregate them like a gradient: differentiate the parallel shift directly.Spline.PCHIP()andSpline.Akima()throw at their kinks, where they returnedNaNor a wrong derivative.- Away from kinks, derivatives are unchanged.
v6.0 to v6.1
Several items below change computed values (curve extrapolation, fitted bootstrap curves where the prior optimizer had not fully converged) or convert previously-silent mispricing into loud errors. Review each against your pipelines before upgrading.
MonotoneConvex(the defaultZeroRateCurveinterpolant) — two value-changing corrections:- Forward rates are now continuous at and beyond the last knot: extrapolation is anchored at the boundary instantaneous forward
f(tₙ)instead of the last discrete forward (seeYield.instantaneous_forward). Extrapolated zero rates change — on a typical upward-sloping curve with a 10y last knot, the 20y zero moves on the order of +10bp (about −2% PV for a 20y cashflow). For steeply inverted/humped curves the boundary forward can be collared to 0, giving a 0% forward tail beyond the last knot — extend your knot grid past your longest cashflow if you discount far beyond it. - The Hagan-West positivity collar was corrected (it previously clamped the wrong nodes and left one node unclamped, so the guaranteed-positive-forwards property could fail). Fitted/interpolated values change only where a clamp binds (sharply non-monotone forward curves); the collar is also generalized to negative discrete forwards.
- The module-local
Yield.forward(mc::MonotoneConvex, t)(instantaneous forward) was renamedYield.instantaneous_forward(mc, t).Yield.forwardnow refers toFinanceCore.forward, so the same call returns the discrete one-period forward as aRate— update qualified callers.
- Forward rates are now continuous at and beyond the last knot: extrapolation is anchored at the boundary instantaneous forward
- Bootstrap
fit(Fit.Bootstrap()) is now an exact per-knot root-solve instead of a per-knot optimizer pass. For zero-coupon quotes (any interpolant) and for coupon quotes with local interpolants (Spline.Linear/Quadratic/Cubic), every quote is repriced to root-finder precision; with global interpolants (Spline.BSpline) later knots still reshape earlier segments, so earlier coupon quotes reprice approximately (comparable to the previous behavior). Quotes are now sorted by maturity internally; duplicate maturities are an error. ZeroRateCurveeagerly builds its interpolation at construction rather than on first evaluation, anddiscount(zrc, t)fort < 0now throws aDomainError(it previously returned1.0silently — a misprice for anything that actually discounted at negative times). Notably, aBond.Floatingwhose maturity is not an integer multiple of the coupon period generates a stub first coupon that referencesforward(model, t - 1/freq, t)with a negative start time: on aZeroRateCurvethis was previously a silent half-sized stub forward and is now a loud error. Align floater maturities/resets to the coupon period.parnow throws an informativeArgumentErrorwhen the requested maturity implies a stub period that cannot be represented with the given coupon frequency (previously a bareInexactError).TransformedYieldis deprecated — useYield.TenorShift. The old name remains available as aBase.@deprecate_bindingalias but will be removed in a future release.- The Makie plotting extension now targets Makie ≥ 0.24 directly (the previous MakieCore-based extension stopped loading when Makie 0.24 absorbed MakieCore, so plot recipes had been silently unavailable). Makie < 0.24 is no longer supported. The UnicodePlots extension now renders only for rich (
text/plainMIME) display;print/string interpolation of curves no longer embeds a chart. - With FinanceCore v3,
irr/internal_rate_of_returnreturnPeriodic(NaN, 1)instead ofnothingwhen no root is found. Replaceisnothing(irr(x))checks withisnan(rate(irr(x))).
v5.x to v6
- Continuous zero rates are the curve primitive. Curve composition and shift arithmetic (
+,-,*,/,TenorShift,ProjectedShift) operate in continuous-zero-rate space, which is equivalent to multiplying/dividing/exponentiating discount factors. See Yield Curve Arithmetic.
v5.4 to v5.5
TransformedYield renamed to TenorShift; new ProjectedShift
Yield.TransformedYield has been renamed to Yield.TenorShift to sit alongside the new Yield.ProjectedShift, which adds a second time axis (projection / as-of time) to the shift rule. Both are concrete subtypes of the new Yield.AbstractYieldShift.
Use ProjectedShift for shifts whose shape evolves across a projection horizon (BMA SBA phase-ins, IFRS17 macro scenarios, EV runoffs). See the Yield Shifts section in Available Models - Yields for usage.
This release is tagged minor (5.4 → 5.5) but contains two breaking behavior changes that downstream code may need to react to:
- Field rename:
.transform→.rule. Direct field access onTransformedYieldinstances (e.g.,ty.transform) will fail. TheTransformedYieldtype name itself was preserved in v5.5 viaconst TransformedYield = TenorShift, so constructor and+-operator call sites continue to work unchanged. (As of v6 the alias emits a deprecation warning — see the v5.x → v6 section above.) - Strict
Ratereturn contract.Base.zeroonTenorShift/ProjectedShiftnow type-asserts the rule's return value asFinanceCore.Rate. Rules that previously returned a plainReal(silently coerced toContinuous) will now raise aTypeErrorat call time. Replace(z, t) -> z.continuous_value + 0.01with(z, t) -> Continuous(z.continuous_value + 0.01), or more idiomatically(z, t) -> z + Continuous(0.01)and letRatearithmetic carry compounding convention.
The TransformedYield alias is slated for removal one minor release after introduction. The + operator semantics (curve + (z, t) -> Rate) are unchanged — only the returned struct's name changes.
v4 to v5
Yield curve + and - now operate in continuous zero-rate space
In v4, curve_a + curve_b added rates in whatever compounding convention the curves happened to use. In v5, + and - always work in continuous zero-rate (CZR) space, which is equivalent to multiplying/dividing discount factors:
# v5 behavior:
combined = curve_a + curve_b
discount(combined, t) == discount(curve_a, t) * discount(curve_b, t)This is the economically correct way to combine deflators — see Yield Curve Arithmetic for a full explanation.
What to check when upgrading: If your v4 code added curves whose rates were expressed in Periodic conventions, the combined discount factors will now differ by the cross-term. For small rates and short horizons the difference is minor, but it compounds over long projections (e.g. 10 bps/year for a 5% base + 2% spread).
ForwardYields renamed to ForwardYield
The plural ForwardYields has been renamed to ForwardYield for consistency with other singular type names (Yield.Constant, ZCBYield, etc.).
v3 to v4
Yields.jl is now FinanceModels.jl
This re-write accomplishes three primary things:
- Provide a composable set of contracts and
Quotes - Those contracts, when combined with a model produce a
Cashflowvia a flexibly definedProjection - models can be
fitwith a new unified API:fit(model_type,quotes,fit_method)
Migrating Code
Update Dependencies
You should remove Yields from your project's dependencies and add FinanceModels instead. (link to Pkg documentation on how to do this)
API Changes
Previously, the API pattern was, e.g.:
model = Yields.Par(SmithWilson(...), rates,timepoints)Now, follow the pattern of:
- Define the quotes you want to fit the model to
fitthe model to those quotes
Example:
quotes = ParYield.(rates,timepoints)
model = fit(Yield.SmithWilson(ufr=0.03, α=0.1), quotes)Note that SmithWilson is not exported at the top level (qualify it as Yield.SmithWilson) and that the ufr and α keyword arguments are required: they are model hyperparameters that are not solved for in the fit.
Details of changes
Previously the kind of contract, the implied quotes, the type of model, and how the fitting process worked were all combined into a single call (Yields.Par). This minimized the amount of code needed to construct a yield curve, but left it fairly cumbersome to extend the package. For example, for every new yield curve model, methods for Par, CMT, OIS, Zero, ... had to be defined. Additionally, all of the inputs needed to be yields - specifying a price was not available as an argument to fit.
With the new design of the package, creating a completely new model is much easier, as only the model itself and the valuation primitives need to be defined. For example, defining a new yield curve type that works to value contracts instrument quotes only requires defining the discount method. To allow the model to be fit requires only defining a default set of parameters to optimize with __default_optic:
using FinanceModels, FinanceCore
using AccessibleModels
using IntervalSets
struct ABDiscountLine{A} <: FinanceModels.Yield.AbstractYieldModel
a::A
b::A
end
# define the default constructor for convenience
ABDiscountLine() = ABDiscountLine(0.,0.)
function FinanceCore.discount(m::ABDiscountLine,t)
#discount rate is approximated by a straight lined, floored at 0.0 and capped at 1.0
clamp(m.a*t + m.b, 0.0,1.0)
end
# `@optic` indicates what in our model variables needs to be updated (from AccessibleModels.jl)
# `-1.0 .. 1.0` says to bound the search from negative to positive one (from IntervalSets.jl)
FinanceModels.__default_optic(m::ABDiscountLine) = (
@optic(_.a) => -1.0 .. 1.0,
@optic(_.b) => -1.0 .. 1.0,
)
quotes = ZCBPrice([0.9, 0.8, 0.7,0.6])
m = fit(ABDiscountLine(),quotes)