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

See Convexity Conventions for rate-shock definitions and worked examples.

Floating-rate, multi-curve & portfolios

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 amountsValue and dollar riskNormalized risk
Empty or all exactly zeroZeroZero by convention
Nonzero amounts with zero net present valueDollar risk can be nonzeroUndefined (NaN/Inf)
Nonzero present valueCalculated as usualCalculated 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))
true

The 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
CS01

Credit 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().

See also: IR01, DV01

source
ActuaryUtilities.FinancialMath.DV01 — Type
DV01

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

See also: IR01, CS01

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

source
ActuaryUtilities.FinancialMath.Effective — Type
Effective

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

source
ActuaryUtilities.FinancialMath.IR01 — Type
IR01

Interest 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().

See also: CS01, DV01

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

See also: DV01, IR01, CS01

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

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

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

source
ActuaryUtilities.FinancialMath.Spread — Type
Spread

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

source
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

source
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) -> NamedTuple

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

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

source
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.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.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
source
ActuaryUtilities.FinancialMath.duration — Method
duration(valuation, ::DV01, [kr::KeyRates,] base::AbstractYieldModel, credit::AbstractYieldModel) -> NamedTuple
duration(valuation, ::DV01, [kr::KeyRates,] curves::NamedTuple) -> NamedTuple

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

source
ActuaryUtilities.FinancialMath.duration — Method
duration(valuation_fn, ::DV01, kr::KeyRates, curve::AbstractYieldModel) -> Vector
duration(::DV01, kr::KeyRates, curve::AbstractYieldModel, cfs, times) -> Vector

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

source
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)
end
source
ActuaryUtilities.FinancialMath.duration — Method
duration(valuation_fn, kr::KeyRates, curve::AbstractYieldModel) -> Vector
duration(kr::KeyRates, curve::AbstractYieldModel, cfs, times = eachindex(cfs)) -> Vector

Return 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)
end
source
ActuaryUtilities.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.

source
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.0
source
ActuaryUtilities.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.76190476190476
source
ActuaryUtilities.FinancialMath.sensitivities — Method
sensitivities([order,] [KeyRates(tenors),] discount, contract; index = discount) -> NamedTuple
sensitivities([order,] [KeyRates(tenors),] discount::NamedTuple, contract; index) -> NamedTuple

Calculate 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)
source
ActuaryUtilities.FinancialMath.sensitivities — Method
sensitivities(valuation, [order,] inputs::NamedTuple) -> NamedTuple

Differentiate 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)
end

Each 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ᵢ / V
  • dv01.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ⱼ / V
  • dollar_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.

source
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) -> NamedTuple

Return 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 / V
  • dv01: -∂V/∂s / 10000, the first-order value lost for a one-basis-point increase
  • convexity: ∂²V/∂sᵢ∂sⱼ / V
  • dollar_convexity: ∂²V/∂sᵢ∂sⱼ, defined at zero value (see DollarConvexity)

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)
end

See also the contract forms, sensitivities(discount, contract; index), the market-input form, sensitivities(valuation, inputs::NamedTuple), and Scenarios.

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

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.009999999999999964, 1)
source
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.

source

Unexported API