Financial Math Submodule
Provides a set of common routines in financial maths.
Quickstart
using ActuaryUtilities
cfs = [5, 5, 105]
times = [1, 2, 3]
discount_rate = 0.03
present_value(discount_rate, cfs, times) # 105.65
duration(Macaulay(), discount_rate, cfs, times) # 2.86
duration(discount_rate, cfs, times) # 2.78
convexity(discount_rate, cfs, times) # 10.62Macaulay/Modified duration are for fixed cashflows. For a floating-rate bond, a portfolio, or any curve-dependent contract, pass the contract directly — duration(Effective(), contract, curve, tenors) / duration(Spread(), …) / dv01(…) re-project the coupons and give the rate-vs-spread durations (years and DV01s); sensitivities(contract, curve, tenors) returns the full bundle, and sensitivities(contract, tenors; discount = (; rf, credit, ilp), index = …) does the multi-curve decomposition. See Key Rate Sensitivities.
Zero cashflow streams
An empty collection and an all-zero projection grid both represent no cashflows. Explicit-cashflow duration, convexity, and key-rate sensitivities return zero risk for either representation. Value and dollar risk are zero; normalized duration and convexity are assigned zero by convention. This includes scalar Macaulay/Modified duration, DV01/IR01/CS01, legacy key rates, and Hull–White cashflow sensitivities. present_values returns an empty vector or a vector of zeros.
Every cashflow needs a corresponding time, but the time grid may be longer. Unused trailing times are ignored, including when deriving a legacy key-rate grid or a Hull–White simulation horizon. Too few times throws DimensionMismatch. Empty cashflows are valid with either an empty or populated time grid.
| Cashflow amounts | Value and dollar risk | Normalized risk |
|---|---|---|
| Empty or all exactly zero | Zero | Zero by convention |
| Nonzero amounts with zero net present value | Dollar risk can be nonzero | Undefined (NaN/Inf) |
| Nonzero present value | Calculated as usual | Calculated as usual |
The check uses exact iszero on amounts, including AD partials, rather than a tolerance or net present value. Both 0.0 and -0.0 are zero; a tiny nonzero amount is not. Normalized duration is invariant to nonzero scaling of amounts, so the assigned value at exactly zero is not the limit as amounts shrink. Explicit key-rate grid validation still applies; zero streams need no derived default grid. Zero streams need no discount factors, so the curve is never evaluated and Hull–White does not simulate. Their numeric types come from amounts and times, plus the tenor grid for key-rate results. They may therefore differ from a nonzero stream's curve-derived type (for example, Float64 versus BigFloat or Dual). Abstractly typed empty inputs fall back to Float64. When preallocating batch or AD buffers, choose a compatible numeric type from the calculation's inputs inside the differentiated function. Do not infer it from the first contract's result, which may come from a zero stream.
Skipping Hull–White simulation leaves the RNG unchanged, so a shared-RNG batch uses different subsequent draws than versions that simulated zero streams. Independently assigned RNG streams avoid dependence on preceding contracts.
To aggregate portfolio risk, sum values and dollar derivatives before normalizing once. This also preserves exposures from positions whose net value is zero. An unweighted average of contract durations includes zero-stream entries as zeros and is not a portfolio duration. Valuation-function and contract APIs retain their existing normalization behavior: a zero valuation alone does not identify a zero stream.
Curve Transformations
FinanceModels.Yield.TenorShift lets you lazily transform any yield curve's zero rates via curve + (z, t) -> new_rate. This is useful for scenario analysis (parallel shifts, twists, stresses) without refitting.
Parallel shift
using ActuaryUtilities, FinanceModels, FinanceCore
base = Yield.Constant(Continuous(0.05))
shifted = base + (z, t) -> z + Continuous(0.01) # +100 bp parallel
zero(shifted, 1.0)Continuous(0.060000000000000005)Periodic rate arithmetic
Rate arithmetic automatically handles compounding conversion — a Periodic(0.01, 1) bump converts to continuous internally:
shifted_p = base + (z, t) -> z + Periodic(0.01, 1)
zero(shifted_p, 5.0) # ≈ Continuous(0.05 + log(1.01))Continuous(0.05995033085316809)Tenor-dependent twist
A steepener that fades at 30y:
twist = base + (z, t) -> z + Continuous(0.02 * max(0.0, 1.0 - t / 30.0))
(zero(twist, 1.0), zero(twist, 15.0), zero(twist, 30.0))(Continuous(0.06933333333333333), Continuous(0.060000000000000005), Continuous(0.05))PV comparison under stress
cfs = [5.0, 5.0, 5.0, 105.0]
times = [1.0, 2.0, 3.0, 4.0]
pv_base = present_value(base, cfs, times)
pv_shifted = present_value(shifted, cfs, times)
pct_change = (pv_shifted - pv_base) / pv_base * 100
(; pv_base = round(pv_base, digits=4), pv_shifted = round(pv_shifted, digits=4), pct_change = round(pct_change, digits=2))(pv_base = 99.5506, pv_shifted = 95.9157, pct_change = -3.65)Bootstrapped curve + stress
quotes = ZCBYield.([0.04, 0.05, 0.055, 0.06], [1.0, 3.0, 5.0, 10.0])
fitted = fit(Spline.Linear(), quotes, Fit.Bootstrap())
stressed = fitted + (z, t) -> z + Continuous(0.005) # +50 bp
dur_base = duration(fitted, cfs, times)
dur_stressed = duration(stressed, cfs, times)
(; dur_base = round(dur_base, digits=4), dur_stressed = round(dur_stressed, digits=4))(dur_base = 3.719, dur_stressed = 3.716)Negative rates
Real-world EUR/JPY/CHF scenarios with negative base rates:
neg = Yield.Constant(Continuous(-0.01))
shifted_neg = neg + (z, t) -> z + Continuous(0.005)
(; rate = zero(shifted_neg, 5.0), df = round(discount(shifted_neg, 5.0), digits=6))(rate = Continuous(-0.005), df = 1.025315)API
Exported API
ActuaryUtilities.FinancialMath.CS01 — Type
CS01 <: DurationCredit Spread 01. The dollar change in value for a 1 basis point parallel shift in the credit spread, holding the risk-free (base) curve constant.
Requires both a base curve and credit spread to be specified. For a flat additive decomposition, CS01 ≈ IR01 ≈ DV01.
ActuaryUtilities.FinancialMath.DV01 — Type
DV01 <: DurationDollar Value of 01. The dollar change in value for a 1 basis point (0.01%) parallel shift in rates.
DV01 = -∂V/∂r / 10000, so a DV01 of 0.045 means the position loses $0.045 per $100 notional for a 1bp rate increase.
ActuaryUtilities.FinancialMath.Effective — Type
Effective <: DurationEffective (rate) duration / convexity for a curve-dependent contract (e.g. a floating-rate bond): reprices under a shifted curve with the projected cashflows RE-COMPUTED, so a floating coupon re-fixes. The correct interest-rate duration for floating-rate instruments; Modified/Macaulay are valid only for curve-independent (fixed) cashflows. duration(Effective(), contract, curve, tenors).
See also: Spread, sensitivities, locked_floater.
ActuaryUtilities.FinancialMath.IR01 — Type
IR01 <: DurationInterest Rate 01. The dollar change in value for a 1 basis point parallel shift in the risk-free (base) curve, holding the credit spread constant.
Requires both a base curve and credit spread to be specified. For a flat additive decomposition, IR01 ≈ CS01 ≈ DV01.
ActuaryUtilities.FinancialMath.KeyRate — Type
KeyRate(timepoints,shift=0.001)A convenience constructor for KeyRateZero.
Extended Help
KeyRateZero is chosen as the default constructor because it has more attractive properties than KeyRatePar:
- rates after the key
timepointremain unaffected by theshift- e.g. shifting the 5-year par rate would (incorrectly) give a 6-year zero coupon bond a negative key rate duration, while a 5-year zero-rate shift leaves it unaffected
ActuaryUtilities.FinancialMath.KeyRatePar — Type
KeyRatePar(timepoint,shift=0.001) <: KeyRateDurationShift the par curve by the given amount at the given timepoint. Use in conjunction with duration to calculate the key rate duration.
Unlike other duration statistics which are computed using analytic derivatives, KeyRateDurations are computed via a shift-and-compute the yield curve approach.
KeyRatePar is more commonly reported (than KeyRateZero) in the fixed income markets, even though KeyRateZero has more analytically attractive properties. See the discussion of KeyRateDuration in the FinanceModels.jl docs.
ActuaryUtilities.FinancialMath.KeyRateZero — Type
KeyRateZero(timepoint,shift=0.001) <: KeyRateDurationShift the zero curve by the given amount at the given timepoint. Use in conjunction with duration to calculate the key rate duration.
Unlike other duration statistics which are computed using analytic derivatives, KeyRateDuration is computed via a shift-and-compute the yield curve approach.
KeyRateZero is less commonly reported (than KeyRatePar) in the fixed income markets, even though zero-curve shifts have more analytically attractive properties (rates beyond the shifted timepoint are unaffected). See the discussion of KeyRateDuration in the FinanceModels.jl docs.
ActuaryUtilities.FinancialMath.KeyRates — Type
KeyRates(tenors) <: DurationMarker type carrying the key-rate knot grid tenors for use with duration, convexity, and sensitivities. Requests the full key-rate decomposition (vector of durations, matrix of convexities) instead of the default scalar summary.
tenors is any AbstractVector{<:Real} of positive knot times. The knot grid is carried with the measurement intent — "key rate durations at these tenors" lives in one object.
tenors = [1.0, 2.0, 5.0, 10.0, 30.0]
duration(KeyRates(tenors), curve, cfs, times) # vector of key rate durations
duration(DV01(), KeyRates(tenors), curve, cfs, times) # vector of key rate DV01s
convexity(KeyRates(tenors), curve, cfs, times) # matrix of key rate convexities
sensitivities(KeyRates(tenors), curve, cfs, times) # value + durations + convexitiesActuaryUtilities.FinancialMath.Spread — Type
Spread <: DurationSpread (credit) duration: bumps the discount curve only, holding the projected (index) cashflows fixed. For a floating-rate bond this is ≈ time to maturity — the discount-margin / credit sensitivity.
See also: Effective, sensitivities.
ActuaryUtilities.FinancialMath.breakeven — Function
breakeven(yield, cashflows::Vector)
breakeven(yield, cashflows::Vector,times::Vector)Calculate the time when the accumulated cashflows breakeven given the yield.
Assumptions:
- cashflows occur at the end of the period
- cashflows evenly spaced with the first one occuring at time zero if
timesnot given
Returns nothing if cashflow stream never breaks even.
julia> breakeven(0.10, [-10,1,2,3,4,8])
5
julia> breakeven(0.10, [-10,15,2,3,4,8])
1
julia> breakeven(0.10, [-10,-15,2,3,4,8]) # returns the `nothing` value
ActuaryUtilities.FinancialMath.convexity — Method
convexity(yield,cfs,times)
convexity(yield,valuation_function)Calculates the convexity. - yield should be a fixed effective yield (e.g. 0.05). - times may be omitted and it will assume cfs are evenly spaced beginning at the end of the first period.
Empty collections and collections whose amounts are all exactly zero return zero by convention, without evaluating the curve. Every cashflow needs a time; unused trailing times are ignored. See Zero cashflow streams for numeric types and zero-net-value portfolios. Valuation-function forms are unchanged.
Examples
Using vectors of cashflows and times
julia> times = 1:5
julia> cfs = [0,0,0,0,100]
julia> duration(0.03,cfs,times)
4.854368932038834
julia> duration(Macaulay(),0.03,cfs,times)
5.0
julia> duration(Modified(),0.03,cfs,times)
4.854368932038835
julia> convexity(0.03,cfs,times)
28.277877274012635
Using any given value function:
julia> lump_sum_value(amount,years,i) = amount / (1 + i ) ^ years
julia> my_lump_sum_value(i) = lump_sum_value(100,5,i)
julia> duration(0.03,my_lump_sum_value)
4.854368932038835
julia> convexity(0.03,my_lump_sum_value)
28.277877274012642
ActuaryUtilities.FinancialMath.dv01 — Method
dv01(args...)Dollar value of a 1bp move. dv01(args...) ≡ duration(DV01(), args...) for the cashflow/curve forms, with dv01(Effective()/Spread(), target, [forward, credit,] tenors) giving the floating-rate dollar durations (years × value ÷ 10⁴).
ActuaryUtilities.FinancialMath.locked_floater — Method
locked_floater(fl::FinanceModels.Bond.Floating, current_coupon, next_reset)Model an in-force floater whose current coupon is LOCKED at current_coupon (the per-period amount fixed at the last reset) until next_reset, after which it floats. A Composite of a coupon-only stub at next_reset plus a forward-starting floater for the remainder (principal rides the forward leg). Gives the conventional rate duration ≈ time to next reset; without it the idealized effective duration is ≈ 0 at a reset.
The remaining term fl.maturity - next_reset must be an integer number of coupon periods. Otherwise the forward-starting leg would carry a stub first coupon whose fix-in-advance reference rate looks back before time zero — a quietly mispriced quantity on curves that extrapolate below $t = 0$ and a DomainError on ZeroRateCurve — so non-commensurate inputs throw an ArgumentError.
ActuaryUtilities.FinancialMath.moic — Method
moic(cashflows<:AbstractArray)The multiple on invested capital ("moic") is the un-discounted sum of distributions divided by the sum of the contributions. The function assumes that negative numbers in the array represent contributions and positive numbers represent distributions.
Examples
julia> moic([-10,20,30])
5.0ActuaryUtilities.FinancialMath.present_values — Function
present_values(interest, cashflows, timepoints)Efficiently calculate a vector representing the present value of the given cashflows at each period prior to the given timepoint.
Empty collections return an empty vector. Collections whose amounts are all exactly zero return a vector of positive zeros without evaluating the curve; the element type comes from the amounts and timepoints. Every cashflow requires a time; additional trailing times are ignored.
Examples
julia> present_values(0.00, [1,1,1])
3-element Vector{Float64}:
3.0
2.0
1.0
julia> present_values(0.05, [10,10,110], [1,2,3])
3-element Vector{Float64}:
113.61624014685238
109.297052154195
104.76190476190476ActuaryUtilities.FinancialMath.price — Method
price(...)The absolute value of the present_value(...).
Extended help
Using price can be helpful if the directionality of the value doesn't matter. For example, in the common usage, duration is more interested in the change in price than present value, so price is used there.
ActuaryUtilities.FinancialMath.reproject — Method
reproject(contract, index_curve)Wrap contract so its coupons are estimated off index_curve: returns the contract itself if it reads no model, else a Projection mapping the contract's keys to index_curve. Lets a multi-curve valuation avoid hand-writing the model Dict.
ActuaryUtilities.FinancialMath.sensitivities — Method
sensitivities(valuation, curves::NamedTuple; tenors) -> (; value, duration, dv01, key_rate)
sensitivities(target, tenors; discount::NamedTuple, index) -> sameMulti-curve sensitivities: differentiate valuation(curves) w.r.t. each named curve in curves in a single AD pass, returning a per-role duration/dv01/key_rate NamedTuple. The structured form assembles discount = sum(discount layers) and projects the contract's coupons on index — e.g. discount = (; rf, credit, ilp) gives r.duration.rf (≈ IR01), .credit (≈ CS01), .ilp ("ILP01"), and .index (the reset sensitivity). ILP / matching-adjustment / basis are just additional named curves.
ActuaryUtilities.FinancialMath.sensitivities — Method
sensitivities(kr::KeyRates, valuation_fn, curve::AbstractYieldModel) -> NamedTuple
sensitivities(kr::KeyRates, curve::AbstractYieldModel, cfs, times) -> NamedTuple
sensitivities(::DV01, kr::KeyRates, valuation_fn, curve::AbstractYieldModel) -> NamedTuple
sensitivities(kr::KeyRates, base::AbstractYieldModel, credit::AbstractYieldModel, cfs, times) -> NamedTuple
sensitivities(::DV01, kr::KeyRates, base, credit, cfs, times) -> NamedTupleBundled value + key-rate durations (or DV01s) + convexity matrix for any AbstractYieldModel or pair, in a single AD pass. The knot grid is carried by KeyRates.
Empty collections and collections whose amounts are all exactly zero have zero value and dollar risk; normalized duration and convexity are zero by convention. Every cashflow needs a time; unused trailing times are ignored. Shapes are preserved, and the curve is not evaluated: zero cashflows need no discount factors. Value types come from the amounts and times, with the tenor grid also participating in risk-result types. Unlike nonzero streams, these result types do not incorporate the curve's numeric type; abstractly typed empty inputs fall back to Float64. The zero check includes automatic-differentiation partials.
Nonzero amounts that offset to zero present value retain dollar exposures and have undefined normalized risk (NaN/Inf). Valuation-function and contract forms retain their existing behavior. For portfolio risk, sum values and dollar derivatives before normalizing once; averaging individual normalized durations is not equivalent. See Zero cashflow streams for batch numeric types and simulation RNG behavior.
ActuaryUtilities.FinancialMath.sensitivities — Method
sensitivities(target, curve, tenors) -> NamedTuple
sensitivities(target, forward, credit, tenors) -> NamedTupleOne-AD-pass bundle for a (possibly curve-dependent) target — a FinanceModels contract or a vector of contracts (portfolio) — re-projecting cashflows under bumped curves. Coupons are estimated on forward, discounted on credit (pass a single curve for both). Returns, over the tenors key-rate grid:
valueeffective_duration/effective_dv01/effective_key_rate— bump both curves (coupons re-fix): the interest-rate duration (≈ next reset for a floater).spread_duration/spread_dv01/spread_key_rate— bump the discount only: the discount-margin / credit duration (≈ maturity for a floater).forward_duration/forward_dv01/forward_key_rate— bump the index only;effective = forward + spread(first order).
Durations in years; DV01s in dollars per 1bp. For a fixed bond effective == spread == the modified duration and forward == 0. See duration with Effective/ Spread, dv01, zspread, locked_floater.
ActuaryUtilities.FinancialMath.spread — Function
spread(curve1,curve2,cashflows)Return the solved-for constant spread to add to curve1 in order to equate the discounted cashflows with curve2
The spread is found via a damped Newton iteration on the pricing residual and is solved to machine precision; an ErrorException is thrown if the solve does not converge within maxiter iterations.
For mixed-sign cashflows the pricing residual can have more than one exact root (e.g. a duration-neutral asset/liability pair); the root reached from a starting spread of zero is returned.
Examples
julia> spread(0.04, 0.05, fill(10.0, 10))
Periodic(0.010000000000000009, 1)ActuaryUtilities.FinancialMath.zspread — Method
zspread(contract, credit, market_price; forward=credit) -> (; zspread, zspread_dv01)Constant continuously-compounded spread s on the credit (discount) curve such that the model price equals market_price, with coupons estimated on forward (held fixed). Returns the spread and its sensitivity ($/1bp parallel move of credit + s). Newton + AD.
ActuaryUtilities.duration — Method
duration(keyrate::KeyRateDuration,curve,cashflows)
duration(keyrate::KeyRateDuration,curve,cashflows,timepoints)
duration(keyrate::KeyRateDuration,curve,cashflows,timepoints,krd_points)Calculate the key rate duration by shifting the zero (not par) curve by the kwarg shift at the timepoint specified by a KeyRateDuration(time).
The approach is to carve up the curve into krd_points (default is the unit steps between 1 and the last timepoint of the casfhlows). The zero rate corresponding to the timepoint within the KeyRateDuration is shifted by shift (specified by the KeyRateZero or KeyRatePar constructors. A new curve is created from the shifted rates. This means that the "width" of the shifted section is ± 1 time period, unless specific points are specified via krd_points.
The curve may be any FinanceModels.jl curve (e.g. does not have to be a curve constructed via FinanceModels.Zero(...)).
Due to the paucity of examples in the literature, this feature does not have unit tests like the rest of JuliaActuary functionality. Additionally, the API may change in a future major/minor version update.
Examples
julia> riskfree_maturities = [0.5, 1.0, 1.5, 2.0];
julia> riskfree = [0.05, 0.058, 0.064,0.068];
julia> rf_curve = FinanceModels.Zero(riskfree,riskfree_maturities);
julia> cfs = [10,10,10,10,10];
julia> duration(KeyRate(1),rf_curve,cfs)
8.932800152336995
Extended Help
Key Rate Duration is not a well specified topic in the literature and in practice. The reference below suggest that shocking the par curve is more common in practice, but that the zero curve produces more consistent results. Future versions may support shifting the par curve.
References:
- Quant Finance Stack Exchange: To compute key rate duration, shall I use par curve or zero curve?
- (Financial Exam Help 123](http://www.financialexamhelp123.com/key-rate-duration/)
ActuaryUtilities.duration — Method
duration(CS01(), base_curve, credit_spread, cfs, times)
duration(CS01(), base_curve, credit_spread, cfs)Calculate the CS01 (Credit Spread 01): the dollar change in value for a 1 basis point parallel shift in the credit spread, holding the risk-free (base) curve constant.
The total discount rate is assumed to be base_curve + credit_spread. For a flat additive decomposition (e.g. scalar rates), CS01 ≈ IR01 ≈ DV01.
Examples
julia> cfs = [5, 5, 5, 105];
julia> times = 1:4;
julia> duration(CS01(), 0.03, 0.02, cfs, times)
0.035459505041623596
julia> duration(CS01(), 0.03, 0.02, cfs, times) ≈ duration(DV01(), 0.05, cfs, times)
trueActuaryUtilities.duration — Method
duration(::DV01, valuation_fn, curve::AbstractYieldModel, tenors) -> scalar
duration(::DV01, curve::AbstractYieldModel, tenors, cfs, times) -> scalar
duration(::DV01, kr::KeyRates, valuation_fn, curve::AbstractYieldModel) -> Vector
duration(::DV01, kr::KeyRates, curve::AbstractYieldModel, cfs, times) -> VectorDV01 (scalar or per-knot vector) for any AbstractYieldModel. Equivalent to the KeyRates variants of duration but in dollars per basis point.
ActuaryUtilities.duration — Method
duration(Effective(), target, curve, tenors) # rate duration, yrs
duration(Spread(), target, curve, tenors) # spread duration, yrs
duration(Effective(), KeyRates(tenors), target, curve) # key-rate vector
dv01(Effective()/Spread(), target, curve, tenors) # the dollar versionsEffective (rate) and spread (credit) duration / DV01 for a contract or portfolio, re-projecting cashflows under bumped curves. Two-curve forms take (forward, credit). See sensitivities for the full one-pass bundle.
ActuaryUtilities.duration — Method
duration(valuation_fn, curve::AbstractYieldModel, tenors) -> scalar
duration(curve::AbstractYieldModel, tenors, cfs, times) -> scalar
duration(curve::AbstractYieldModel, tenors, cfs::AbstractVector{<:Cashflow}) -> scalarScalar modified duration for any AbstractYieldModel evaluated against a KRD knot grid. Equivalent to sum(duration(KeyRates(tenors), ...)).
Use KeyRates to obtain the per-knot vector decomposition.
Example
duration(pv, my_composite_curve, [0.25, 1, 5, 10, 30])ActuaryUtilities.duration — Method
duration(::IR01, valuation_fn, base::AbstractYieldModel, credit::AbstractYieldModel, tenors) -> scalar
duration(::IR01, base::AbstractYieldModel, credit::AbstractYieldModel, tenors, cfs, times) -> scalar
duration(::IR01, kr::KeyRates, valuation_fn, base, credit) -> Vector
duration(::IR01, kr::KeyRates, base, credit, cfs, times) -> Vector
duration(::CS01, ...) -> ...Two-curve IR01/CS01 for any AbstractYieldModel pair sharing a tenor grid. IR01 bumps the base (risk-free) curve only; CS01 bumps the credit (spread) curve only.
ActuaryUtilities.duration — Method
duration(IR01(), base_curve, credit_spread, cfs, times)
duration(IR01(), base_curve, credit_spread, cfs)Calculate the IR01 (Interest Rate 01): the dollar change in value for a 1 basis point parallel shift in the risk-free (base) curve, holding the credit spread constant.
The total discount rate is assumed to be base_curve + credit_spread. For a flat additive decomposition (e.g. scalar rates), IR01 ≈ CS01 ≈ DV01.
Examples
julia> cfs = [5, 5, 5, 105];
julia> times = 1:4;
julia> duration(IR01(), 0.03, 0.02, cfs, times)
0.035459505041623596
julia> duration(IR01(), 0.03, 0.02, cfs, times) ≈ duration(DV01(), 0.05, cfs, times)
trueActuaryUtilities.duration — Method
duration(kr::KeyRates, valuation_fn, curve::AbstractYieldModel) -> Vector
duration(kr::KeyRates, curve::AbstractYieldModel, cfs, times) -> Vector
duration(kr::KeyRates, curve::AbstractYieldModel, cfs::AbstractVector{<:Cashflow}) -> VectorKey-rate durations (modified) for any AbstractYieldModel, computed by layering a triangular-hat zero-rate bump at each tenor in kr.tenors over the user's curve via Yield.TenorShift, then taking the AD gradient w.r.t. the bump magnitudes. The user's curve is preserved at all non-knot points.
Empty collections and collections whose amounts are all exactly zero return zero key-rate durations by convention, with one entry per tenor and no curve evaluation. Every cashflow needs a time; unused trailing times are ignored. See Zero cashflow streams for numeric types and zero-net-value portfolios.
Tenor grid
kr.tenors is the KRD knot grid — a separate modeling choice from any tenor structure baked into the curve itself. You can evaluate key-rate durations on any grid (e.g. Bloomberg {0.25, 1, 2, 5, 10, 30}, FRTB {0.25, 0.5, 1, 2, 3, 5, 10, 15, 20, 30}, etc.) without re-fitting the underlying curve.
The grid must be nonempty, sorted ascending, distinct, and strictly positive. The KeyRates constructor validates these requirements.
Bump shape and endpoint extrapolation
The bump at the i-th knot is a triangular hat centered at tenors[i] with support [tenors[i-1], tenors[i+1]]. Outside the knot range it is flat: bumping tenors[1] perturbs all cashflows at t ≤ tenors[1] equally, and bumping tenors[end] perturbs all cashflows at t ≥ tenors[end] equally. For long-duration insurance liabilities (LTC, deferred / payout annuities), the last-knot KRD absorbs all super-tenor sensitivity — extend the grid past your longest cashflow if you want that decomposed.
For a linearly-interpolated zero-rate curve the result matches AD over the curve's own rates exactly. For other splines the bump kernel is hat-shaped rather than spline-shaped, so per-knot KRDs shift slightly; the sum of KRDs (= scalar modified duration) is invariant either way.
Example
duration(KeyRates([0.25, 1, 5, 10, 30]), pv, curve)
duration(KeyRates([0.25, 1, 5, 10, 30]), curve) do c
pv(c)
endActuaryUtilities.duration — Method
duration(Macaulay(),interest_rate,cfs,times)
duration(Modified(),interest_rate,cfs,times)
duration(DV01(),interest_rate,cfs,times)
duration(IR01(),base_curve,credit_spread,cfs,times)
duration(CS01(),base_curve,credit_spread,cfs,times)
duration(interest_rate,cfs,times) # Modified Duration
duration(interest_rate,valuation_function) # Modified DurationCalculates the Macaulay, Modified, DV01, IR01, or CS01 duration. times may be ommitted and the valuation will assume evenly spaced cashflows starting at the end of the first period.
cfs can be an AbstractVector{<:Cashflow} (from FinanceCore), in which case times is extracted automatically and should be omitted.
Empty collections and collections whose amounts are all exactly zero return zero risk without evaluating the curve. Every cashflow needs a time; unused trailing times are ignored. See Zero cashflow streams for the normalization convention, numeric types, and zero-net-value portfolios. Valuation-function forms are unchanged.
When not given Modified() or Macaulay() as an argument, will default to Modified().
- Modified duration: the relative change per point of yield change.
- Macaulay: the cashflow-weighted average time.
- DV01: the absolute change per basis point (hundredth of a percentage point).
- IR01: the absolute change per basis point shift in the risk-free (base) curve, holding credit spread constant.
- CS01: the absolute change per basis point shift in the credit spread, holding the risk-free (base) curve constant.
Periodicity convention
The Modified duration returned depends on the space in which the parallel rate shock is applied, and this differs between plain rates and yield models:
- A scalar (e.g.
0.04) or aRateis shocked in its own compounding space. A scalar is treated asPeriodic(0.04, 1), so Modified = Macaulay / (1 + 0.04); in general aPeriodic(y, m)rate gives Modified = Macaulay / (1 + y/m), and aContinuous(y)rate gives Modified = Macaulay. - A yield model (e.g.
Yield.Constant(0.04)from FinanceModels) composes the shock in continuous-zero space, so Modified = Macaulay under the curve's own discounting, regardless of the compounding convention stored in the model.
The same inputs therefore produce two different numbers by design:
julia> times = 1:5; cfs = [0,0,0,0,100];
julia> duration(0.04, cfs, times) # Periodic(1) shock: Macaulay / 1.04
4.8076923076923075
julia> duration(Yield.Constant(0.04), cfs, times) # continuous-zero shock: Macaulay
5.0Examples
Using vectors of cashflows and times
julia> times = 1:5;
julia> cfs = [0,0,0,0,100];
julia> duration(0.03,cfs,times)
4.854368932038835
julia> duration(Periodic(0.03,1),cfs,times)
4.854368932038835
julia> duration(Continuous(0.03),cfs,times)
5.0
julia> duration(Macaulay(),0.03,cfs,times)
5.0
julia> duration(Modified(),0.03,cfs,times)
4.854368932038835
julia> convexity(0.03,cfs,times)
28.277877274012635
Using any given value function:
julia> lump_sum_value(amount,years,i) = amount / (1 + i ) ^ years
julia> my_lump_sum_value(i) = lump_sum_value(100,5,i)
julia> duration(0.03,my_lump_sum_value)
4.854368932038835
julia> convexity(0.03,my_lump_sum_value)
28.277877274012642
Unexported API
ActuaryUtilities.FinancialMath._tent_bump — Method
_tent_bump(shift, τ, krd_points)Return a closure (z, t) -> z + Continuous(bump) implementing the Ho (1992) tent function for key-rate duration bump-and-reprice:
- First KRD point: flat
shiftfort ≤ τ, linear ramp to 0 at next neighbor. - Last KRD point: linear ramp from 0 at previous neighbor, flat
shiftfort ≥ τ. - Interior: triangle with peak
shiftatτ, zero at both neighbors.