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.62
Floating-rate, multi-curve & portfolios

Macaulay/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.

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.059950330853168095)

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.CS01Type
CS01 <: Duration

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.

Requires both a base curve and credit spread to be specified. For a flat additive decomposition, CS01 ≈ IR01 ≈ DV01.

See also: IR01, DV01

source
ActuaryUtilities.FinancialMath.DV01Type
DV01 <: Duration

Dollar 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.

See also: IR01, CS01

source
ActuaryUtilities.FinancialMath.EffectiveType
Effective <: Duration

Effective (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.

source
ActuaryUtilities.FinancialMath.IR01Type
IR01 <: Duration

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.

Requires both a base curve and credit spread to be specified. For a flat additive decomposition, IR01 ≈ CS01 ≈ DV01.

See also: CS01, DV01

source
ActuaryUtilities.FinancialMath.KeyRateType
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 timepoint remain unaffected by the shift
    • 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
source
ActuaryUtilities.FinancialMath.KeyRateParType
KeyRatePar(timepoint,shift=0.001) <: KeyRateDuration

Shift 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.

source
ActuaryUtilities.FinancialMath.KeyRateZeroType
KeyRateZero(timepoint,shift=0.001) <: KeyRateDuration

Shift 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.

source
ActuaryUtilities.FinancialMath.KeyRatesType
KeyRates(tenors) <: Duration

Marker 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 + convexities

See also: DV01, IR01, CS01

source
ActuaryUtilities.FinancialMath.breakevenFunction
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 times not 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

source
ActuaryUtilities.FinancialMath.convexityMethod
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.

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
source
ActuaryUtilities.FinancialMath.dv01Method
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⁴).

source
ActuaryUtilities.FinancialMath.locked_floaterMethod
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.

source
ActuaryUtilities.FinancialMath.moicMethod
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.0
source
ActuaryUtilities.FinancialMath.present_valuesFunction
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.

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.76190476190476
source
ActuaryUtilities.FinancialMath.priceMethod
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.

source
ActuaryUtilities.FinancialMath.reprojectMethod
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.

source
ActuaryUtilities.FinancialMath.sensitivitiesMethod
sensitivities(valuation, curves::NamedTuple; tenors) -> (; value, duration, dv01, key_rate)
sensitivities(target, tenors; discount::NamedTuple, index) -> same

Multi-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.

source
ActuaryUtilities.FinancialMath.sensitivitiesMethod
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) -> NamedTuple

Bundled 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.

source
ActuaryUtilities.FinancialMath.sensitivitiesMethod
sensitivities(target, curve, tenors) -> NamedTuple
sensitivities(target, forward, credit, tenors) -> NamedTuple

One-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:

  • value
  • effective_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.

source
ActuaryUtilities.FinancialMath.spreadFunction
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.

Note

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)
source
ActuaryUtilities.FinancialMath.zspreadMethod
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.

source
ActuaryUtilities.durationMethod
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(...)).

Experimental

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:

source
ActuaryUtilities.durationMethod
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)
true
source
ActuaryUtilities.durationMethod
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) -> Vector

DV01 (scalar or per-knot vector) for any AbstractYieldModel. Equivalent to the KeyRates variants of duration but in dollars per basis point.

source
ActuaryUtilities.durationMethod
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 versions

Effective (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.

source
ActuaryUtilities.durationMethod
duration(valuation_fn, curve::AbstractYieldModel, tenors) -> scalar
duration(curve::AbstractYieldModel, tenors, cfs, times) -> scalar
duration(curve::AbstractYieldModel, tenors, cfs::AbstractVector{<:Cashflow}) -> scalar

Scalar 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])
source
ActuaryUtilities.durationMethod
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.

source
ActuaryUtilities.durationMethod
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)
true
source
ActuaryUtilities.durationMethod
duration(kr::KeyRates, valuation_fn, curve::AbstractYieldModel) -> Vector
duration(kr::KeyRates, curve::AbstractYieldModel, cfs, times) -> Vector
duration(kr::KeyRates, curve::AbstractYieldModel, cfs::AbstractVector{<:Cashflow}) -> Vector

Key-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)
end
source
ActuaryUtilities.durationMethod
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 Duration

Calculates 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 a Rate is shocked in its own compounding space. A scalar is treated as Periodic(0.04, 1), so Modified = Macaulay / (1 + 0.04); in general a Periodic(y, m) rate gives Modified = Macaulay / (1 + y/m), and a Continuous(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.0

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.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
source

Unexported API

ActuaryUtilities.FinancialMath._tent_bumpMethod
_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 shift for t ≤ τ, linear ramp to 0 at next neighbor.
  • Last KRD point: linear ramp from 0 at previous neighbor, flat shift for t ≥ τ.
  • Interior: triangle with peak shift at τ, zero at both neighbors.
source