Utilities submodule
Provides miscellaneous routines common in actuarial and financial work.
API
Exported API
ActuaryUtilities.duration — Function
duration(d1::Date, d2::Date)Compute the duration given two dates, which is the number of years since the first date. The interval [0,1) is defined as having duration 1. Can return negative durations if second argument is before the first.
julia> issue_date = Date(2018,9,30);
julia> duration(issue_date , Date(2019,9,30) )
2
julia> duration(issue_date , issue_date)
1
julia> duration(issue_date , Date(2018,10,1) )
1
julia> duration(issue_date , Date(2019,10,1) )
2
julia> duration(issue_date , Date(2018,6,30) )
0
julia> duration(Date(2018,9,30),Date(2017,6,30))
-1duration(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.
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
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)
trueduration(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)
trueduration(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/)
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])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.
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 sorted ascending, distinct, and strictly positive. These preconditions are not checked at runtime — a malformed grid produces wrong gradients silently.
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)
endduration(::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.
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.
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.Utilities.years_between — Function
years_between(d1::Date, d2::Date, overlap=true)Compute the number of integer years between two dates, with the first date typically before the second. Will return negative number if first date is after the second. The third argument overlap indicates whether the calendar anniversary should count as a full year (default true).
Examples
julia> d1 = Date(2018,09,30);
julia> d2 = Date(2019,09,30);
julia> d3 = Date(2019,10,01);
julia> years_between(d1,d3)
1
julia> years_between(d1,d2,false) # same month/day but `false` overlap
0
julia> years_between(d1,d2) # same month/day but `true` overlap
1
julia> years_between(d1,d2) # using default `true` overlap
1 ActuaryUtilities.Utilities.accum_offset — Function
accum_offset(x; op=*, init=1.0)A shortcut for the common operation wherein a vector is scanned with an operation, but has an initial value and the resulting array is offset from the traditional accumulate.
This is a common pattern when calculating things like survivorship given a mortality vector and you want the first value of the resulting vector to be 1.0, and the second value to be 1.0 * x[1], etc.
Two keyword arguments:
opis the binary (two argument) operator you want to use, such as*or+initis the initial value in the returned array
Examples
julia> accum_offset([0.9, 0.8, 0.7])
3-element Array{Float64,1}:
1.0
0.9
0.7200000000000001
julia> accum_offset(1:5) # the product of elements 1:n, with the default `1` as the first value
5-element Array{Int64,1}:
1
1
2
6
24
julia> accum_offset(1:5,op=+)
5-element Array{Int64,1}:
1
2
4
7
11