Financial Math Submodule
Calculate present values, duration, convexity, and related financial measures.
Quickstart
using ActuaryUtilities
cfs = [5, 5, 105]
times = [1, 2, 3]
discount_rate = 0.03
present_value(discount_rate, cfs, times) # 105.66
duration(Macaulay(), discount_rate, cfs, times) # 2.86
duration(discount_rate, cfs, times) # 2.78
convexity(discount_rate, cfs, times) # 10.63See Convexity Conventions for rate-shock definitions and worked examples.
Use Macaulay and Modified for fixed cashflows. Pass contracts or portfolios directly, after the curve, to reproject coupons under curve shocks. Effective() selects rate risk; Spread() selects spread risk. sensitivities returns the discount and index exposures in one calculation. See Interest-Rate Sensitivities for examples and multiple curves.
Zero cashflow streams
Empty and all-zero cashflow collections return zero value and risk. Normalized duration and convexity are zero by convention. This applies to scalar, key-rate, 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. Too few times throws DimensionMismatch. Amounts and times pair by position from their first entries, so offset vectors pair too. 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 |
Scalar DV01 differentiates signed value directly. It remains defined at zero present value when the valuation has a finite derivative; IR01 and CS01 use the same calculation. Normalized duration and convexity are still undefined there.
julia> using ActuaryUtilities, FinanceModels, FinanceCore
julia> curve = Yield.Constant(Continuous(0.0));
julia> cfs = [-1.0, 1.0]; times = [0.0, 1.0];
julia> pv(curve, cfs, times)
0.0
julia> (duration(DV01(), curve, cfs, times),
duration(c -> pv(c, cfs, times), DV01(), curve),
duration((b, c) -> pv(b + c, cfs, times), IR01(), curve, curve),
duration((b, c) -> pv(b + c, cfs, times), CS01(), curve, curve))
(0.0001, 0.0001, 0.0001, 0.0001)
julia> !isfinite(duration(curve, cfs, times))
trueThe zero check uses exact iszero on amounts, including AD partials. Both 0.0 and -0.0 count as zero; tiny nonzero amounts do not. Assigned zero duration is a convention, not the limit as amounts shrink. Explicit tenor grids are still validated.
Zero streams value no payments and skip Hull–White simulation. Their value is zero by linearity; its numeric type is a convention: the type the same measure returns for a nonempty stream with the same amount, time, and rate or curve types (plus the tenor grid for key-rate risk). The rate or curve is queried once, at time zero, for that type, so a curve that cannot be evaluated at time zero cannot value a zero stream either. An untyped empty collection (Any[], Cashflow[], ()) takes its type from the rate or curve.
Valuation does not advance the RNG passed to Scenarios: it draws its seed when constructed, and every valuation with it, zero streams included, reuses that seed.
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 is not portfolio duration. Callback and contract forms do not use this convention: a zero value from them gives undefined normalized risk.
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 + Rate adds in the left operand's convention. With a continuous z, adding Periodic(0.01, 1) adds its continuous equivalent, log(1.01), which multiplies the accumulation factor by 1.01. To add 100 bp in annual-effective terms, convert, add and convert back:
shifted_p = base + (z, t) -> z + Periodic(0.01, 1) # adds log(1.01)
shifted_ae = base + (z, t) -> Continuous(Periodic(1)(z) + 0.01) # +100 bp annual effective
zero(shifted_p, 5.0), zero(shifted_ae, 5.0)(Continuous(0.05995033085316809), Continuous(0.059467337245390814))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)Fitted 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)
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
CS01Credit Spread 01: signed dollar risk for a one-basis-point parallel shift in the credit curve, holding the risk-free (base) curve fixed, of a valuation callback that receives (base, credit): duration(valuation, CS01(), base, credit).
For fixed cashflows discounted at base + credit, CS01, IR01 and the combined curve's DV01 are equal, so use duration(DV01(), base + credit, cfs, times). For contracts, use Effective() and Spread().
ActuaryUtilities.FinancialMath.DV01 — Type
DV01Signed dollar risk for a one-basis-point (0.01%) parallel rate shift: DV01 = -∂V/∂r / 10000. A positive DV01 of 0.045 means a 1bp rate increase reduces the position's value by approximately 0.045, in the cashflows' currency units.
ActuaryUtilities.FinancialMath.DollarConvexity — Type
DollarConvexity()Select dollar convexity in convexity: the signed second derivative of value, $H_{ij} = ∂^2V/∂s_i∂s_j$, in the shock coordinates of the other measures. It has no factor of one-half and no basis-point scaling, so its unit is currency per squared unit of decimal rate. Normalized convexity is $H / V$; dollar convexity is defined at zero value, keeps the position's sign and adds across positions. A shift of b basis points changes value by about -dv01 * b + H / 2 * (b / 10_000)^2, and H * 1e-8 is the second derivative per squared basis point.
convexity(DollarConvexity(), curve, cfs, times) # a number
convexity(DollarConvexity(), KeyRates(tenors), curve, cfs, times) # a matrix
convexity(valuation, DollarConvexity(), [KeyRates(tenors),] curve)
convexity(valuation, DollarConvexity(), [KeyRates(tenors),] base, credit) # blocks keyed by role
convexity(DollarConvexity(), [Effective() | Spread(),] [KeyRates(tenors),] discount, contract; index)Fixed cashflows use analytic derivatives, and the parallel form also takes a scalar or Rate. Contracts default to Effective(), whose parallel dollar convexity includes the cross terms between the index and discount curves. Second-order sensitivities return the same derivatives as their dollar_convexity field, also for named curves, market inputs and Scenarios. Empty and all-zero cashflow streams return typed zeros.
See also SecondOrder, DV01.
ActuaryUtilities.FinancialMath.Effective — Type
EffectiveMeasure contract risk while reprojecting cashflows under shifted curves, so floating coupons reset: duration(Effective(), discount, contract; index = discount) shifts both the index and the discount curve. The same marker applies to duration(DV01(), …) and convexity. Modified and Macaulay operate on fixed cashflows.
See also: Spread, sensitivities, locked_floater.
ActuaryUtilities.FinancialMath.FirstOrder — Type
FirstOrder()Select first-order sensitivities: (; value, duration, dv01). It is the default.
See also: SecondOrder.
ActuaryUtilities.FinancialMath.IR01 — Type
IR01Interest Rate 01: signed dollar risk for a one-basis-point parallel shift in the risk-free (base) curve, holding the credit curve fixed, of a valuation callback that receives (base, credit): duration(valuation, IR01(), base, credit).
For fixed cashflows discounted at base + credit, IR01, CS01 and the combined curve's DV01 are equal, so use duration(DV01(), base + credit, cfs, times). For contracts, use Effective() and Spread().
ActuaryUtilities.FinancialMath.KeyRates — Type
KeyRates(tenors)Select the tenor grid for key-rate duration, convexity, and sensitivities. Results contain per-tenor vectors and convexity matrices. tenors must be a nonempty AbstractVector{<:Real} of finite, positive, strictly increasing knot times in years. KeyRates keeps its own 1-based copy of them.
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 and DV01s
sensitivities(SecondOrder(), KeyRates(tenors), curve, cfs, times) # and the convexity matrixActuaryUtilities.FinancialMath.Scenarios — Type
Scenarios(hw::HullWhite; horizon, n_scenarios = 1000, timestep = 1/12, rng = Random.default_rng())Simulated Hull–White short-rate paths for sensitivities. Curve shocks move hw.curve, the model recalibrates its drift to each shocked curve, and the valuation sees the paths simulated from it; mean reversion and volatility stay fixed. Without Scenarios, hw is a curve like any other: a bump moves hw.curve, and caps and swaptions keep their closed forms.
s = Scenarios(hw; horizon = 10.0, n_scenarios = 500, rng = Xoshiro(42))
sensitivities(KeyRates(tenors), s, cfs, times) # mean present value across paths
sensitivities(KeyRates(tenors), s) do paths # a valuation of the vector of paths
sum(pv(p, cfs, times) for p in paths) / length(paths)
endThe forms and results are those of a curve: an optional FirstOrder or SecondOrder marker, then an optional KeyRates grid.
horizon is required. It must be a whole number of timesteps, up to floating-point rounding (FinanceModels.simulation_steps decides), or the constructor throws ArgumentError; the paths end there, and a payment after horizon throws. The constructor fixes the time grid and draws one seed from rng, and nothing else uses rng. Every valuation with the same Scenarios simulates the same paths from that seed, in every automatic-differentiation evaluation too, so value and derivatives share the same draws, and results add across calls: values and dollar derivatives add, and normalized duration and convexity add when weighted by value. Empty and all-zero cashflow streams are valued on hw.curve without simulating.
ActuaryUtilities.FinancialMath.SecondOrder — Type
SecondOrder()Select second-order sensitivities: (; value, duration, dv01, convexity, dollar_convexity), from one evaluation of the value and its first and second derivatives. A valuation that has no second derivative throws its own error; there is no fallback to first order.
This marker belongs to ActuaryUtilities and is unrelated to DifferentiationInterface.SecondOrder.
See also: FirstOrder.
ActuaryUtilities.FinancialMath.Spread — Type
SpreadSpread (credit) duration: duration(Spread(), discount, contract; index = discount) shifts the discount curve only, so cashflows projected on the index curve stay fixed. For a floating-rate bond it is close to the duration of a fixed-rate bond with the same maturity.
See also: Effective, sensitivities.
ActuaryUtilities.FinancialMath.breakeven — Method
breakeven(yield, cashflows::Vector)
breakeven(yield, cashflows::Vector,times::Vector)Return the payment time from which the balance accumulated at yield stays nonnegative, or nothing if it never does. Without times, cashflow k is paid at time k - 1.
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(valuation_fn, kr::KeyRates, curve::AbstractYieldModel) -> Matrix
convexity(kr::KeyRates, curve::AbstractYieldModel, cfs, times) -> Matrix
convexity(valuation_fn, base::AbstractYieldModel, credit::AbstractYieldModel) -> NamedTuple
convexity(valuation_fn, kr::KeyRates, base, credit) -> NamedTupleReturn normalized convexity for a yield model or a pair of curves. Matrix entries are (∂²V/∂rᵢ∂rⱼ) / V; for ∂²V/∂rᵢ∂rⱼ itself, put DollarConvexity after the valuation. For a single curve's scalar parallel convexity, use convexity(curve, cfs, times) or convexity(valuation_fn, curve).
Empty and all-zero cashflow streams return zero convexity in the usual shape; see Zero cashflow streams.
The two-curve forms return the blocks keyed by role, (; base = (; base, credit), credit = (; base, credit)), the convexity field of sensitivities with SecondOrder: each block is (∂²V/∂sᵢ∂sⱼ) / V for continuous-zero shifts of the named curves. The parallel blocks are numbers, ≈ the sums of the KeyRates blocks, and base.credit == credit.base. The KeyRates blocks are matrices, and credit.base is the transpose of base.credit, which need not be symmetric. For decimal parallel shifts u and v, the second-order P&L is V / 2 * (base.base * u^2 + 2 * base.credit * u * v + credit.credit * v^2). For fixed cashflows discounted at base + credit, all four blocks equal convexity(base + credit, cfs, times) when the value is nonzero. Shifting both curves by one basis point shifts their combined continuous zero rates by two basis points. See Two-curve convexity blocks. Use sensitivities with SecondOrder to also obtain the value, durations and DV01s from the same derivatives.
ActuaryUtilities.FinancialMath.duration — Method
duration(Effective(), discount, contract; index = discount) # rate duration, yrs
duration(Spread(), discount, contract; index = discount) # spread duration, yrs
duration(Effective(), KeyRates(tenors), discount, contract; index = discount) # vector
duration(DV01(), Effective(), discount, contract; index = discount) # the dollar versions
convexity(Effective(), discount, contract; index = discount) # and the convexities
duration(discount, contract; index = discount) # Effective() by default
duration(DV01(), [KeyRates(tenors),] discount, contract; index = discount)
convexity(discount, contract; index = discount)Effective (rate) and spread (credit) duration, DV01 and convexity for a contract or a portfolio (a vector of contracts), reprojecting cashflows under continuous-zero shifts. Coupons project on the index curve, which defaults to discount, and every payment is discounted on discount:
Effective()shifts both curves, so floating coupons reset.Spread()shifts the discount curve only, so projected coupons stay fixed.
Markers come in the order DV01(), then Effective() or Spread(), then KeyRates(tenors). Without Effective() or Spread(), contract and portfolio measures use Effective(). The parallel forms take no tenor grid; KeyRates(tenors) gives per-tenor vectors and convexity matrices. Effective convexity includes the cross terms between the curves: for the parallel second derivatives Cᵢᵢ, Cᵢd, Cdᵢ and Cdd of the index and discount roles, it is their sum, and spread convexity is Cdd. A portfolio's value and dollar derivatives are summed before normalizing. See sensitivities for the roles separately, zspread and locked_floater.
ActuaryUtilities.FinancialMath.duration — Method
duration(Macaulay(),interest_rate,cfs,times)
duration(Modified(),interest_rate,cfs,times)
duration(DV01(),interest_rate,cfs,times)
duration(interest_rate,cfs,times) # Modified Duration
duration(valuation_function,interest_rate) # Modified Duration
duration(valuation_function,DV01(),interest_rate)Calculate Macaulay or modified duration, or signed dollar DV01. For numeric amounts, omitted times default to 1:length(cfs).
cfs can be an AbstractVector{<:Cashflow} (from FinanceCore), in which case times may be omitted. A Cashflow always supplies its embedded amount and time; explicit times supply payment times for numeric amounts. If supplied, times must still contain an entry for each cashflow; unused trailing entries are ignored.
Cashflow methods accept arrays, tuples, and finite generators, for amounts and for times. Arrays are flattened in column-major order; generators are collected once before valuation. Amounts and times pair by position, so offset vectors pair too, and omitted times are the amounts' indices. Use collect for other iterables, such as Iterators.take or skipmissing. Normalized duration is unchanged when the position sign reverses; dollar DV01 reverses sign with the position.
Empty and all-zero cashflow streams return zero risk; see Zero cashflow streams. Dollar sensitivities differentiate the signed value directly, so they stay defined at zero present value, where normalized duration is not. Callback forms do not use the zero-stream convention: a zero value from them gives undefined normalized risk.
The default measure is Modified(). With a contract or a portfolio after the curve, it is Effective(); see Effective.
- Modified duration:
-∂V/∂r / V, the relative value lost per unit increase in the shocked rate. - Macaulay: the present-value-weighted average payment time.
- DV01:
-∂V/∂r / 10000, the first-order value lost for a one-basis-point increase.
For fixed cashflows discounted at base + credit, IR01 and CS01 both equal duration(DV01(), base + credit, cfs, times). Use the IR01/CS01 callback forms when the curves play different roles.
Shock coordinates
Each single-rate input moves in its own shock coordinate. See Shock coordinates:
- Scalars are annual effective rates: Modified = Macaulay / (1 + y).
Periodic(y, m)shocks its nominal rate: Modified = Macaulay / (1 + y/m).Continuous(y)and every yield model use continuous-zero shifts: Modified = Macaulay.
Wrapping a scalar in Yield.Constant preserves its discount factors but changes the shock coordinate, so duration and DV01 change by a factor of 1 + y:
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.277877274012628
Using any given value function, passed first so that do-block syntax works:
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(my_lump_sum_value,0.03)
4.8543689320388355
julia> convexity(my_lump_sum_value,0.03)
28.277877274012642
julia> duration(0.03) do i
lump_sum_value(100,5,i)
end
4.8543689320388355
ActuaryUtilities.FinancialMath.duration — Method
duration(valuation, ::DV01, [kr::KeyRates,] base::AbstractYieldModel, credit::AbstractYieldModel) -> NamedTuple
duration(valuation, ::DV01, [kr::KeyRates,] curves::NamedTuple) -> NamedTupleSigned DV01 of a valuation callback for each curve role, (; base, credit) or keyed by the names of curves: numbers for a parallel shift of each curve, or per-tenor vectors with KeyRates. These are the dv01 fields of sensitivities. The valuation receives (base, credit) or a NamedTuple of curves. For two curves, the roles are the IR01 and CS01.
ActuaryUtilities.FinancialMath.duration — Method
duration(valuation_fn, ::DV01, kr::KeyRates, curve::AbstractYieldModel) -> Vector
duration(::DV01, kr::KeyRates, curve::AbstractYieldModel, cfs, times) -> VectorPer-knot signed DV01s for any AbstractYieldModel: the KeyRates variants of duration in dollars per basis point. Their sum is ≈ the parallel DV01, duration(DV01(), curve, cfs, times).
ActuaryUtilities.FinancialMath.duration — Method
duration(valuation_fn, ::IR01, base::AbstractYieldModel, credit::AbstractYieldModel) -> scalar
duration(valuation_fn, ::IR01, kr::KeyRates, base, credit) -> Vector
duration(valuation_fn, ::CS01, ...) -> ...Two-curve IR01 and CS01 of a valuation callback that receives (base, credit): -∂V/∂s / 10000, the first-order value lost per basis point. IR01 shifts the continuous zero rates of the base curve and holds the credit curve fixed; CS01 shifts the credit curve and holds the base curve fixed. Each is the callback's DV01 in its one curve. The scalar forms apply a parallel shift and are ≈ the sums of the KeyRates vectors.
For fixed cashflows discounted at base + credit, IR01 and CS01 both equal duration(DV01(), base + credit, cfs, times); a callback can give different values when the curves affect payments differently. These curve shifts do not recalibrate to bumped market quotes; see Two curves: IR01 and CS01 and Market Inputs.
duration(IR01(), base, credit) do b, c
present_value(b + c, cfs, times)
endActuaryUtilities.FinancialMath.duration — Method
duration(valuation_fn, kr::KeyRates, curve::AbstractYieldModel) -> Vector
duration(kr::KeyRates, curve::AbstractYieldModel, cfs, times = eachindex(cfs)) -> VectorReturn normalized key-rate durations -∂V/∂rᵢ / V for an AbstractYieldModel. Each rᵢ is a triangular continuous-zero bump at kr.tenors[i]. The base curve is used directly, without resampling or refitting.
Empty and all-zero cashflow streams return zero key-rate durations, one per tenor; see Zero cashflow streams. Every cashflow needs a time; unused trailing times are ignored. Wrapped Cashflow objects use their embedded amounts and payment times, including when explicit times are supplied. Numeric amounts use the explicit times, which default to eachindex(cfs) (periods 1:n), and pair with the amounts by position, as in duration. The same holds for every key-rate cashflow form.
Tenor grid
Choose kr.tenors independently of the curve's own knots. The grid must be nonempty, finite, positive, and strictly increasing. It is validated both at construction and when calculating sensitivities.
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]] for interior knots. Endpoint bumps stay constant beyond the grid. All sensitivity after the last tenor belongs to its bucket; extend the grid to separate exposures at longer maturities.
The hats sum to one, so the sum of key-rate durations is ≈ the parallel duration. These are sensitivities to the specified bumps, not to spline parameters.
Example
duration(pv, KeyRates([0.25, 1, 5, 10, 30]), curve)
duration(KeyRates([0.25, 1, 5, 10, 30]), curve) do c
pv(c)
endActuaryUtilities.FinancialMath.locked_floater — Method
locked_floater(fl::FinanceModels.Bond.Floating, current_coupon, next_reset)Pay the fixed coupon amount current_coupon at next_reset, then resume floating coupons. Return a Composite of that coupon and a forward-starting floater carrying the principal. Effective duration is approximately the time to the next reset.
The remaining term fl.maturity - next_reset must contain a whole number of coupon periods. Otherwise throw ArgumentError: a stub coupon would require a reference rate before time zero.
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.
A total loss (contributions only) has a moic of 0.0. With no contributions the ratio divides by zero: Inf when distributions are positive, and NaN for an empty or all-zero stream. Neither is a meaningful multiple.
Examples
julia> moic([-10,20,30])
5.0
julia> moic([-10,-20])
0.0ActuaryUtilities.FinancialMath.present_values — Method
present_values(interest, cashflows, timepoints)Return the value of remaining cashflows before each payment period. Entry k values payments k:end at the time payment k-1 is made, or time zero for k = 1. A Cashflow is paid at its own time, whatever its paired time; a number is paid at its paired time. The entries are numbers.
Empty collections return an empty vector. Collections whose amounts are all exactly zero return a vector of positive zeros without valuing any payment; the element type is the one a nonempty stream's result would have (see Zero cashflow streams). Every cashflow requires a time; additional trailing times are ignored. Amounts and times pair by position, so offset vectors pair too.
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.sensitivities — Method
sensitivities([order,] [KeyRates(tenors),] discount, contract; index = discount) -> NamedTuple
sensitivities([order,] [KeyRates(tenors),] discount::NamedTuple, contract; index) -> NamedTupleCalculate value and risk for a contract or a portfolio (a vector of contracts), reprojecting cashflows under shifted curves. Coupons project on index, which defaults to discount, and every payment is discounted on discount. The result has the shape of sensitivities for curves, with the derivative fields keyed by role:
discount: shift the discount curve only, so coupons stay fixed. This is spread (credit) risk, close to that of a fixed-rate bond with the same maturity.index: shift the index curve only, so coupons reset but are discounted as before.
Effective (rate) risk shifts both: its DV01 is dv01.discount + dv01.index, and its convexity is the sum of all four blocks of convexity (see duration with Effective). Each role is shifted separately even when index is discount, so the index exposure is explicit. For a fixed bond, index risk is zero.
With a NamedTuple of discount layers, such as (; rf, credit, ilp), every payment is discounted on their sum, and each layer is its own role next to index, which is then required. A layer named index throws an ArgumentError.
Durations are in years; DV01s are in dollars per basis point. Dollar risk uses signed value derivatives and remains defined at zero value, where normalized duration is undefined.
s = sensitivities(KeyRates(tenors), curve, floater) # s.dv01.discount, s.dv01.index
s = sensitivities(SecondOrder(), (; rf, credit), floater; index = sofr)ActuaryUtilities.FinancialMath.sensitivities — Method
sensitivities(valuation, [order,] inputs::NamedTuple) -> NamedTupleDifferentiate valuation(inputs) with respect to every element of each named input vector. Use this form when the valuation builds its own curves from market data, for example by fitting quotes or layering spreads:
sensitivities((; sofr = rates, credit = spreads)) do m
curve = fit(Spline.Linear(), OISYield.(m.sofr, tenors), Fit.Bootstrap())
present_value(curve + credit_curve(m.credit), liability)
endEach input must be an AbstractVector{<:Real}; wrap a scalar input in a one-element vector. Inputs are read by position: the valuation receives 1-based vectors of the same lengths, also for offset inputs, and the derivative vectors and matrices are 1-based. As for curves, order is FirstOrder() (the default) or SecondOrder(), and the result is (; value, duration, dv01), with convexity and dollar_convexity at second order. Each derivative field is keyed by input name, with one entry per element:
duration.name: the vector-∂V/∂xᵢ / Vdv01.name: the vector-∂V/∂xᵢ / 10000, the signed value lost for a 0.0001 increase in each element (negative when the value rises)convexity.name.other: the matrix∂²V/∂xᵢ∂yⱼ / Vdollar_convexity.name.other: the matrix∂²V/∂xᵢ∂yⱼ, defined at zero value
Dollar measures assume the inputs are rates in decimal units, so 0.0001 is one basis point, and they keep the position's sign. They remain defined at zero value, where the normalized measures are not. The bump coordinate is the one in which the inputs are expressed: an annual quoted rate is bumped as an annual rate.
The sum of duration.name or dv01.name is the measure for a shift added to every element of that input. It equals the derivative of a parallel shift only where the valuation is differentiable, which excludes interpolation kinks such as a flat Spline.MonotoneConvex interval (see FinanceModels' Sensitivities Through Calibration).
The valuation must be differentiable with ForwardDiff. At first order it runs once for the value and once more for the derivatives, which takes one forward pass per 64 input elements. A valuation that fits a curve therefore fits it twice (once more per additional 64 elements), not once per input.
See also KeyRates for sensitivities to bumps of a given curve.
ActuaryUtilities.FinancialMath.sensitivities — Method
sensitivities([order,] [KeyRates(tenors),] curve, cfs, times = eachindex(cfs)) -> NamedTuple
sensitivities(valuation, [order,] [KeyRates(tenors),] curve) -> NamedTuple
sensitivities(valuation, [order,] [KeyRates(tenors),] base, credit) -> NamedTuple
sensitivities(valuation, [order,] [KeyRates(tenors),] curves::NamedTuple) -> NamedTupleReturn value and risk from one derivative calculation. order is FirstOrder(), the default, or SecondOrder():
FirstOrder()returns(; value, duration, dv01).SecondOrder()returns(; value, duration, dv01, convexity, dollar_convexity).
value is always a number. The derivative fields are numbers for one parallel shift of the curve, which moves in its own coordinate as in duration (see Shock coordinates). With KeyRates they are per-tenor vectors, and convexity is a matrix. With several curves, the derivative fields are keyed by curve role: dv01.base and convexity.base.credit for (base, credit), and the given names for curves. Each role takes its own shock, so the cross blocks of convexity are the mixed derivatives.
duration:-∂V/∂s / Vdv01:-∂V/∂s / 10000, the first-order value lost for a one-basis-point increaseconvexity:∂²V/∂sᵢ∂sⱼ / Vdollar_convexity:∂²V/∂sᵢ∂sⱼ, defined at zero value (seeDollarConvexity)
Without KeyRates, each role's shift is differentiated directly. With them, the hats sum to one, so the parallel measures are ≈ the sums of the key-rate ones. FirstOrder() takes no second derivatives, and SecondOrder() takes the value and both derivatives from one pass. A valuation that has no second derivative throws its own error.
Fixed cashflows use analytic derivatives on one curve; valuation callbacks use AD. The valuation receives the shifted curve, the shifted (base, credit), or a NamedTuple of shifted curves with the given names. Normalized duration and convexity do not change when the position's sign does; DV01 does.
Empty and all-zero cashflow streams return zero value and risk; see Zero cashflow streams. Nonzero amounts that offset to zero present value keep their dollar risk but have undefined normalized risk (NaN/Inf); for portfolio risk, sum values and dollar derivatives before normalizing. Callback forms do not use the zero-stream convention: a zero value from them gives undefined normalized risk.
s = sensitivities(KeyRates(tenors), curve, cfs, times) # s.duration, s.dv01: vectors
s = sensitivities(SecondOrder(), curve, cfs, times) # s.convexity: a number
sensitivities(KeyRates(tenors), (; rf, credit)) do c # s.dv01.rf, s.dv01.credit
present_value(c.rf + c.credit, cfs, times)
endSee also the contract forms, sensitivities(discount, contract; index), the market-input form, sensitivities(valuation, inputs::NamedTuple), and Scenarios.
ActuaryUtilities.FinancialMath.spread — Method
spread(curve1, curve2, cashflows, times = eachindex(cashflows); tol = 1e-12, maxiter = 100)Find the constant spread to add to curve1 so the cashflows have the same present value as under curve2.
The spread is found via a damped Newton iteration on the pricing residual. A candidate is accepted once the undamped Newton step is smaller than tol in rate units (not currency) and the candidate reprices the cashflows: its pricing residual must be at most sqrt(eps) times the sum of the gross discounted values Σ|cfᵢ|⋅dfᵢ under the two curves. Both tests are unchanged by scaling the cashflows, and the gross scale is nonzero for zero-price and mixed-sign streams. A small step alone is not enough: near a combined annual rate of -100% the price derivative is so large that the step is tiny far from the root. Steps never go more than halfway to the edge of the spread's domain (the spread must exceed -1, and so must the combined rate per period when it is added to a periodic base). An ErrorException is thrown if the valuation or its derivative is NaN, or if no candidate is accepted 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.009999999999999964, 1)ActuaryUtilities.FinancialMath.zspread — Method
zspread(discount, contract, market_price; index = discount, s0 = 0.0, tol = 1e-12, maxiter = 100) -> (; zspread, zspread_dv01)Constant continuously-compounded spread s on the discount curve such that the model price equals market_price, with coupons estimated on index (held fixed). zspread is s as a Continuous rate, so discount + result.zspread is the spread curve; FinanceCore.rate(result.zspread) is the number. zspread_dv01 is a number, -∂V/∂s / 10000 at the solved spread: the value lost per basis point of s. The solve takes Newton steps from s0, a number read as continuously compounded or a Rate, with ForwardDiff derivatives.
The solve stops once a Newton step is smaller than tol in rate units (not currency), so the result does not depend on the contract's notional and is defined for a zero market_price. An ErrorException is thrown if that does not happen within maxiter steps.