Version Upgrade Guide
v5.12.0 to v6.0.0
v6 measures parallel risk without tenor grids. Each single-rate input uses its own shock coordinate; fixed cashflows on two curves use the combined rate's. See Shock coordinates for the full rule. v6 requires FinanceModels 7 and FinanceCore 3.
Call shapes
Arguments come in one order: the valuation callback (if any), the order marker of sensitivities, a measure marker such as DV01() or KeyRates(tenors), the rate or curves, then the cashflows and times or the contract. A contract's index curve is the keyword index, which defaults to the discount curve.
| Measure | Fixed cashflows | Valuation callback | Contract or portfolio |
|---|---|---|---|
| duration | duration([Macaulay(),] curve, cfs, times) | duration(valuation, curve) | duration([Spread(),] discount, contract; index) |
| DV01 | duration(DV01(), curve, cfs, times) | duration(valuation, DV01(), curve) | duration(DV01(), [Spread(),] discount, contract; index) |
| convexity | convexity(curve, cfs, times) | convexity(valuation, curve) | convexity([Spread(),] discount, contract; index) |
| dollar convexity | convexity(DollarConvexity(), curve, cfs, times) | convexity(valuation, DollarConvexity(), curve) | convexity(DollarConvexity(), [Spread(),] discount, contract; index) |
| key rates | duration(KeyRates(t), curve, cfs, times) | duration(valuation, KeyRates(t), curve) | duration(KeyRates(t), discount, contract; index) |
| two curves | duration(DV01(), base + credit, cfs, times) | duration(valuation, IR01(), base, credit), duration(valuation, DV01(), base, credit) | duration(DV01(), discount, contract; index) |
| all at once | sensitivities([SecondOrder(),] [KeyRates(t),] curve, cfs, times) | sensitivities(valuation, [SecondOrder(),] [KeyRates(t),] curve) | sensitivities([SecondOrder(),] [KeyRates(t),] discount, contract; index) |
| spreads | spread(curve1, curve2, cfs, times) | zspread(discount, contract, price; index) |
- Markers come in the order
DV01()orDollarConvexity(), thenEffective()orSpread(), thenKeyRates(t). Contract forms default toEffective(). - Callbacks also take two curves,
(valuation, base, credit), named curves,(valuation, curves::NamedTuple), and market inputs,sensitivities(valuation, inputs::NamedTuple). AScenariostakes the place of a Hull–White curve:Scenarios(hw; horizon, ...). sensitivitiesreturns(; value, duration, dv01), and withSecondOrder()alsoconvexityanddollar_convexity.valueis a number. The other fields are numbers withoutKeyRates, vectors (and a convexity matrix) with it, and keyed by role for several curves:s.dv01.base,s.dv01.discount,s.convexity.discount.index.
Changes
Curve convexity uses continuous-zero shocks. This changes scalar convexity for every yield model, including constant curves, ZeroRateCurve, Nelson–Siegel, and custom models. Cashflow and callback results are ≈ the sum of the full key-rate convexity matrix. For
[5, 5, 105]at[1, 2, 3]underYield.Constant(Periodic(0.04, 1)), convexity changes from 11.26 to 8.40. Scalars and explicitRateinputs retain their compounding conventions. See Convexity Conventions for formulas and examples.Dollar risk preserves position sign and zero-value exposure. DV01, IR01, and CS01 reverse sign when the position is reversed. Ordinary liability payments entered as negative cashflows contribute negative dollar risk; a mixed position's net present value alone does not determine the risk's sign. For
[-1, 1]at[0, 1]under a 0% curve, they return0.0001instead ofNaN. Contract sensitivity bundles also retain dollar exposure at zero value. Normalized duration and convexity remain undefined there. Migration: aggregateasset_dv01 + liability_dv01, notasset_dv01 - liability_dv01. Remove sign corrections added to compensate for the former use of absolute value.Fixed-cashflow IR01 and CS01 are the combined rate's DV01. For fixed cashflows,
duration(IR01(), base, credit, cfs, times)and the matching CS01 are replaced byduration(DV01(), base + credit, cfs, times)(see the removed forms below). Scalar-only inputs give v5's values. Mixed inputs change: a yield-model component gives a continuous-zero shock (v5 shocked a scalar spread as an annual rate, so CS01 was about IR01 / (1 + spread)), and a sum ofRates takes the left one's compounding. Use the callback forms when the curves play different roles. See Two curves: IR01 and CS01.Tenor grids appear only where results have a tenor dimension. Parallel measures no longer accept a
tenorsargument; the grid never changed their values, and its position let swapped arguments return wrong numbers silently. The old calls now throwMethodError:v5 call v6 replacement duration(curve, tenors, cfs, times),duration(curve, tenors, cashflows)duration(curve, cfs, times),duration(curve, cashflows)duration(DV01(), curve, tenors, cfs, times)duration(DV01(), curve, cfs, times)duration(IR01(), base, credit, tenors, cfs, times)(andCS01)duration(DV01(), base + credit, cfs, times)convexity(curve, tenors, cfs, times)convexity(curve, cfs, times)convexity(base, credit, tenors, cfs, times)convexity(base + credit, cfs, times)duration(valuation, curve, tenors)duration(valuation, curve)orduration(curve) do c ... endduration(DV01(), valuation, curve, tenors)duration(valuation, DV01(), curve)orduration(DV01(), curve) do c ... endduration(IR01(), valuation, base, credit, tenors)(andCS01)duration(valuation, IR01(), base, credit)orduration(IR01(), base, credit) do b, c ... endconvexity(valuation, curve, tenors)convexity(valuation, curve)orconvexity(curve) do c ... endconvexity(valuation, base, credit, tenors)convexity(valuation, base, credit)orconvexity(base, credit) do b, c ... endduration(Effective(), target, curve, tenors),duration(Spread(), ...), and thedv01andconvexity(Effective(), ...)formsduration(Effective(), curve, target),duration(Spread(), curve, target), and likewise forduration(DV01(), Effective(), curve, target)andconvexityduration(target, curve, tenors)duration(curve, target)two-curve contract forms (target, forward, credit, tenors)(credit, target; index = forward)KeyRates(tenors)forms keep their grids, andsensitivitiestakes one when it should decompose by tenor. The parallel measures are ≈ the sums of the key-rate results, soduration(Effective(), curve, target) ≈ sum(duration(Effective(), KeyRates(tenors), curve, target)).Key-rate grids are always a
KeyRates(tenors)marker, after the callback and order markers and before the curves. The contract bundles and named-curve callbacks took a raw tenor vector, positionally or as a keyword; those calls now throwMethodError:v5 call v6 replacement sensitivities(target, curve, tenors)sensitivities(KeyRates(tenors), curve, target)sensitivities(target, forward, credit, tenors)sensitivities(KeyRates(tenors), credit, target; index = forward)sensitivities(target, tenors; discount, index)sensitivities(KeyRates(tenors), discount, target; index)sensitivities(valuation, curves::NamedTuple; tenors)sensitivities(valuation, KeyRates(tenors), curves)sensitivities(curves::NamedTuple; tenors) do c ... endsensitivities(KeyRates(tenors), curves) do c ... endThe results take v6's shape, described below. The market-input form
sensitivities(valuation, inputs)has no grid.The valuation callback comes first in every callback form, then any marker, then the rate, curves, or inputs, as in
sensitivities(valuation, inputs). Do-block calls such asduration(curve) do c ... endandsensitivities(KeyRates(tenors), curve) do c ... endare unchanged, and now also work for scalar andRateinputs; v5 accepted a do-block only for yield models. The callback-second orders throwMethodError:v5 call v6 replacement duration(rate, valuation)duration(valuation, rate)orduration(rate) do r ... endduration(DV01(), rate, valuation)duration(valuation, DV01(), rate)convexity(rate, valuation)convexity(valuation, rate)duration(KeyRates(tenors), valuation, curve)duration(valuation, KeyRates(tenors), curve)duration(DV01(), KeyRates(tenors), valuation, curve)duration(valuation, DV01(), KeyRates(tenors), curve)duration(IR01(), KeyRates(tenors), valuation, base, credit)(andCS01)duration(valuation, IR01(), KeyRates(tenors), base, credit)convexity(KeyRates(tenors), valuation, curve)(andbase, credit)convexity(valuation, KeyRates(tenors), curve)sensitivities(KeyRates(tenors), valuation, curve)(andbase, credit)sensitivities(valuation, KeyRates(tenors), curve)Contract forms put the curve before the contract; see below.
The finite-difference
KeyRateDurationAPI is removed.KeyRate,KeyRateZero,KeyRatePar, andkrd_pointsare gone. Replaceduration(KeyRateZero(t), curve, cfs, times, grid)with the entry attofduration(KeyRates(grid), curve, cfs, times). It applies the same triangular continuous-zero bump, differentiated exactly rather than by a ±10 bp central difference, so values move slightly. Wrap a scalar orRateinYield.Constant. Pass the grid explicitly; the former default grid of annual knots from year 1 is no longer implied.KeyRateParhas a different migration: use the market-input callback to rebuild a curve from par quotes.KeyRatesis not a par-rate replacement. See Par-yield sensitivities: AD and bump-and-reprice for both AD and a configurable basis-point bump, including the old normalization convention.Embedded cashflow times take precedence. Key-rate forms now accept a
Cashflowvector together with atimesvector; eachCashflowkeeps its own time. Scalar, key-rate, and bundled sensitivities use embedded payment times. Numeric amounts use the corresponding explicit times. Explicit time vectors must cover the collection; trailing entries are ignored. Omitted times default toeachindex(cfs)in every key-rate andScenarioscashflow form, so numeric amounts are paid at periods1:nas in the scalar measures. Migration: to change payment dates, construct updatedCashflowobjects or pass numeric amounts with the desired times.present_valuespays aCashflowat its own time and returns numbers. v5 discounted aCashflowover its paired time (or its index) and returnedCashflows, sopresent_values(curve, [Cashflow(100.0, 2.0)], [10.0])under a 3% continuous curve gave aCashflowworth 74.08 instead of 94.18, andCashflows at different times threw.Hull–White simulation is requested with
Scenarios. A bareHullWhitemodel is now a curve everywhere: a bump moveshw.curve, cashflows are discounted on it, and caps and swaptions keep their Hull–White closed forms on the bumped curve. In v5 it was a curve toduration,convexity, and the cashflow forms without times, but a scenario generator tosensitivitiescallbacks and to cashflow forms with times, so two believable key-rate vectors could come back for the same position. The simulation keywords move intoScenarios:v5 call v6 replacement sensitivities(KeyRates(tenors), hw, cfs, times; n_scenarios, timestep, horizon, rng)(andDV01())sensitivities(SecondOrder(), KeyRates(tenors), Scenarios(hw; horizon, n_scenarios, timestep, rng), cfs, times)sensitivities(KeyRates(tenors), hw; kws...) do paths ... end(andDV01())sensitivities(SecondOrder(), KeyRates(tenors), Scenarios(hw; horizon, kws...)) do paths ... endsensitivities(KeyRates(tenors), hw, cfs, times)withoutScenariosnow discounts onhw, asduration(KeyRates(tenors), hw, cfs, times)does.A
Scenariosfixes its horizon, time grid and seed when it is constructed.horizonis required: v5 defaulted to 30 years for callbacks and to one year past the last payment for cashflows, so two positions were simulated on different grids and their risks did not add. It must be a whole number oftimesteps, up to rounding:horizon = 0.9, timestep = 0.5used to reach 1.0 silently and now throwsArgumentError. A payment after the horizon throws. The constructor draws one seed fromrng, and every valuation with the sameScenariosreuses it, where v5 drew a new seed fromrngon each call. Results are therefore reproducible and add across calls: values and dollar derivatives add, and normalized measures add when weighted by value. For the v5 default horizon of cashflows, passhorizon= the last payment time + 1.Unmarked contract and portfolio DV01 and convexity, which v5 did not define, now default to
Effective(), as unmarked duration already did. So doduration(DV01(), KeyRates(tenors), curve, target)andconvexity(KeyRates(tenors), curve, target). UseSpread()for spread risk.Scalar and
RateDV01 use the analytic formula. v5 used automatic differentiation, so values can move in the last bits (at most 4 ulp inFloat64). DV01 now keeps the rate's precision: aFloat32rate withFloat32amounts returns aFloat32DV01, where v5 returnedFloat64. Macaulay and modified duration are unchanged, and so is convexity for scalars andRates.Callback APIs accept callable structs. Cashflow APIs accept arrays, tuples, and finite generators. Arrays are flattened in column-major order; generators are collected once before valuation.
Results own independent arrays for each role and convexity block. Mutating one no longer changes another.
Zero cashflow streams take the type of a nonempty result. Empty and all-zero streams still return exact zeros without valuing any payment, but their numeric type now includes the rate or curve: it is the type the same measure returns for a nonempty stream. The rate or curve is queried once, at time zero, for this. An untyped empty collection (
Any[],Cashflow[],()) takes its type from the rate or curve instead of falling back toFloat64. For example, a zero stream under aBigFloatcurve now returnsBigFloat, notFloat64. An abstractly typed nonempty stream (Cashflow[...],Any[...]) takes its time type from the times its payments use, as valuation does.moicno longer throws for one-sign and empty streams. A sum over no terms contributes zero, so a total loss (contributions only) returns0.0. Without contributions the ratio divides by zero:Inffor positive distributions, andNaN(undefined) for an empty or all-zero stream. v5 threw anArgumentErrorfor these.wassersteinno longer wraps around on integer samples. The gap between two quantiles was taken in the samples' own integer type:wasserstein(UInt8[0], UInt8[1])returned 255 andwasserstein(Int8[-100], Int8[100])returned 56. Gaps between signed or unsigned integer samples are now computed exactly and rounded once to the result's float type, so these return 1 and 200 whatever integer types hold the samples.Float32samples' gaps are no longer rounded toFloat32first, which can change results in the last bits.spreadandzspreadtest convergence on the Newton step in rate units, not on the price residual in currency.tol(default1e-12) now bounds the last Newton step.spreadalso checks that the result reprices the cashflows. Results no longer depend on the notional: at a notional of1e-10, a true 2% z-spread previously came back as about 1.96%. A zeromarket_pricenow works. Passtolas a rate if you set it explicitly.zspreadreturns its spread as aContinuousrate, asspreadreturns aPeriodicone. A number added to a curve is read as an annual rate, sodiscount + result.zspreadmispriced: for a 5-year 5% semiannual bond at a 1% spread over a 4% continuous curve, by 2.2e-4 per unit of face. The typed spread reprices.zspread_dv01stays a number, ands0may be a number (continuous) or aRate.v5 call v6 replacement zspread(...).zspread(a number)FinanceCore.rate(zspread(...).zspread);discount + zspread(...).zspreadtakes the typed rate directlyContract measures value in FinanceModels valuation contexts. They differentiate
present_value(Models(discount; index), contract). The bumped discount curve discounts cashflows and values closed forms. Every key the contract reads returns the bumped index curve. Contracts that v5 missed now work: anInterestRateSwap, whose floating leg is a transformed contract, threw aMethodError. ACompositeor a portfolio of self-timed contracts that share a reporting currency is worth ≈ the sum of its parts, so closed-form contracts inside one now have risk too. Migration: a custom contract is valued by FinanceModels: give it a cashflow projection, or a closed formpresent_value(ctx, c::MyContract)readingdiscount(ctx, t),ctx[key]andvaluation_model(ctx). A closed form defined only for a yield model,present_value(m::AbstractYieldModel, c::MyContract), is not reached through the context. v5 valued an unknown contract as if it needed no index curve, so a custom floating contract never saw it; a contract with neither a projection nor a closed form now throws aMethodError.sensitivitieshas one result shape, and its order is a marker. It returns(; value, duration, dv01), or withSecondOrder()(; value, duration, dv01, convexity, dollar_convexity).FirstOrder()is the default.valueis always a number. WithoutKeyRatesthe derivative fields are numbers for one parallel shift; withKeyRates(tenors)they are per-tenor vectors and a convexity matrix; with several curves they are keyed by role:s.dv01.base,s.convexity.base.credit. Key-rate cashflow and callback forms used to return convexities by default: passSecondOrder()for them. TheDV01()forms ofsensitivitiesare removed, since every result hasdv01. WithoutKeyRates, each curve takes one parallel shift, soFirstOrder()costs one derivative per curve.v5 field v6 field durations,dv01s,convexitiesduration,dv01,convexity(SecondOrder())base_durations,credit_durations,base_dv01s,credit_dv01sduration.base,duration.credit,dv01.base,dv01.creditconvexities.base,.credit,.crossconvexity.base.base,.credit.credit,.base.creditnamed curves and market inputs: key_rate.role,key_rate_dv01.roleduration.role,dv01.rolenamed curves and market inputs: parallel duration.role,dv01.rolethe same call without KeyRates, orsum(s.duration.role)contracts: spread_*,forward_*duration.discount,dv01.discount;duration.index,dv01.indexcontracts: effective_duration,effective_dv01duration.discount + duration.index(anddv01), orduration(Effective(), ...)contracts: *_key_ratethe same fields with KeyRates(tenors)Two-curve
convexity(valuation, [KeyRates(t),] base, credit)returns the blocks keyed by role, theconvexityfield ofsensitivities(valuation, SecondOrder(), ...):.base,.creditand.crossbecome.base.base,.credit.creditand.base.credit..credit.baseequals.base.creditfor parallel shifts and is its transpose withKeyRates.Dollar convexity is new.
DollarConvexity()selects the signed second derivative∂²V/∂sᵢ∂sⱼin the usual shock coordinates, with no factor of one-half and no basis-point scaling:convexity(DollarConvexity(), curve, cfs, times),convexity(valuation, DollarConvexity(), curve)andconvexity(DollarConvexity(), [Effective() | Spread(),] [KeyRates(t),] discount, contract; index), with the layouts ofconvexity. Second-ordersensitivitiesreturn it asdollar_convexity. It is defined at zero value and adds across positions, as DV01 does; a shift ofbbasis points changes value by about-dv01 * b + 0.5 * dollar_convexity * (b / 10_000)^2.dv01is removed;duration(DV01(), ...)is the one standalone DV01, and everysensitivitiesresult keeps itsdv01field. The valuation callback still comes first, so do-blocks work:duration(DV01(), curve) do c ... end. With two curves or aNamedTupleof curves, the callback form returns one DV01 per role, thedv01field ofsensitivities.Removed call v6 replacement dv01(curve, cfs, times)duration(DV01(), curve, cfs, times)dv01(kr, curve, cfs, times)duration(DV01(), kr, curve, cfs, times)dv01(f, curve)duration(f, DV01(), curve)dv01(f, kr, curve)duration(f, DV01(), kr, curve)dv01(f, base, credit)duration(f, DV01(), base, credit)dv01(f, named_curves)duration(f, DV01(), named_curves)dv01(Effective(), contract, curve)duration(DV01(), Effective(), discount, contract; index)dv01(Spread(), contract, curve)duration(DV01(), Spread(), discount, contract; index)Contract measures take the curve first and the index curve as a keyword, as FinanceCore and FinanceModels do:
duration(Effective(), discount, contract; index = discount), and the same forSpread(),duration(DV01(), ...),convexity,sensitivitiesandzspread(discount, contract, price; index = discount). The contract-first calls and the positional(forward, credit)curves throwMethodError. Contractsensitivitiesreport the index exposure separately under the role names ofModels(discount; index):s.dv01.discount(spread risk) ands.dv01.index(coupon reset). The layered formsensitivities(KeyRates(tenors), target; discount = (; rf, credit), index)becomessensitivities(KeyRates(tenors), (; rf, credit), target; index).convexity(Spread(), ...)is new; effective convexity includes the cross terms between the curves. A vector ofCashflows after a curve is fixed cashflows; to value one as a portfolio of contracts, give it the element typeAbstractContract.Amounts and times pair by position in every cashflow form, including
present_values,breakevenandspread: the k-th amount is paid at the k-th time, counted from each vector's first entry. Offset time vectors used to pair by index:present_values(Yield.Constant(Continuous(0.03)), [100, 100], OffsetArray([1, 2, 3], 0:2))valued the payments at times 2 and 3 (185.57) and now values them at 1 and 2 (191.22). Offset key-rate grids read out of bounds;KeyRatesnow keeps its own 1-based copy of its tenors. Key-rate andScenarioscashflow forms accept tuples and generators, as the scalar forms already did.spreadnow ignores unused trailing times like the other forms, andbreakeventhrowsDimensionMismatchfor too few times instead ofBoundsError.priceis removed. It wasabs(present_value(...)); write that instead, or usepresent_valuewhen the position's sign matters.The unused
Durationsupertype of the measure markers is removed.Fixed-cashflow forms that repeated one result per curve are removed. For cashflows discounted at
base + credit, every role has the same derivatives, so these forms returned one number or matrix several times. They now throwMethodError:Removed call Replacement duration(IR01(), base, credit, cfs, times)(andCS01)duration(DV01(), base + credit, cfs, times)duration(IR01(), KeyRates(tenors), base, credit, cfs, times)(andCS01)duration(DV01(), KeyRates(tenors), base + credit, cfs, times)convexity(base, credit, cfs, times)convexity(base + credit, cfs, times), which each block equaledconvexity(KeyRates(tenors), base, credit, cfs, times)convexity(KeyRates(tenors), base + credit, cfs, times)sensitivities(KeyRates(tenors), base, credit, cfs, times)(andDV01())sensitivities(KeyRates(tenors), base + credit, cfs, times)sensitivities(KeyRates(tenors), curves::NamedTuple, cfs, times),convexity(KeyRates(tenors), curves, cfs, times)the same with reduce(+, curves)as the curveThe
IR01/CS01callback forms remain for valuations in which the curves play different roles.duration(issue_date, date)is renamedpolicy_duration(issue_date, date), sodurationmeans only interest-rate duration. The old call throwsMethodError.reprojectis removed, because it duplicated FinanceModels' valuation contexts. Replacepresent_value(discount, reproject(contract, index_curve))withpresent_value(Models(discount; index = index_curve), contract), or pass a model store,Models(discount, Dict(key => curve, ...)), for distinct index curves or FX. The projected cashflows arecollect(Projection(contract, Models(discount; index = index_curve))).
v5.11.2 to v5.12.0
- ForwardDiff 1.x is now required. Version 1.0 made Dual comparisons account for partials, which the exact zero-stream check needs to preserve cashflow-amount derivatives. Support for ForwardDiff 0.10 is removed; Julia 1.10 remains supported.
- Analytic
KeyRatescalculations preserve the numeric type of discounted cashflows, includingBigFloatand automatic-differentiation values from curve parameters. - Empty cashflow collections and all-zero cashflow amounts now have zero value and zero risk, including normalized duration and convexity by convention. This applies to explicit-cashflow scalar duration/convexity, DV01/IR01/CS01, legacy key rates, and
KeyRatessensitivities (including Hull–White). Previously these inputs could produceNaN, aBoundsError, or a legacy default-gridArgumentError. Results keep their usual shapes and use positive zeros.present_valueslikewise returns an empty vector or a vector of zeros. - Covered explicit-cashflow methods accept a time grid longer than the amount vector and ignore trailing entries. Previously some scalar methods rejected them. Too few times now consistently throws
DimensionMismatch. Legacy default key-rate grids and Hull–White default horizons use only the supplied cashflows' times. Empty streams return zero even with a populated time grid; legacy forms skip default-grid derivation for zero streams but still validate an explicit grid. - Zero streams do not evaluate the curve or run simulations. Their numeric types come from the amounts and times, plus the tenor grid for key-rate results, without the curve's numeric type; nonzero streams still promote from discounted cashflows. Abstractly typed empty inputs fall back to
Float64. (v6.0 adds the curve's type and drops theFloat64fallback; see above.) - Skipping Hull–White simulation for zero streams leaves the RNG unchanged. In a batch using one shared RNG, subsequent contracts therefore receive different draws than in prior versions. Use independently assigned RNG streams when contract-level reproducibility must be independent of preceding contracts.
- Nonzero amounts that offset to zero present value are not zero streams: normalized duration and convexity still produce
NaN/Inf, and dollar exposures are not reset to zero. Valuation-function and contract inputs retain their existing behavior. Aggregate portfolio values and dollar derivatives before normalizing once. An unweighted average of individual durations includes zero-stream entries as zeros; it is not a portfolio duration. See Zero cashflow streams. VaRandCTEnow enforce their documented domain0 ≤ α < 1at construction, including through theValueAtRiskandConditionalTailExpectationaliases and explicitly typed constructors.WangTransformrequires0 < α < 1; its earlier documentation incorrectly included the endpoints. Out-of-domain real values (includingNaNand infinities) throwArgumentError. Explicitly typed constructors validate the value after conversion. At zero,VaRremains the essential infimum andCTEremains the mean. This establishes behavior for invalid parameters; it does not change results at valid confidence levels.
v5.11.1 to v5.11.2
This release fixes a class of bugs where risk measures returned believable but wrong finite numbers. Two behavior changes matter for existing code.
VaR now uses the standard lower quantile
The old implementation selected the upper quantile at exact atom boundaries. The new VaR(α) is the standard generalized inverse, $\inf\{x : F(x) \ge \alpha\}$, everywhere: distributions, arrays, and discrete laws. Values can decrease by a whole atom at exact boundaries. This is an intentional correctness fix, not a numerical drift:
VaR(0.5)(Bernoulli(0.5)) # old: 1, new: 0
VaR(0.95)(collect(1.0:1000.0)) # old: 951, new: 950Related details:
CTEsemantics are unchanged. It still averages exactly the worst1-αof probability mass, with fractional weight on the boundary atom.VaRon a distribution now returnsDistributions.quantile(risk, α)and keeps its type — for example anIntfor a count distribution.VaR(0)is the essential infimum. For a distribution with support unbounded below it returns-Inf; previously it returned an unconverged quadrature number.robustvalue's adverse scenario forVaRnow shifts the rank VaR itself selects. Previously, at an exact boundary the shifted set could exclude that rank, and the reported "robust" value equaled the base value.
Divergent risks return honest values or throw
The old implementation integrated distorted distribution functions with unchecked quadrature and subtracted the two halves. Divergent integrals produced plausible finite numbers:
Expectation()(Cauchy()) # old: 0.0, new: NaN (mean does not exist)
Expectation()(Pareto(0.5, 1)) # old: ≈2.9e8, new: Inf (mean diverges)
CTE(0.95)(Cauchy()) # old: ≈215.14, new: Inf (tail mean diverges)
WangTransform(0.9)(Cauchy()) # old: a finite number, new: throws ErrorExceptionThe policy: results that are provably divergent from the distribution's mean semantics return NaN/±Inf; a quadrature result that cannot be verified throws instead of returning a number.
Related details:
Expectation,VaR, andCTEon distributions are now analytic or exact where possible. Values move within the old quadrature tolerance.- All distortion measures on arrays and on discrete distributions (
DiscreteNonParametric,Binomial,Poisson, …) now evaluate as exact or checked weighted sums instead of quadrature over step functions. DualPowerand the internal complementary distortions now use numerically stable forms (expm1/log1p). The old algebra could silently lose the quadrature error certificate in far tails.- Empty arrays now throw an
ArgumentErrorfor every risk measure.
v5.8 to v5.9
Non-breaking unless you relied on the specific edge-case behaviors noted below.
Risk measures
- Array inputs to
VaR,CTE, andExpectationare now computed as exact order statistics (the discrete Choquet integral evaluated as a finite weighted sum) instead of adaptive quadrature over the empirical CDF's step function. The exact value of the risk measure is now returned: results may differ from v5.8 in the last few ulps, or visibly at plateaus/quantile boundaries where the quadrature approximation was least accurate. Distribution inputs are unchanged (numerical integration of the distorted CDF). Expectationis now exported —using ActuaryUtilitiesbrings it into scope (previouslyActuaryUtilities.RiskMeasures.Expectation).
Financial math
spreadsolves via Newton + AD on the pricing residual, converging to machine precision (previously a derivative-free minimization with ~√tolerance precision). It now throws anErrorExceptionon non-convergence instead of returning the best-so-far point.moicthrows an explicitArgumentErrorwhen the input has no positive or no negative cashflows ("moic requires at least one positive (distribution) and one negative (contribution) cashflow"); previously such degenerate input surfaced as an obscure reduce-over-empty-collection error.- Analytic fast paths for
Modifiedduration andconvexitywith flat yields (Real,Rate{Periodic},Rate{Continuous},Yield.Constant): same values as the AD path (equality-tested), substantially faster. present_valuesis now O(n) (previously O(n²) and recursive — very long cashflow vectors could overflow the stack) and propagates AD dual numbers through its accumulator.- Legacy bump-and-reprice
duration(keyrate::KeyRateZero/KeyRatePar, curve, cashflows)now derives the default key-rate grid from embeddedCashflowtimes rather than the vector indices. Pricing always used the embedded times, so values at grid points common to both versions are unchanged; what changes is the default grid extent (e.g. semiannualCashflows at 0.5…5.0 previously implied a grid out to 10 years, soduration(KeyRate(7), curve, cfs)returned0.0and now — like any timepoint outside the grid — raises anArgumentError). Plain amount vectors are unaffected. When every timepoint is below 1, the default grid would be empty and also raises anArgumentErrorasking for an explicitkrd_points(previously an obscureMethodError).
Dependencies & ecosystem
- Optimization.jl and OptimizationOptimJL.jl are no longer dependencies (
spreadwas their only use). - FinanceCore v3 is now supported. A failed
irr/internal_rate_of_returnstill returnsnothingunder FinanceCore 3.0, soisnothing(irr(x))checks keep working.
v5.7 to v5.8
Additive release — no existing method changed. A contract / portfolio-aware layer was added on top of the key-rate AD engine, so curve-dependent instruments (e.g. floating-rate bonds) can be passed directly and have their cashflows re-projected under bumped curves:
EffectiveandSpreadduration markers:duration(Effective(), contract, curve, tenors)reprices with coupons re-fixed (the correct interest-rate duration for floaters);duration(Spread(), contract, curve, tenors)bumps the discount curve only (discount-margin / credit duration). A contract or aVectorof contracts (a portfolio) is accepted.dv01verb:dv01(Effective()/Spread(), target, [forward, credit,] tenors)for the dollar versions;dv01(args...)is equivalent toduration(DV01(), args...)for the existing cashflow/curve forms.sensitivities(target, [forward, credit,] tenors): one-AD-pass bundle for a contract/portfolio returningvalue,effective_*,spread_*, andforward_*durations / DV01s / key-rate vectors.- NamedTuple multi-curve
sensitivities:sensitivities(target, tenors; discount = (; rf, credit, ilp), index = ...)decomposes sensitivities per named discount role (rf≈ IR01,credit≈ CS01, etc.) plus theindex(reset) sensitivity, returning(; value, duration, dv01, key_rate)per role. A do-block formsensitivities(valuation, curves::NamedTuple; tenors)is also available. - Helpers:
zspread(Newton-solved constant spread to match a market price, with its DV01),locked_floater(in-force floater with the current coupon locked until the next reset), andreproject(wrap a contract so its coupons are estimated off a given index curve).
v5.6 to v5.7
Overview
The key-rate sensitivity API (duration, convexity, sensitivities, with KeyRates, IR01, CS01, and two-curve variants) now carries the KRD knot grid on the KeyRates marker itself: KeyRates(tenors). The previous ZeroRateCurve-specific dispatch that pulled the grid implicitly from zrc.tenors is gone — KeyRates(tenors) is now the single uniform way to specify the knot grid.
The AD pathway is also rewritten: it now layers a triangular-hat zero-rate bump on top of the user's curve via FinanceModels.Yield.TenorShift rather than rebuilding the curve from AD-tagged rates. This works on any AbstractYieldModel — composites, UFR extrapolators, fitted Nelson-Siegel models, etc. — without requiring the user to first convert to ZeroRateCurve.
API Changes
Breaking — vector / matrix key-rate calls now require KeyRates(tenors) carrying the knot grid. The migration is a one-symbol replacement: KeyRates() → KeyRates(tenors), and any trailing tenors argument falls out of the call. sensitivities also adopts the KeyRates(tenors) marker for uniformity.
# v5.6
duration(KeyRates(), zrc, cfs, times)
duration(DV01(), KeyRates(), zrc, cfs, times)
convexity(KeyRates(), zrc, cfs, times)
sensitivities(zrc, cfs, times)
duration(IR01(), KeyRates(), base, credit, cfs, times)
sensitivities(hw, cfs, times) # Hull-White MC
# v5.7 (typical migration: use zrc.tenors as the grid)
tenors = zrc.tenors
duration(KeyRates(tenors), zrc, cfs, times)
duration(DV01(), KeyRates(tenors), zrc, cfs, times)
convexity(KeyRates(tenors), zrc, cfs, times)
sensitivities(KeyRates(tenors), zrc, cfs, times)
duration(IR01(), KeyRates(tenors), base, credit, cfs, times)
sensitivities(KeyRates(tenors), hw, cfs, times)You can pass any knot grid — KRD buckets are now an explicit modeling choice, not tied to the curve's storage tenors:
# A curve fit on monthly observations, KRDs reported at FRTB buckets
FRTB = [0.25, 0.5, 1, 2, 3, 5, 10, 15, 20, 30]
duration(KeyRates(FRTB), pv, fitted_curve)Non-breaking — scalar duration / convexity / DV01 calls fall through to the generic finite-difference scalar path and continue to work without tenors:
duration(zrc, cfs, times) # still works (FD scalar)
duration(DV01(), zrc, cfs, times) # still works (FD scalar)
convexity(zrc, cfs, times) # still works (FD scalar)
duration(zrc) do c; pv(c); end # still works (FD scalar)Numerical values agree with the v5.6 AD-based scalars to FD precision (~1e-6).
Per-knot KRDs may shift slightly for non-Linear-spline ZeroRateCurve inputs. The new AD path uses triangular-hat bumps; the old path propagated AD through the curve's spline. For Spline.Linear() ZRCs the answers are bitwise identical. For Spline.MonotoneConvex() (the default), PCHIP, Cubic, etc., per-knot KRDs differ by sub-bp on discount factors at typical knot spacing. Sum of KRDs, scalar modified duration, and parallel-shift sensitivity are all invariant. The new convention matches the textbook KRD definition and is independent of the curve's interpolator choice.
Hull-White no longer requires hw.curve to be a ZeroRateCurve. Any AbstractYieldModel works, and the knot grid is supplied via KeyRates(tenors) at the call site: sensitivities(KeyRates(tenors), hw, cfs, times).
Two-curve API: the previous ArgumentError on mismatched base.tenors != credit.tenors is gone — supply your own knot grid via KeyRates(tenors) and the two curves can have any storage structure.
v3 to v4
Overview
The shape and API of the package is mostly unchanged. The changes that have made fall into a few categores:
- Accommodating FinanceModels.jl, the next-generation version of Yields.jl.
- Simplifying the API, generally making function calls require more specific arguments to avoid ambiguity
- Accommodating the new
Cashflowtype which makes modeling heterogeneous assets and liabilities simpler.
API Changes
- Breaking: The functions
europutandeurocallhave been moved toFinanceModels - Breaking: Previously, the first argument to
present_valueorpresent_valueswould be interpreted as a set ofPeriodic(1)one-period forward rates if a vector of real values was passed. Users should explicitly create the yield model first, instead of relying on the implicit conversion:
# old
pv([0.05,0.1], cfs)
# new
using FinanceModels
y = fit(Spline.Linear(), ForwardYield([0.05,0.1]), Fit.Bootstrap())
pv(y,cfs)