diff --git a/.github/workflows/github_ci.yml b/.github/workflows/github_ci.yml index 5610f33..48a7163 100644 --- a/.github/workflows/github_ci.yml +++ b/.github/workflows/github_ci.yml @@ -10,6 +10,8 @@ jobs: matrix: version: - '1.6' + - '1.7' + - 'nightly' os: - ubuntu-latest - macOS-latest diff --git a/src/LifeContingencies.jl b/src/LifeContingencies.jl index c1d704f..44ff180 100644 --- a/src/LifeContingencies.jl +++ b/src/LifeContingencies.jl @@ -5,7 +5,7 @@ using MortalityTables using Transducers using Dates using Yields - + const mt = MortalityTables export LifeContingency, @@ -39,7 +39,7 @@ abstract type Life end """ struct SingleLife - mort + mortality issue_age::Int alive::Bool fractional_assump::MortalityTables.DeathDistribution @@ -48,44 +48,44 @@ abstract type Life end A `Life` object containing the necessary assumptions for contingent maths related to a single life. Use with a `LifeContingency` to do many actuarial present value calculations. Keyword arguments: -- `mort` pass a mortality vector, which is an array of applicable mortality rates indexed by attained age +- `mortality` pass a mortality vector, which is an array of applicable mortality rates indexed by attained age - `issue_age` is the assumed issue age for the `SingleLife` and is the basis of many contingency calculations. - `alive` Default value is `true`. Useful for joint insurances with different status on the lives insured. - `fractional_assump`. Default value is `Uniform()`. This is a `DeathDistribution` from the `MortalityTables.jl` package and is the assumption to use for non-integer ages/times. # Examples using MortalityTables - mort = MortalityTables.table("2001 VBT Residual Standard Select and Ultimate - Male Nonsmoker, ANB") + mortality = MortalityTables.table("2001 VBT Residual Standard Select and Ultimate - Male Nonsmoker, ANB") SingleLife( mort = mort.select[30], issue_age = 30 ) """ -struct SingleLife <: Life - mort - issue_age - alive - fractional_assump +struct SingleLife{M,D} <: Life + mortality::M + issue_age::Int + alive::Bool + fractional_assump::D end -function SingleLife(;mort,issue_age=nothing,alive=true,fractional_assump = mt.Uniform()) - return SingleLife(mort;issue_age,alive,fractional_assump) +function SingleLife(; mortality, issue_age = nothing, alive = true, fractional_assump = mt.Uniform()) + return SingleLife(mortality; issue_age, alive, fractional_assump) end -function SingleLife(mort;issue_age=nothing,alive=true,fractional_assump = mt.Uniform()) +function SingleLife(mortality; issue_age = nothing, alive = true, fractional_assump = mt.Uniform()) if isnothing(issue_age) - issue_age = firstindex(mort) + issue_age = firstindex(mortality) end - if !(eltype(mort) <: Real) - # most likely case is that mort is an array of vectors + if !(eltype(mortality) <: Real) + # most likely case is that mortality is an array of vectors # use issue age to select the right one (assuming indexed with issue age - return SingleLife(mort[issue_age],issue_age,alive,fractional_assump) + return SingleLife(mortality[issue_age], issue_age, alive, fractional_assump) else - return SingleLife(mort,issue_age,alive,fractional_assump) + return SingleLife(mortality, issue_age, alive, fractional_assump) end - + end """ @@ -99,7 +99,7 @@ abstract type JointAssumption end """ Frasier() -The assumption of independnt lives in a joint life calculation. +The assumption of independent lives in a joint life calculation. Is a subtype of `JointAssumption`. """ struct Frasier <: JointAssumption end @@ -144,14 +144,14 @@ Keyword arguments: # Examples using MortalityTables - mort = MortalityTables.table("2001 VBT Residual Standard Select and Ultimate - Male Nonsmoker, ANB") + mortality = MortalityTables.table("2001 VBT Residual Standard Select and Ultimate - Male Nonsmoker, ANB") l1 = SingleLife( - mort = mort.select[30], + mortality = mortality.select[30], issue_age = 30 ) l2 = SingleLife( - mort = mort.select[30], + mortality = mortality.select[30], issue_age = 30 ) @@ -161,19 +161,19 @@ Keyword arguments: joint_assumption = Frasier() ) """ -Base.@kwdef struct JointLife <: Life +Base.@kwdef struct JointLife{C<:Contingency,J<:JointAssumption} <: Life lives::Tuple{SingleLife,SingleLife} - contingency::Contingency = LastSurvivor() - joint_assumption::JointAssumption = Frasier() + contingency::C = LastSurvivor() + joint_assumption::J = Frasier() end """ struct LifeContingency life::Life """ -struct LifeContingency - life::Life - int +struct LifeContingency{L,Y} + life::L + int::Y end Base.broadcastable(lc::LifeContingency) = Ref(lc) @@ -204,11 +204,11 @@ function mt.omega(lc::LifeContingency) end function mt.omega(l::SingleLife) - return mt.omega(l.mort) - l.issue_age + 1 + return mt.omega(l.mortality) - l.issue_age + 1 end function mt.omega(l::JointLife) - return minimum( omega.(l.lives) ) + return minimum(omega.(l.lives)) end @@ -222,7 +222,7 @@ end ``D_x`` is a retrospective actuarial commutation function which is the product of the survival and discount factor. """ function D(lc::LifeContingency, to_time) - return discount(lc.int, to_time) * survival(lc,to_time) + return discount(lc.int, to_time) * survival(lc, to_time) end @@ -231,8 +231,8 @@ end ``l_x`` is a retrospective actuarial commutation function which is the survival up to a certain point in time. By default, will have a unitary basis (ie `1.0`), but you can specify `basis` keyword argument to use something different (e.g. `1000` is common in the literature.) """ -function l(lc::LifeContingency, to_time; basis=1.0) - return survival(lc.life,to_time) * basis +function l(lc::LifeContingency, to_time; basis = 1.0) + return survival(lc.life, to_time) * basis end """ @@ -241,8 +241,8 @@ end ``C_x`` is a retrospective actuarial commutation function which is the product of the discount factor and the difference in `l` (``l_x``). """ function C(lc::LifeContingency, to_time) - discount(lc.int, to_time+1) * (l(lc,to_time) - l(lc, to_time+1)) - + discount(lc.int, to_time + 1) * (l(lc, to_time) - l(lc, to_time + 1)) + end """ @@ -252,7 +252,8 @@ end """ function N(lc::LifeContingency, from_time) range = from_time:(omega(lc)-1) - return foldxt(+,Map(from_time->D(lc, from_time)), range) + vals = Iterators.map(from_time -> D(lc, from_time), range) + return sum(vals) end """ @@ -263,10 +264,11 @@ Issue age is based on the issue_age in the LifeContingency `lc`. """ function M(lc::LifeContingency, from_time) range = from_time:omega(lc)-1 - return foldxt(+,Map(from_time->C(lc, from_time)), range) + vals = Iterators.map(from_time -> C(lc, from_time), range) + return sum(vals) end -E(lc::LifeContingency, t, x) = D(lc,x + t) / D(lc,x) +E(lc::LifeContingency, t, x) = D(lc, x + t) / D(lc, x) ################## @@ -275,24 +277,28 @@ E(lc::LifeContingency, t, x) = D(lc,x + t) / D(lc,x) abstract type Insurance end -LifeContingency(ins::Insurance) = LifeContingency(ins.life,ins.int) +function LifeContingency(ins::I) where {I<:Insurance} + LifeContingency(ins.life, ins.int) +end -struct WholeLife <: Insurance - life - int +struct WholeLife{L,Y} <: Insurance + life::L + int::Y end -struct Term <: Insurance - life - int - n +struct Term{L,Y} <: Insurance + life::L + int::Y + term::Int end """ - Insurance(lc::LifeContingency; n=nothing) - Insurance(life,interest; n=nothing) + Insurance(lc::LifeContingency, term) + Insurance(life,interest, term) + Insurance(lc::LifeContingency) + Insurance(life,interest) -Life insurance with a term period of `n`. If `n` is `nothing`, then whole life insurance. +Life insurance with a term period of `term`. If `term` is `nothing`, then whole life insurance. Issue age is based on the `issue_age` in the LifeContingency `lc`. @@ -300,44 +306,57 @@ Issue age is based on the `issue_age` in the LifeContingency `lc`. ``` ins = Insurance( - SingleLife(mort = UltimateMortality([0.5,0.5]),issue_age = 0), + SingleLife(mortality = UltimateMortality([0.5,0.5]),issue_age = 0), Yields.Constant(0.05), - n = 1 + 1 # 1 year term ) ``` """ -Insurance(lc::LifeContingency; n=nothing) = Insurance(lc.life,lc.int;n) +Insurance(lc::LifeContingency, term) = Insurance(lc.life, lc.int, term) +Insurance(lc::LifeContingency) = Insurance(lc.life, lc.int) -function Insurance(lc,int;n=nothing) - if isnothing(n) - return WholeLife(lc,int) - elseif n < 1 - return ZeroBenefit(lc,int) - else - Term(lc,int,n) - end +function Insurance(life, int, term::Int) + term < 1 && return ZeroBenefit(life, int) + return Term(life, int, term) end +function Insurance(life, int) + return WholeLife(life, int) +end + +abstract type AnnuityKind end +struct Due <: AnnuityKind end +struct Immediate <: AnnuityKind end -struct Due end -struct Immediate end +abstract type AnnuityPayable end +abstract type AnnuityCertain <: AnnuityPayable end -struct Annuity <: Insurance - life - int - payable - n - start_time - certain - frequency +struct TermCertain <: AnnuityCertain + term::Int + certain::Int +end +struct LifeCertain <: AnnuityCertain + certain::Int +end +struct TermAnnuity <: AnnuityPayable + term::Int end +struct LifeAnnuity <: AnnuityPayable end -struct ZeroBenefit <: Insurance - life - int +struct Annuity{L,Y,K<:AnnuityKind,P<:AnnuityPayable} <: Insurance + life::L + int::Y + kind::K + payable::P + start_time::Int + frequency::Int +end +struct ZeroBenefit{L,Y} <: Insurance + life::L + int::Y end -function ZeroBenefit(lc::LifeContingency) - return ZeroBenefit(lc.life,lc.int) +function ZeroBenefit(lc::LifeContingency) + return ZeroBenefit(lc.life, lc.int) end """ @@ -350,67 +369,92 @@ Annuity due with the benefit period starting at `start_time` and ending after `n ``` ins = AnnuityDue( - SingleLife(mort = UltimateMortality([0.5,0.5]),issue_age = 0), + SingleLife(mortality = UltimateMortality([0.5,0.5]),issue_age = 0), Yields.Constant(0.05), - n = 1 + 1, # term of policy ) ``` - +#TODO update docs """ -function AnnuityDue(life, int; n=nothing,start_time=0,certain=nothing,frequency=1) - if ~isnothing(n) && n < 1 - return ZeroBenefit(life,int) +function AnnuityDue(life, int, term; certain = nothing, start_time = 0, frequency = 1) + term < 1 && return ZeroBenefit(life, int) + if isnothing(certain) + Annuity(life, int, Due(), TermAnnuity(term), start_time, frequency) else - Annuity(life,int,Due(),n,start_time,certain,frequency) + Annuity(life, int, Due(), TermCertain(term, certain), start_time, frequency) end end -function AnnuityDue(lc::LifeContingency; n=nothing,start_time=0,certain=nothing,frequency=1) - return AnnuityDue(lc.life,lc.int;n,start_time,certain,frequency) +function AnnuityDue(life, int; certain = nothing, start_time = 0, frequency = 1) + if isnothing(certain) + Annuity(life, int, Due(), LifeAnnuity(), start_time, frequency) + else + Annuity(life, int, Due(), LifeCertain(certain), start_time, frequency) + end +end + +function AnnuityDue(lc::L, term; certain = nothing, start_time = 0, frequency = 1) where {L<:LifeContingency} + return AnnuityDue(lc.life, lc.int, term; certain, start_time, frequency) end +function AnnuityDue(lc::L; certain = nothing, start_time = 0, frequency = 1) where {L<:LifeContingency} + return AnnuityDue(lc.life, lc.int; certain, start_time, frequency) +end """ - AnnuityImmediate(lc::LifeContingency; n=nothing, start_time=0; certain=nothing,frequency=1) - AnnuityImmediate(life, interest; n=nothing, start_time=0; certain=nothing,frequency=1) + AnnuityImmediate(lc::LifeContingency; term=nothing, start_time=0; certain=nothing,frequency=1) + AnnuityImmediate(life, interest; term=nothing, start_time=0; certain=nothing,frequency=1) -Annuity immediate with the benefit period starting at `start_time` and ending after `n` periods with `frequency` payments per year of `1/frequency` amount and a `certain` period with non-contingent payments. +Annuity immediate with the benefit period starting at `start_time` and ending after `term` periods with `frequency` payments per year of `1/frequency` amount and a `certain` period with non-contingent payments. # Examples ``` ins = AnnuityImmediate( - SingleLife(mort = UltimateMortality([0.5,0.5]),issue_age = 0), + SingleLife(mortality = UltimateMortality([0.5,0.5]),issue_age = 0), Yields.Constant(0.05), - n = 1 + 1 # term of policy ) ``` """ -function AnnuityImmediate(life, int; n=nothing,start_time=0,certain=nothing,frequency=1) - if ~isnothing(n) && n < 1 - return ZeroBenefit(life,int) +function AnnuityImmediate(life, int, term; certain = nothing, start_time = 0, frequency = 1) + term < 1 && return ZeroBenefit(life, int) + if isnothing(certain) + Annuity(life, int, Immediate(), TermAnnuity(term), start_time, frequency) + else + Annuity(life, int, Immediate(), TermCertain(term, certain), start_time, frequency) + end +end + +function AnnuityImmediate(life, int; certain = nothing, start_time = 0, frequency = 1) + if isnothing(certain) + Annuity(life, int, Immediate(), LifeAnnuity(), start_time, frequency) else - return Annuity(life,int,Immediate(),n,start_time,certain,frequency) + Annuity(life, int, Immediate(), LifeCertain(certain), start_time, frequency) end end -function AnnuityImmediate(lc::LifeContingency; n=nothing,start_time=0,certain=nothing,frequency=1) - return AnnuityImmediate(lc.life,lc.int;n,start_time,certain,frequency) +function AnnuityImmediate(lc::L, term; certain = nothing, start_time = 0, frequency = 1) where {L<:LifeContingency} + return AnnuityImmediate(lc.life, lc.int, term; certain, start_time, frequency) +end + +function AnnuityImmediate(lc::L; certain = nothing, start_time = 0, frequency = 1) where {L<:LifeContingency} + return AnnuityImmediate(lc.life, lc.int; certain, start_time, frequency) end """ survival(Insurance) -The survorship vector for the given insurance. +The survivorship vector for the given insurance. """ -function MortalityTables.survival(ins::Insurance) - return [survival(ins.life,t-1) for t in timepoints(ins)] +function MortalityTables.survival(ins::I) where {I<:Insurance} + return Iterators.map(t -> survival(ins.life, t - 1), timepoints(ins)) end function MortalityTables.survival(ins::Annuity) - return [survival(ins.life,t) for t in timepoints(ins)] + return Iterators.map(t -> survival(ins.life, t), timepoints(ins)) end """ @@ -418,26 +462,26 @@ end The discount vector for the given insurance. """ -function Yields.discount(ins::Insurance) - return Yields.discount.(ins.int,timepoints(ins)) +function Yields.discount(ins::I) where {I<:Insurance} + return Iterators.map(t -> Yields.discount.(ins.int, t), timepoints(ins)) end """ benefit(Insurance) -The unit benefit vector for the given insurance. +The unit benefit for the given insurance. """ -function benefit(ins::Insurance) - return ones(length(timepoints(ins))) +function benefit(ins::I) where {I<:Insurance} + return 1.0 end function benefit(ins::ZeroBenefit) - return zeros(length(timepoints(ins))) + return 0.0 end function benefit(ins::Annuity) - return ones(length(timepoints(ins))) ./ ins.frequency + return 1.0 / ins.frequency end @@ -446,19 +490,29 @@ end The vector of contingent benefit probabilities for the given insurance. """ -function probability(ins::Insurance) - return [survival(ins.life,t-1) * decrement(ins.life,t-1,t) for t in timepoints(ins)] +function probability(ins::I) where {I<:Insurance} + # f(t) = survival(ins.life, 0, t) * (1 - survival(lc.life, t - 1, t)) + # f(t) = (survival(ins.life, 0, t) - survival(ins.life, 0, t) * survival(lc.life, t - 1, t)) + return Iterators.map(timepoints(ins)) do t + survival(ins.life, t - 1) * decrement(ins.life, t - 1, t) + end end function probability(ins::ZeroBenefit) - return ones(length(timepoints(ins))) + return Iterators.repeated(1.0, length(timepoints(ins))) end function probability(ins::Annuity) - if isnothing(ins.certain) - return [survival(ins.life,t) for t in timepoints(ins)] - else - return [t <= ins.certain + ins.start_time ? 1.0 : survival(ins.life,t) for t in timepoints(ins)] + return probability(ins.payable, ins) +end + +function probability(ap::AP, ins::Annuity) where {AP<:AnnuityPayable} + return Iterators.map(t -> survival(ins.life, t), timepoints(ins)) +end + +function probability(ap::AP, ins::Annuity) where {AP<:AnnuityCertain} + return Iterators.map(timepoints(ins)) do t + t <= ap.certain + ins.start_time ? 1.0 : survival(ins.life, t) end end @@ -468,8 +522,9 @@ end The vector of decremented benefit cashflows for the given insurance. """ -function cashflows(ins::Insurance) - return probability(ins) .* benefit(ins) +function cashflows(ins::I) where {I<:Insurance} + b = benefit(ins) + return Iterators.map(p -> p * b, probability(ins)) end @@ -478,41 +533,80 @@ end The vector of times corresponding to the cashflow vector for the given insurance. """ -function timepoints(ins::Insurance) - return collect(1:omega(ins.life)) +function timepoints(ins::Insurance)::UnitRange{Int64} + return 1:omega(ins.life) end -function timepoints(ins::Term) - return collect(1:min(omega(ins.life),ins.n)) +function timepoints(ins::Term)::UnitRange{Int64} + return 1:min(omega(ins.life), ins.term) end function timepoints(ins::ZeroBenefit) - return [0.] + return Iterators.repeated(0.0, 1) end function timepoints(ins::Annuity) - return timepoints(ins,ins.payable) + return timepoints(ins, ins.kind) end -function timepoints(ins::Annuity,payable::Due) - if isnothing(ins.n) - end_time = omega(ins.life) - else - end_time = ins.n + ins.start_time - 1 / ins.frequency - end +function timepoints(ins::Annuity, kind::K) where {K<:AnnuityKind} + return timepoints(ins.payable, ins, kind) +end + +function timepoints(ap::LifeCertain, ins::Annuity, ::Due) + end_time = omega(ins.life) timestep = 1 / ins.frequency - collect(ins.start_time:timestep:end_time) + return ins.start_time:timestep:end_time end -function timepoints(ins::Annuity,payable::Immediate) - if isnothing(ins.n) - end_time = omega(ins.life) - else - end_time = ins.n + ins.start_time - end +function timepoints(ap::LifeAnnuity, ins::Annuity, ::Due) + # same timepoints as LifeCertain + end_time = omega(ins.life) timestep = 1 / ins.frequency - end_time = max(ins.start_time + timestep,end_time) # return at least one timepoint to avoid returning empty array - collect((ins.start_time + timestep):timestep:end_time) + return ins.start_time:timestep:end_time +end + +function timepoints(ap::TermCertain, ins::Annuity, ::Due) + end_time = ap.term + ins.start_time - 1 / ins.frequency + timestep = 1 / ins.frequency + return ins.start_time:timestep:end_time +end + +function timepoints(ap::TermAnnuity, ins::Annuity, ::Due) + # same timepoints as + end_time = ap.term + ins.start_time - 1 / ins.frequency + timestep = 1 / ins.frequency + return ins.start_time:timestep:end_time +end + +function timepoints(ap::LifeCertain, ins::Annuity, ::Immediate) + end_time = omega(ins.life) + timestep = 1 / ins.frequency + end_time = max(ins.start_time + timestep, end_time) # return at least one timepoint to avoid returning empty array + return (ins.start_time+timestep):timestep:end_time +end + +function timepoints(ap::LifeAnnuity, ins::Annuity, ::Immediate) + # same timepoints as LifeCertain + end_time = omega(ins.life) + timestep = 1 / ins.frequency + end_time = max(ins.start_time + timestep, end_time) # return at least one timepoint to avoid returning empty array + return (ins.start_time+timestep):timestep:end_time +end + +function timepoints(ap::TermCertain, ins::Annuity, ::Immediate) + end_time = ap.term + ins.start_time + timestep = 1 / ins.frequency + end_time = max(ins.start_time + timestep, end_time) # return at least one timepoint to avoid returning empty array + return (ins.start_time+timestep):timestep:end_time +end + +function timepoints(ap::TermAnnuity, ins::Annuity, ::Immediate) + # same timepoints as + end_time = ap.term + ins.start_time + timestep = 1 / ins.frequency + end_time = max(ins.start_time + timestep, end_time) # return at least one timepoint to avoid returning empty array + return (ins.start_time+timestep):timestep:end_time end """ @@ -520,8 +614,12 @@ end The actuarial present value of the given insurance. """ -function ActuaryUtilities.present_value(ins) - return present_value(ins.int,cashflows(ins),timepoints(ins)) +function ActuaryUtilities.present_value(ins::T) where {T<:Insurance} + cfs = cashflows(ins) + times = timepoints(ins) + yield = ins.int + pv = present_value(yield, cfs, times) + return pv end """ @@ -532,18 +630,21 @@ The net premium for a whole life insurance (without second argument) or a term l The net premium is based on 1 unit of insurance with the death benfit payable at the end of the year and assuming annual net premiums. """ -premium_net(lc::LifeContingency) = A(lc) / ä(lc) -premium_net(lc::LifeContingency,to_time) = A(lc,to_time) / ä(lc,to_time) +function premium_net(lc::LifeContingency) + return A(lc) / ä(lc) +end + +premium_net(lc::LifeContingency, to_time) = A(lc, to_time) / ä(lc, to_time) """ reserve_premium_net(lc::LifeContingency,time) The net premium reserve at the end of year `time`. """ -function reserve_premium_net(lc::LifeContingency, time) - PVFB = A(lc) - A(lc,n=time) - PVFP = premium_net(lc) * (ä(lc) - ä(lc,n=time)) - return (PVFB - PVFP) / APV(lc,time) +function reserve_premium_net(lc::LifeContingency, time) + PVFB = A(lc) - A(lc, time) + PVFP = premium_net(lc) * (ä(lc) - ä(lc, time)) + return (PVFB - PVFP) / APV(lc, time) end """ @@ -551,8 +652,8 @@ end The **actuarial present value** which is the survival times the discount factor for the life contingency. """ -function APV(lc::LifeContingency,to_time) - return survival(lc,to_time) * discount(lc.int,to_time) +function APV(lc::LifeContingency, to_time) + return survival(lc, to_time) * discount(lc.int, to_time) end """ @@ -561,58 +662,58 @@ end Return the probablity of death for the given LifeContingency. """ -mt.decrement(lc::LifeContingency,from_time,to_time) = 1 - survival(lc.life,from_time,to_time) +mt.decrement(lc::LifeContingency, from_time, to_time) = 1 - survival(lc.life, from_time, to_time) """ survival(lc::LifeContingency,from_time,to_time) survival(lc::LifeContingency,to_time) -Return the probablity of survival for the given LifeContingency. +Return the probability of survival for the given LifeContingency. """ -mt.survival(lc::LifeContingency,to_time) = survival(lc.life, 0, to_time) -mt.survival(lc::LifeContingency,from_time,to_time) = survival(lc.life, from_time, to_time) +mt.survival(lc::LifeContingency, to_time) = survival(lc.life, 0, to_time) +mt.survival(lc::LifeContingency, from_time, to_time) = survival(lc.life, from_time, to_time) -mt.survival(l::SingleLife,to_time) = survival(l,0,to_time) -mt.survival(l::SingleLife,from_time,to_time) =survival(l.mort,l.issue_age + from_time,l.issue_age + to_time, l.fractional_assump) +mt.survival(l::SingleLife, to_time) = survival(l, 0, to_time) +mt.survival(l::SingleLife, from_time, to_time) = survival(l.mortality, l.issue_age + from_time, l.issue_age + to_time, l.fractional_assump) """ - surival(life) + survival(life) Return a survival vector for the given life. """ -function mt.survival(l::Life) - ω = omega(l) - return [survival(l,t) for t in 0:ω] +function mt.survival(l::L) where {L<:Life} + ω = omega(l) + return Iterators.map(t -> survival(l, t), 0:ω) end -mt.survival(l::JointLife,to_time) = survival(l::JointLife,0,to_time) -function mt.survival(l::JointLife,from_time,to_time) - return survival(l.contingency,l.joint_assumption,l::JointLife,from_time,to_time) +mt.survival(l::JointLife, to_time) = survival(l::JointLife, 0, to_time) +function mt.survival(l::JointLife, from_time, to_time) + return survival(l.contingency, l.joint_assumption, l::JointLife, from_time, to_time) end -function mt.survival(ins::LastSurvivor,assump::JointAssumption,l::JointLife,from_time,to_time) +function mt.survival(ins::LastSurvivor, assump::JointAssumption, l::JointLife, from_time, to_time) to_time == 0 && return 1.0 - - l1,l2 = l.lives - ₜpₓ = survival(l1.mort,l1.issue_age + from_time,l1.issue_age + to_time,l1.fractional_assump) - ₜpᵧ = survival(l2.mort,l2.issue_age + from_time,l2.issue_age + to_time,l2.fractional_assump) + + l1, l2 = l.lives + ₜpₓ = survival(l1.mortality, l1.issue_age + from_time, l1.issue_age + to_time, l1.fractional_assump) + ₜpᵧ = survival(l2.mortality, l2.issue_age + from_time, l2.issue_age + to_time, l2.fractional_assump) return ₜpₓ + ₜpᵧ - ₜpₓ * ₜpᵧ end -Yields.discount(lc::LifeContingency,t) = discount(lc.int,t) -Yields.discount(lc::LifeContingency,t1,t2) = discount(lc.int,t1,t2) +Yields.discount(lc::LifeContingency, t) = discount(lc.int, t) +Yields.discount(lc::LifeContingency, t1, t2) = discount(lc.int, t1, t2) -# function compostion with kwargs. -# https://stackoverflow.com/questions/64740010/how-to-alias-composite-function-with-keyword-arguments -⋄(f, g) = (x...; kw...)->f(g(x...; kw...)) # unexported aliases const V = reserve_premium_net const v = Yields.discount -const A = present_value ⋄ Insurance -const a = present_value ⋄ AnnuityImmediate -const ä = present_value ⋄ AnnuityDue +# A(args) = present_value(Insurance(args)) +# a(args, kwargs) = present_value(AnnuityImmediate(args...; kwargs...)) +# ä(args) = present_value(AnnuityDue(args)) +const A = present_value ∘ Insurance +const a = present_value ∘ AnnuityImmediate +const ä = present_value ∘ AnnuityDue const P = premium_net const ω = omega diff --git a/src/scratchpad.jl b/src/scratchpad.jl deleted file mode 100644 index f8b72a1..0000000 --- a/src/scratchpad.jl +++ /dev/null @@ -1,96 +0,0 @@ -using LifeContingencies -using MortalityTables -using Dates -using DataFrames - -mort_table_name = "2012 IAM Period Table – Male, ANB" -improv_table = "Projection Scale G2 – Male, ANB" -iss_date = Date(2017,09,01) -val_date = Date(2017,12,31) -issue_age = 65 -annual_income = 12000 -deferral_period = 0 -benefit_period = 120 -interest_rate = 0.04 - -# Not used because table name provides -# sex = :M -# projection_scale = :G2 -# age_rule = :ANB - - -mort = MortalityTables.table(mort_table_name) -imp = MortalityTables.table(improv_table) - -table_end = omega(mort) - -# the projection scale table only goes to age 105, wheras we want through age 120 -function imp_rate(table,age) - if age > lastindex(table) - return 0.0 - else - return table[age] - end -end - -SRA = map(0:(table_end - issue_age)) do time - - att_age = issue_age + time - - if time > 1 - imp_factor = prod(map(age -> 1 - imp_rate(imp,age),issue_age:(att_age-1))) - else - imp_factor = 1.0 - end - - # returned values for each time - - ( - att_age = att_age, - t=time, - q=mort[att_age], - imp_factor = imp_factor, - q_imp = mort[att_age] * imp_factor, - - ) -end - -### A type to handle life contingent maths via the LifeContingencies package -ins = LifeContingency( - SingleLife( - mort = UltimateMortality([x.q_imp for x in SRA],start_age = issue_age), # mort is indexed by attained age, not starting at 1 - issue_age = issue_age - ), - InterestRate(interest_rate) -) - - -ä(ins,55) * annual_income -sum(LifeContingencies.APV.(ins,1:55) * 12000) - - - -ℓ₁ = [43302,42854,42081,41351,40050] -ℓ₂ = [47260,47040,46755,46500,46227] - -ps₁ = ℓ₁ ./ ℓ₁[1] -qs₁ = [1 - ps₁[t] / ps₁[t - 1] for t in 2:5 ] - -ps₂ = ℓ₂ ./ ℓ₂[1] -qs₂ = [1 - ps₂[t] / ps₂[t - 1] for t in 2:5 ] - -m1 = UltimateMortality(qs₁, start_age=65) -m2 = UltimateMortality(qs₂, start_age=60) - - -l1 = SingleLife(mort = m1, issue_age = 65) -l2 = SingleLife(mort = m2, issue_age = 60) - -jl = JointLife(lives=(l1, l2), contingency = LastSurvivor(), joint_assumption=Frasier()) - - - -ins = LifeContingency(jl,InterestRate(0.05)) -ins_l1 = LifeContingency(jl.lives[1],InterestRate(0.05)) -ins_l2 = LifeContingency(jl.lives[2],InterestRate(0.05)) -# problem 9.1.f \ No newline at end of file diff --git a/test/AMLCR.jl b/test/AMLCR.jl index 31366b6..57f4cb6 100644 --- a/test/AMLCR.jl +++ b/test/AMLCR.jl @@ -3,40 +3,40 @@ # §5.7 payment frequency and certain (Tables 5.1 and 5.2) function f(age) - life = SingleLife(mort = AMLCR_std, issue_age = age) + life = SingleLife(mortality = AMLCR_std, issue_age = age) int = Yields.Constant(0.05) lc = [ - AnnuityImmediate(life,int) - AnnuityImmediate(life,int,frequency=4) - AnnuityDue(life,int) - AnnuityDue(life,int,frequency=4) - AnnuityImmediate(life,int,n=10) - AnnuityImmediate(life,int,n=10,frequency=4) - AnnuityDue(life,int,n=10,frequency=4) + AnnuityImmediate(life, int) + AnnuityImmediate(life, int, frequency = 4) + AnnuityDue(life, int) + AnnuityDue(life, int, frequency = 4) + AnnuityImmediate(life, int, 10) + AnnuityImmediate(life, int, 10, frequency = 4) + AnnuityDue(life, int, 10, frequency = 4) ] - + return present_value.(lc) end # atol only to two decimals b/c AMLCR uses rounded rates? - @test all(isapprox.(f(20), [18.966, 19.338, 19.966, 19.588, 7.711, 7.855, 7.952],atol=.01)) - @test all(isapprox.(f(80), [ 7.548, 7.917, 8.548, 8.167, 6.128, 6.373, 6.539],atol=.01)) + @test all(isapprox.(f(20), [18.966, 19.338, 19.966, 19.588, 7.711, 7.855, 7.952], atol = 0.01)) + @test all(isapprox.(f(80), [7.548, 7.917, 8.548, 8.167, 6.128, 6.373, 6.539], atol = 0.01)) - life = SingleLife(mort = AMLCR_std, issue_age = 20) + life = SingleLife(mortality = AMLCR_std, issue_age = 20) int = Yields.Constant(0.05) # property tests - @test present_value(AnnuityDue(life,int)) > present_value(AnnuityDue(life,int,start_time=5)) + @test present_value(AnnuityDue(life, int)) > present_value(AnnuityDue(life, int, start_time = 5)) + + @test present_value(AnnuityDue(life, int, 5, certain = 5, start_time = 5)) == sum([1 for t = 5:9] .* [1 / 1.05^t for t = 5:9]) - @test present_value(AnnuityDue(life,int,n=5,certain=5,start_time=5)) == sum([1 for t in 5:9] .* [1/1.05 ^t for t in 5:9]) - # relation on pg 124 @test issorted(present_value.([ - AnnuityImmediate(life,int) - AnnuityImmediate(life,int,frequency=4) - AnnuityDue(life,int,frequency=4) - AnnuityDue(life,int) + AnnuityImmediate(life, int) + AnnuityImmediate(life, int, frequency = 4) + AnnuityDue(life, int, frequency = 4) + AnnuityDue(life, int) ])) end# \ No newline at end of file diff --git a/test/joint_life.jl b/test/joint_life.jl index b133ad5..724a931 100644 --- a/test/joint_life.jl +++ b/test/joint_life.jl @@ -1,72 +1,72 @@ @testset "Joint Life" begin - + @testset "Joint Last Survivor unknown status" begin - + @testset "ALMCR §9.4" begin - ℓ₁ = [43302,42854,42081,41351,40050] - ℓ₂ = [47260,47040,46755,46500,46227] + ℓ₁ = [43302, 42854, 42081, 41351, 40050] + ℓ₂ = [47260, 47040, 46755, 46500, 46227] + + ps₁ = ℓ₁ ./ ℓ₁[1] + qs₁ = [1 - ps₁[t] / ps₁[t-1] for t = 2:5] - ps₁ = ℓ₁ ./ ℓ₁[1] - qs₁ = [1 - ps₁[t] / ps₁[t - 1] for t in 2:5 ] + ps₂ = ℓ₂ ./ ℓ₂[1] + qs₂ = [1 - ps₂[t] / ps₂[t-1] for t = 2:5] - ps₂ = ℓ₂ ./ ℓ₂[1] - qs₂ = [1 - ps₂[t] / ps₂[t - 1] for t in 2:5 ] - - m1 = UltimateMortality(qs₁, start_age=65) - m2 = UltimateMortality(qs₂, start_age=60) + m1 = UltimateMortality(qs₁, start_age = 65) + m2 = UltimateMortality(qs₂, start_age = 60) @test decrement(m1, 65, 66) == 1 - ℓ₁[2] / ℓ₁[1] @test decrement(m2, 60, 61) == 1 - ℓ₂[2] / ℓ₂[1] - l1 = SingleLife(mort = m1, issue_age = 65) - l2 = SingleLife(mort = m2, issue_age = 60) - - jl = JointLife(lives=(l1, l2), contingency = LastSurvivor(), joint_assumption=Frasier()) - - @test isapprox( survival(jl, 2) , 0.9997, atol = 1e-4) - - - ins = LifeContingency(jl,Yields.Constant(0.05)) - ins_l1 = LifeContingency(jl.lives[1],Yields.Constant(0.05)) - ins_l2 = LifeContingency(jl.lives[2],Yields.Constant(0.05)) + l1 = SingleLife(mortality = m1, issue_age = 65) + l2 = SingleLife(mortality = m2, issue_age = 60) + + jl = JointLife(lives = (l1, l2), contingency = LastSurvivor(), joint_assumption = Frasier()) + + @test isapprox(survival(jl, 2), 0.9997, atol = 1e-4) + + + ins = LifeContingency(jl, Yields.Constant(0.05)) + ins_l1 = LifeContingency(jl.lives[1], Yields.Constant(0.05)) + ins_l2 = LifeContingency(jl.lives[2], Yields.Constant(0.05)) # problem 9.1.f - @test isapprox( present_value(AnnuityDue(ins,n=5)), 4.5437, atol = 1e-4) + @test isapprox(present_value(AnnuityDue(ins, 5)), 4.5437, atol = 1e-4) - @test isapprox( present_value(AnnuityDue(ins)), 4.5437, atol = 1e-4) + @test isapprox(present_value(AnnuityDue(ins)), 4.5437, atol = 1e-4) end - + @testset "CIA tables" begin m1 = MortalityTables.table("1986-92 CIA – Male Smoker, ANB") m2 = MortalityTables.table("1986-92 CIA – Female Nonsmoker, ANB") - l1 = SingleLife(mort = m1.ultimate, issue_age = 40) - l2 = SingleLife(mort = m2.ultimate, issue_age = 37) + l1 = SingleLife(mortality = m1.ultimate, issue_age = 40) + l2 = SingleLife(mortality = m2.ultimate, issue_age = 37) + + jl = JointLife(lives = (l1, l2), contingency = LastSurvivor(), joint_assumption = Frasier()) - jl = JointLife(lives=(l1, l2), contingency=LastSurvivor(), joint_assumption=Frasier()) - @testset "independent lives" begin - for time in 1:40 - tpx = survival(l1,time) - tpy = survival(l2,time) + for time = 1:40 + tpx = survival(l1, time) + tpy = survival(l2, time) @test survival(jl, time) == tpx + tpy - tpx * tpy end end - - q_annual = [0.00000141120,0.00000478349,0.00000921100,0.00001508953,0.00002255325,0.00003179413,0.00004327035,0.00005782855,0.00007524178,0.00009695039,0.00012370408,0.00015606419,0.00019612090,0.00024529620,0.00030502659,0.00037768543,0.00046587280,0.00057325907,0.00070238876,0.00085952824,0.00104786439,0.00127420204,0.00154640464,0.00187374100,0.00226186101,0.00272457482,0.00327193743,0.00391984214,0.00468084563,0.00557778027,0.00662135901,0.00783684326,0.00924475551,0.01086696400,0.01272977389,0.01485150445,0.01727277853,0.02000566587,0.02308982834,0.02654184882,0.03039819991,0.03469299691,0.03944737905,0.04469931521,0.05048294677,0.05683522818,0.06379854263,0.07142629169,0.07976664920,0.08888579420,0.09884411368,0.10971505011,0.12159455247,0.13456410066,0.14873755448,0.16421406941,0.18111392905,0.19954137714,0.21962070363,0.24144744481,0.26515968560,0.29099967654,0.31901974549,0.35111309929,0.39733960046,0.47004540325,0.58255000000,0.74852030000,1.00000000000] - q_cumulative = [0.00000141120,0.00000619469,0.00001540563,0.00003049492,0.00005304748,0.00008483992,0.00012810660,0.00018592774,0.00026115553,0.00035808060,0.00048174038,0.00063772939,0.00083372522,0.00107881691,0.00138351443,0.00176067732,0.00222572986,0.00279771301,0.00349813669,0.00435465818,0.00539795948,0.00666528343,0.00820138084,0.01005975458,0.01229886182,0.01498992747,0.01821281880,0.02206126957,0.02663884979,0.03206804441,0.03847706939,0.04601237389,0.05483175625,0.06510286552,0.07700389465,0.09071177542,0.10641770955,0.12429441828,0.14451430984,0.16722048170,0.19253547997,0.22054884408,0.25129614928,0.28476269870,0.32086998531,0.35946849466,0.40033347120,0.44316542761,0.48758225561,0.53312891378,0.57927637250,0.62543608637,0.67098101782,0.71525516126,0.75760741222,0.79741168545,0.83410325108,0.86720651682,0.89637071504,0.92139174110,0.94223548231,0.95904493828,0.97211041164,0.98190281145,0.98909354112,0.99422007198,0.99758716905,0.99939322200,1.00000000000] + + q_annual = [0.00000141120, 0.00000478349, 0.00000921100, 0.00001508953, 0.00002255325, 0.00003179413, 0.00004327035, 0.00005782855, 0.00007524178, 0.00009695039, 0.00012370408, 0.00015606419, 0.00019612090, 0.00024529620, 0.00030502659, 0.00037768543, 0.00046587280, 0.00057325907, 0.00070238876, 0.00085952824, 0.00104786439, 0.00127420204, 0.00154640464, 0.00187374100, 0.00226186101, 0.00272457482, 0.00327193743, 0.00391984214, 0.00468084563, 0.00557778027, 0.00662135901, 0.00783684326, 0.00924475551, 0.01086696400, 0.01272977389, 0.01485150445, 0.01727277853, 0.02000566587, 0.02308982834, 0.02654184882, 0.03039819991, 0.03469299691, 0.03944737905, 0.04469931521, 0.05048294677, 0.05683522818, 0.06379854263, 0.07142629169, 0.07976664920, 0.08888579420, 0.09884411368, 0.10971505011, 0.12159455247, 0.13456410066, 0.14873755448, 0.16421406941, 0.18111392905, 0.19954137714, 0.21962070363, 0.24144744481, 0.26515968560, 0.29099967654, 0.31901974549, 0.35111309929, 0.39733960046, 0.47004540325, 0.58255000000, 0.74852030000, 1.00000000000] + q_cumulative = [0.00000141120, 0.00000619469, 0.00001540563, 0.00003049492, 0.00005304748, 0.00008483992, 0.00012810660, 0.00018592774, 0.00026115553, 0.00035808060, 0.00048174038, 0.00063772939, 0.00083372522, 0.00107881691, 0.00138351443, 0.00176067732, 0.00222572986, 0.00279771301, 0.00349813669, 0.00435465818, 0.00539795948, 0.00666528343, 0.00820138084, 0.01005975458, 0.01229886182, 0.01498992747, 0.01821281880, 0.02206126957, 0.02663884979, 0.03206804441, 0.03847706939, 0.04601237389, 0.05483175625, 0.06510286552, 0.07700389465, 0.09071177542, 0.10641770955, 0.12429441828, 0.14451430984, 0.16722048170, 0.19253547997, 0.22054884408, 0.25129614928, 0.28476269870, 0.32086998531, 0.35946849466, 0.40033347120, 0.44316542761, 0.48758225561, 0.53312891378, 0.57927637250, 0.62543608637, 0.67098101782, 0.71525516126, 0.75760741222, 0.79741168545, 0.83410325108, 0.86720651682, 0.89637071504, 0.92139174110, 0.94223548231, 0.95904493828, 0.97211041164, 0.98190281145, 0.98909354112, 0.99422007198, 0.99758716905, 0.99939322200, 1.00000000000] @testset "precalced vectors" begin - for time in 1:40 - @test isapprox(decrement(jl,time), q_cumulative[time], atol = 1e-6) + for time = 1:40 + @test isapprox(decrement(jl, time), q_cumulative[time], atol = 1e-6) end - for time in 1:40 - q′ = 1 - survival(jl, time) / survival(jl, time-1) + for time = 1:40 + q′ = 1 - survival(jl, time) / survival(jl, time - 1) @test isapprox(q′, q_annual[time], atol = 1e-6) end - for time in 1:40 + for time = 1:40 @test isapprox(survival(jl, time), 1 - q_cumulative[time], atol = 1e-6) end end diff --git a/test/simple_mort.jl b/test/simple_mort.jl index 408a1b6..e1e4b4f 100644 --- a/test/simple_mort.jl +++ b/test/simple_mort.jl @@ -1,139 +1,139 @@ @testset "basic building blocks" begin - mt = UltimateMortality([0.5,0.5]) + mt = UltimateMortality([0.5, 0.5]) # whole life insurance ins = Insurance( - SingleLife(mort = mt,issue_age = 0), - Yields.Constant(0.05) - ) - - @test all(survival(SingleLife(mort = mt,issue_age = 0)) .== [1.0, 0.5,0.25]) - - @test timepoints(ins) == [1.0,2.0] - @test survival(ins) == [1.0,0.5] - @test discount(ins) == [1.0 / 1.05, 1 / 1.05^2] - @test benefit(ins) == [1.0,1.0] - @test probability(ins) == [0.5,0.25] - @test cashflows(ins) == [0.5,0.25] - @test cashflows(ins) == benefit(ins) .* probability(ins) - @test present_value(ins) ≈ 0.5 / 1.05 + 0.5 * 0.5 / 1.05 ^ 2 - @test present_value(ins) ≈ LifeContingencies.A(ins.life,ins.int) + SingleLife(mortality = mt, issue_age = 0), + Yields.Constant(0.05) + ) + + @test all(survival(SingleLife(mortality = mt, issue_age = 0)) .== [1.0, 0.5, 0.25]) + + @test [timepoints(ins)...] == [1.0, 2.0] + @test [survival(ins)...] == [1.0, 0.5] + @test [discount(ins)...] == [1.0 / 1.05, 1 / 1.05^2] + @test benefit(ins) == 1.0 + @test [probability(ins)...] == [0.5, 0.25] + @test [cashflows(ins)...] == [0.5, 0.25] + @test [cashflows(ins)...] == benefit(ins) .* probability(ins) + @test present_value(ins) ≈ 0.5 / 1.05 + 0.5 * 0.5 / 1.05^2 + @test present_value(ins) ≈ LifeContingencies.A(ins.life, ins.int) # basic life contingency tests - @test survival(LifeContingency(ins),0,1) ≈ 0.5 - @test survival(LifeContingency(ins),0,2) ≈ 0.25 - @test discount(LifeContingency(ins),0,1) ≈ 1/1.05 - @test discount(LifeContingency(ins),0,2) ≈ 1/1.05^2 + @test survival(LifeContingency(ins), 0, 1) ≈ 0.5 + @test survival(LifeContingency(ins), 0, 2) ≈ 0.25 + @test discount(LifeContingency(ins), 0, 1) ≈ 1 / 1.05 + @test discount(LifeContingency(ins), 0, 2) ≈ 1 / 1.05^2 # term life insurance ins = Insurance( - SingleLife(mort = mt,issue_age = 0), - Yields.Constant(0.05), - n=1 - ) - - @test survival(ins) == [1.0] - @test discount(ins) == [1.0 / 1.05] - @test benefit(ins) == [1.0] - @test probability(ins) == [0.5] - @test cashflows(ins) == [0.5] - @test cashflows(ins) == benefit(ins) .* probability(ins) - @test timepoints(ins) == [1.0] - @test present_value(ins) ≈ 0.5 / 1.05 - + SingleLife(mortality = mt, issue_age = 0), + Yields.Constant(0.05), + 1 + ) + + @test [survival(ins)...] == [1.0] + @test [discount(ins)...] == [1.0 / 1.05] + @test benefit(ins) == 1.0 + @test [probability(ins)...] == [0.5] + @test [cashflows(ins)...] == [0.5] + @test [cashflows(ins)...] == benefit(ins) .* probability(ins) + @test [timepoints(ins)...] == [1.0] + @test present_value(ins) ≈ 0.5 / 1.05 + # annuity due ins = AnnuityDue( - SingleLife(mort = mt,issue_age = 0), - Yields.Constant(0.05) - ) - - @test survival(ins) == [1.0,0.5,.25] - @test discount(ins) == [1.0, 1 / 1.05^1, 1 / 1.05^2] - @test benefit(ins) == [1.0,1.0,1.0] - @test timepoints(ins) == [0.0,1.0,2.0] - @test probability(ins) == [1.,0.5,0.25] - @test cashflows(ins) == [1.0,0.5,0.25] - @test cashflows(ins) == benefit(ins) .* probability(ins) - @test present_value(ins) ≈ 1 + 1 * .5 / 1.05 + 1 * .25 / 1.05 ^2 + SingleLife(mortality = mt, issue_age = 0), + Yields.Constant(0.05) + ) + + @test [survival(ins)...] == [1.0, 0.5, 0.25] + @test [discount(ins)...] == [1.0, 1 / 1.05^1, 1 / 1.05^2] + @test benefit(ins) == 1.0 + @test [timepoints(ins)...] == [0.0, 1.0, 2.0] + @test [probability(ins)...] == [1.0, 0.5, 0.25] + @test [cashflows(ins)...] == [1.0, 0.5, 0.25] + @test [cashflows(ins)...] == benefit(ins) .* probability(ins) + @test present_value(ins) ≈ 1 + 1 * 0.5 / 1.05 + 1 * 0.25 / 1.05^2 ins = AnnuityDue( - SingleLife(mort = mt,issue_age = 0), + SingleLife(mortality = mt, issue_age = 0), Yields.Constant(0.05), - n = 2 -) - @test present_value(ins) ≈ 1 + 1 * .5 / 1.05 + 2 + ) + @test present_value(ins) ≈ 1 + 1 * 0.5 / 1.05 ins = AnnuityImmediate( - SingleLife(mort = mt,issue_age = 0), + SingleLife(mortality = mt, issue_age = 0), Yields.Constant(0.05), - n = 1 - ) + 1 + ) - @test present_value(ins) ≈ 1 * .5 * 1 / 1.05 + @test present_value(ins) ≈ 1 * 0.5 * 1 / 1.05 ins = AnnuityDue( - SingleLife(mort = mt,issue_age = 0), + SingleLife(mortality = mt, issue_age = 0), Yields.Constant(0.05), - n = 1, - start_time=1 - ) + 1, + start_time = 1 + ) - @test timepoints(ins) == [1.0] - @test probability(ins) == [0.5] - @test present_value(ins) ≈ 1 * .5 * 1 / 1.05 + @test timepoints(ins) == [1.0] + @test [probability(ins)...] == [0.5] + @test present_value(ins) ≈ 1 * 0.5 * 1 / 1.05 end @testset "one year" begin ins = LifeContingency( SingleLife( - mort = UltimateMortality([0.5]), + mortality = UltimateMortality([0.5]), issue_age = 0 ), Yields.Constant(0.05) ) - @test survival(ins,1) ≈ 0.5 - @test survival(ins,0) ≈ 1.0 - @test survival(ins.life,0,0.5) ≈ 1 - 0.5 * 0.5 - @test survival(ins,0.5) ≈ 1 - 0.5 * 0.5 + @test survival(ins, 1) ≈ 0.5 + @test survival(ins, 0) ≈ 1.0 + @test survival(ins.life, 0, 0.5) ≈ 1 - 0.5 * 0.5 + @test survival(ins, 0.5) ≈ 1 - 0.5 * 0.5 @test omega(ins) ≈ 1 - @test present_value(AnnuityDue(ins)) ≈ 1 + 1 * .5 / 1.05 - @test present_value(AnnuityDue(ins,n=1)) ≈ 1 - @test present_value(AnnuityDue(ins,n=0)) == 0 - @test present_value(AnnuityImmediate(ins,n=1)) ≈ 1 * .5 / 1.05 - @test present_value(AnnuityImmediate(ins,n=0)) == 0 + @test present_value(AnnuityDue(ins)) ≈ 1 + 1 * 0.5 / 1.05 + @test present_value(AnnuityDue(ins, 1)) ≈ 1 + @test present_value(AnnuityDue(ins, 0)) == 0 + @test present_value(AnnuityImmediate(ins, 1)) ≈ 1 * 0.5 / 1.05 + @test present_value(AnnuityImmediate(ins, 0)) == 0 @test present_value(Insurance(ins)) ≈ 0.5 / 1.05 - @test present_value(Insurance(ins,n=1)) ≈ 0.5 / 1.05 - @test present_value(Insurance(ins,n=0)) ≈ 0 + @test present_value(Insurance(ins, 1)) ≈ 0.5 / 1.05 + @test present_value(Insurance(ins, 0)) ≈ 0 ins_jl = LifeContingency( - JointLife((ins.life,ins.life),LastSurvivor(),Frasier()), + JointLife((ins.life, ins.life), LastSurvivor(), Frasier()), Yields.Constant(0.05) ) @test omega(ins_jl) ≈ 1 - @test present_value(AnnuityDue(ins_jl)) ≈ 1 + 1 * .75 / 1.05 - @test present_value(AnnuityDue(ins_jl,n=1)) ≈ 1 - @test present_value(AnnuityImmediate(ins_jl,n=1)) ≈ 1 * .75 / 1.05 - @test present_value(AnnuityDue(ins_jl,n=2)) ≈ 1 + 1 * .75 / 1.05 - @test present_value(AnnuityDue(ins_jl,n=2;certain=2)) ≈ 1 + 1 / 1.05 - @test present_value(AnnuityDue(ins_jl,n=0)) == 0 - - @test survival(ins_jl,1) ≈ .5 + .5 - .5 * .5 - @test all(survival(JointLife((ins.life,ins.life),LastSurvivor(),Frasier())) .== [1.,.75]) - @test present_value(Insurance(ins_jl)) ≈ .25 / 1.05 - @test present_value(Insurance(ins_jl,n=1)) ≈ 0.25 / 1.05 - @test present_value(Insurance(ins_jl,n=0)) ≈ 0 + @test present_value(AnnuityDue(ins_jl)) ≈ 1 + 1 * 0.75 / 1.05 + @test present_value(AnnuityDue(ins_jl, 1)) ≈ 1 + @test present_value(AnnuityImmediate(ins_jl, 1)) ≈ 1 * 0.75 / 1.05 + @test present_value(AnnuityDue(ins_jl, 2)) ≈ 1 + 1 * 0.75 / 1.05 + @test present_value(AnnuityDue(ins_jl, 2; certain = 2)) ≈ 1 + 1 / 1.05 + @test present_value(AnnuityDue(ins_jl, 0)) == 0 + + @test survival(ins_jl, 1) ≈ 0.5 + 0.5 - 0.5 * 0.5 + @test all(survival(JointLife((ins.life, ins.life), LastSurvivor(), Frasier())) .== [1.0, 0.75]) + @test present_value(Insurance(ins_jl)) ≈ 0.25 / 1.05 + @test present_value(Insurance(ins_jl, 1)) ≈ 0.25 / 1.05 + @test present_value(Insurance(ins_jl, 0)) ≈ 0 end @testset "two year no discount" begin ins = LifeContingency( SingleLife( - mort = UltimateMortality([0.0,0.0]), + mortality = UltimateMortality([0.0, 0.0]), issue_age = 0 ), Yields.Constant(0.00) @@ -141,111 +141,111 @@ end @test omega(ins) ≈ 2 @test present_value(AnnuityDue(ins)) ≈ 3 - @test present_value(AnnuityDue(ins;start_time=1)) ≈ 2 - @test present_value(AnnuityDue(ins,n=1)) ≈ 1 - @test present_value(AnnuityDue(ins,n=2)) ≈ 2 - @test_throws BoundsError present_value(AnnuityDue(ins,n=2;start_time=2)) - @test present_value(AnnuityDue(ins,n=3)) ≈ 3 - @test present_value(AnnuityDue(ins,n=0)) == 0 - @test present_value(AnnuityImmediate(ins,n=0)) == 0 - @test present_value(AnnuityImmediate(ins,n=1)) ≈ 1 - @test present_value(AnnuityImmediate(ins,n=1;start_time=1)) ≈ 1 - @test_throws BoundsError present_value(AnnuityImmediate(ins;start_time=2)) - @test present_value(AnnuityImmediate(ins,n=2)) ≈ 2 + @test present_value(AnnuityDue(ins; start_time = 1)) ≈ 2 + @test present_value(AnnuityDue(ins, 1)) ≈ 1 + @test present_value(AnnuityDue(ins, 2)) ≈ 2 + @test_throws BoundsError present_value(AnnuityDue(ins, 2; start_time = 2)) + @test present_value(AnnuityDue(ins, 3)) ≈ 3 + @test present_value(AnnuityDue(ins, 0)) == 0 + @test present_value(AnnuityImmediate(ins, 0)) == 0 + @test present_value(AnnuityImmediate(ins, 1)) ≈ 1 + @test present_value(AnnuityImmediate(ins, 1; start_time = 1)) ≈ 1 + @test_throws BoundsError present_value(AnnuityImmediate(ins; start_time = 2)) + @test present_value(AnnuityImmediate(ins, 2)) ≈ 2 @test present_value(AnnuityImmediate(ins)) ≈ 2 - @test present_value(AnnuityImmediate(ins;certain=0)) ≈ 2 - @test present_value(AnnuityImmediate(ins;certain=2)) ≈ 2 + @test present_value(AnnuityImmediate(ins; certain = 0)) ≈ 2 + @test present_value(AnnuityImmediate(ins; certain = 2)) ≈ 2 @test present_value(Insurance(ins)) ≈ 0 - @test present_value(Insurance(ins,n=1)) ≈ 0 - @test present_value(Insurance(ins,n=0)) ≈ 0 + @test present_value(Insurance(ins, 1)) ≈ 0 + @test present_value(Insurance(ins, 0)) ≈ 0 ins_jl = LifeContingency( JointLife( - lives = (ins.life,ins.life), + lives = (ins.life, ins.life), contingency = LastSurvivor(), joint_assumption = Frasier()), Yields.Constant(0.00) ) - @test survival(ins_jl,0) ≈ 1.0 - @test survival(ins_jl,1) ≈ 1 - @test survival(ins_jl,2) ≈ 1 + @test survival(ins_jl, 0) ≈ 1.0 + @test survival(ins_jl, 1) ≈ 1 + @test survival(ins_jl, 2) ≈ 1 end @testset "two year with interest" begin ins = LifeContingency( SingleLife( - mort = UltimateMortality([0.5,0.5]), + mortality = UltimateMortality([0.5, 0.5]), issue_age = 0 ), Yields.Constant(0.05) ) @test omega(ins) ≈ 2 - @test present_value(AnnuityDue(ins)) ≈ 1 + 1 * .5 * 1 / 1.05 + 1 * .25 / 1.05 ^2 - @test present_value(AnnuityDue(ins,n=1)) ≈ 1 - @test present_value(AnnuityDue(ins,n=2)) ≈ 1 + 1 * .5 * 1 / 1.05 - @test present_value(AnnuityDue(ins,n=3)) ≈ 1 + 1 * .5 * 1 / 1.05 + 1 * .25 / 1.05 ^2 - @test present_value(AnnuityDue(ins,n=0)) ≈ 0 - @test present_value(AnnuityImmediate(ins,n=0)) ≈ 0 - @test present_value(AnnuityImmediate(ins,n=1)) ≈ 1 * .5 * 1 / 1.05 - - @test present_value(Insurance(ins)) ≈ 0.5 / 1.05 + 0.5 * 0.5 / 1.05 ^ 2 - @test present_value(Insurance(ins,n=1)) ≈ 0.5 / 1.05 - @test present_value(Insurance(ins,n=0)) ≈ 0 + @test present_value(AnnuityDue(ins)) ≈ 1 + 1 * 0.5 * 1 / 1.05 + 1 * 0.25 / 1.05^2 + @test present_value(AnnuityDue(ins, 1)) ≈ 1 + @test present_value(AnnuityDue(ins, 2)) ≈ 1 + 1 * 0.5 * 1 / 1.05 + @test present_value(AnnuityDue(ins, 3)) ≈ 1 + 1 * 0.5 * 1 / 1.05 + 1 * 0.25 / 1.05^2 + @test present_value(AnnuityDue(ins, 0)) ≈ 0 + @test present_value(AnnuityImmediate(ins, 0)) ≈ 0 + @test present_value(AnnuityImmediate(ins, 1)) ≈ 1 * 0.5 * 1 / 1.05 + + @test present_value(Insurance(ins)) ≈ 0.5 / 1.05 + 0.5 * 0.5 / 1.05^2 + @test present_value(Insurance(ins, 1)) ≈ 0.5 / 1.05 + @test present_value(Insurance(ins, 0)) ≈ 0 ins_jl = LifeContingency( JointLife( - lives = (ins.life,ins.life), + lives = (ins.life, ins.life), contingency = LastSurvivor(), joint_assumption = Frasier()), Yields.Constant(0.05) ) - @test survival(ins_jl,0) ≈ 1.0 - @test survival(ins_jl,1) ≈ .5 + .5 - .5 * .5 - @test survival(ins_jl,2) ≈ (.25 + .25 - .25 * .25) + @test survival(ins_jl, 0) ≈ 1.0 + @test survival(ins_jl, 1) ≈ 0.5 + 0.5 - 0.5 * 0.5 + @test survival(ins_jl, 2) ≈ (0.25 + 0.25 - 0.25 * 0.25) end @testset "two years" begin ins = LifeContingency( SingleLife( - mort = UltimateMortality([0.5,0.5]), + mortality = UltimateMortality([0.5, 0.5]), issue_age = 0 ), Yields.Constant(0.05) ) @test omega(ins) ≈ 2 - @test present_value(AnnuityDue(ins,n=1)) ≈ 1 - @test present_value(AnnuityDue(ins,n=2)) ≈ 1 + 1 * .5 * 1 / 1.05 - @test present_value(AnnuityDue(ins,n=3)) ≈ 1 + 1 * .5 * 1 / 1.05 + 1 * .25 * 1 / 1.05 ^ 2 - @test present_value(AnnuityDue(ins,n=0)) == 0 - @test present_value(AnnuityImmediate(ins,n=0)) == 0 - @test present_value(AnnuityImmediate(ins,n=1)) ≈ 1 * .5 * 1 / 1.05 + @test present_value(AnnuityDue(ins, 1)) ≈ 1 + @test present_value(AnnuityDue(ins, 2)) ≈ 1 + 1 * 0.5 * 1 / 1.05 + @test present_value(AnnuityDue(ins, 3)) ≈ 1 + 1 * 0.5 * 1 / 1.05 + 1 * 0.25 * 1 / 1.05^2 + @test present_value(AnnuityDue(ins, 0)) == 0 + @test present_value(AnnuityImmediate(ins, 0)) == 0 + @test present_value(AnnuityImmediate(ins, 1)) ≈ 1 * 0.5 * 1 / 1.05 - @test present_value(Insurance(ins)) ≈ 0.5 / 1.05 + 0.5 * 0.5 / 1.05 ^ 2 - @test present_value(Insurance(ins,n=1)) ≈ 0.5 / 1.05 - @test present_value(Insurance(ins,n=0)) ≈ 0 + @test present_value(Insurance(ins)) ≈ 0.5 / 1.05 + 0.5 * 0.5 / 1.05^2 + @test present_value(Insurance(ins, 1)) ≈ 0.5 / 1.05 + @test present_value(Insurance(ins, 0)) ≈ 0 ins_jl = LifeContingency( JointLife( - lives = (ins.life,ins.life), + lives = (ins.life, ins.life), contingency = LastSurvivor(), joint_assumption = Frasier()), Yields.Constant(0.05) ) @test omega(ins_jl) ≈ 2 - @test present_value(AnnuityDue(ins_jl,n=1)) ≈ 1 - @test present_value(AnnuityDue(ins_jl,n=2)) ≈ 1 + 1 * .75 /1.05 - @test present_value(AnnuityDue(ins_jl,n=3)) ≈ 1 + 1 * .75 /1.05 + 1 * survival(ins_jl,2) / 1.05 ^ 2 - @test present_value(AnnuityDue(ins_jl,n=0)) == 0 - @test present_value(AnnuityImmediate(ins_jl,n=0)) == 0 - @test present_value(AnnuityImmediate(ins_jl,n=1)) ≈ 1 * .75 /1.05 + @test present_value(AnnuityDue(ins_jl, 1)) ≈ 1 + @test present_value(AnnuityDue(ins_jl, 2)) ≈ 1 + 1 * 0.75 / 1.05 + @test present_value(AnnuityDue(ins_jl, 3)) ≈ 1 + 1 * 0.75 / 1.05 + 1 * survival(ins_jl, 2) / 1.05^2 + @test present_value(AnnuityDue(ins_jl, 0)) == 0 + @test present_value(AnnuityImmediate(ins_jl, 0)) == 0 + @test present_value(AnnuityImmediate(ins_jl, 1)) ≈ 1 * 0.75 / 1.05 end @@ -254,7 +254,7 @@ t = UltimateMortality(maleMort) @testset "demo mortality" begin i = Yields.Constant(0.05) - ins = LifeContingency(SingleLife(mort = t, issue_age = 0), i) + ins = LifeContingency(SingleLife(mortality = t, issue_age = 0), i) @test l(ins, 0) ≈ 1.0 @test l(ins, 1) ≈ 0.993010000000000 @@ -276,21 +276,21 @@ t = UltimateMortality(maleMort) @test M(ins, 1) ≈ 0.0355801393752753 @test M(ins, 2) ≈ 0.0351775312392208 - @test present_value(Insurance(ins )) ≈ 0.04223728223 - @test present_value(Insurance(ins, n=0)) ≈ 0.0 - @test present_value(Insurance(ins, n=1)) ≈ 0.0066571428571429 - @test present_value(AnnuityDue(ins, n=0)) ≈ 0.0 - @test present_value(AnnuityDue(ins, n=1)) ≈ 1.0 - @test present_value(AnnuityDue(ins, n=2)) ≈ 1.0 + survival(ins,1) / 1.05 - @test present_value(AnnuityImmediate(ins, n=0)) ≈ 0.0 - @test present_value(AnnuityImmediate(ins, n=1)) ≈ survival(ins,1) / 1.05 + @test present_value(Insurance(ins)) ≈ 0.04223728223 + @test present_value(Insurance(ins, 0)) ≈ 0.0 + @test present_value(Insurance(ins, 1)) ≈ 0.0066571428571429 + @test present_value(AnnuityDue(ins, 0)) ≈ 0.0 + @test present_value(AnnuityDue(ins, 1)) ≈ 1.0 + @test present_value(AnnuityDue(ins, 2)) ≈ 1.0 + survival(ins, 1) / 1.05 + @test present_value(AnnuityImmediate(ins, 0)) ≈ 0.0 + @test present_value(AnnuityImmediate(ins, 1)) ≈ survival(ins, 1) / 1.05 - @test present_value(Insurance(ins, n=30)) ≈ 0.0137761089686975 + @test present_value(Insurance(ins, 30)) ≈ 0.0137761089686975 @test N(ins, 26) ≈ 5.156762988852310 @test D(ins, 26) ≈ 0.275358702015970 - @test present_value(AnnuityDue(ins, n=26)) ≈ 14.9562540842669 + @test present_value(AnnuityDue(ins, 26)) ≈ 14.9562540842669 -end \ No newline at end of file +end \ No newline at end of file diff --git a/test/single_life.jl b/test/single_life.jl index b1eeb4e..9aae269 100644 --- a/test/single_life.jl +++ b/test/single_life.jl @@ -2,7 +2,7 @@ @testset "issue age 116" begin t = MortalityTables.table("2001 VBT Residual Standard Select and Ultimate - Male Nonsmoker, ANB") i = Yields.Constant(0.05) - lc = LifeContingency(SingleLife(mort = t.ultimate, issue_age = 116), i) + lc = LifeContingency(SingleLife(mortality = t.ultimate, issue_age = 116), i) @test l(lc, 0) ≈ 1.0 @@ -27,13 +27,13 @@ @test present_value(Insurance(lc)) ≈ 0.9418373716628330 @test present_value(AnnuityDue(lc)) ≈ 1.2214151950805000 - + qs = t.ultimate[116:118] - @test present_value(Insurance(lc, n=3)) ≈ sum(qs .* [1;cumprod(1 .- qs[1:2])] .* [1.05 ^ -t for t in 1:3]) - @test present_value(AnnuityDue(lc, n=3)) ≈ sum([1;cumprod(1 .- qs[1:2])] .* [1.05 ^ -t for t in 0:2]) - - @test LifeContingencies.V(lc,1) == reserve_premium_net(lc,1) - @test LifeContingencies.v(lc,1) == Yields.discount(lc,1) + @test present_value(Insurance(lc, 3)) ≈ sum(qs .* [1; cumprod(1 .- qs[1:2])] .* [1.05^-t for t = 1:3]) + @test present_value(AnnuityDue(lc, 3)) ≈ sum([1; cumprod(1 .- qs[1:2])] .* [1.05^-t for t = 0:2]) + + @test LifeContingencies.V(lc, 1) == reserve_premium_net(lc, 1) + @test LifeContingencies.v(lc, 1) == Yields.discount(lc, 1) @test LifeContingencies.A(lc) == present_value(Insurance(lc)) @test LifeContingencies.ä(lc) == present_value(AnnuityDue(lc)) @test LifeContingencies.a(lc) == present_value(AnnuityImmediate(lc)) @@ -44,12 +44,12 @@ @testset "issue age 30" begin t = MortalityTables.table("2001 VBT Residual Standard Select and Ultimate - Male Nonsmoker, ANB") i = Yields.Constant(0.05) - life = SingleLife(mort = t.select[30], issue_age = 30) + life = SingleLife(mortality = t.select[30], issue_age = 30) ins = LifeContingency(life, i) - @test life.issue_age == SingleLife(mort = t.select[30]).issue_age + @test life.issue_age == SingleLife(mortality = t.select[30]).issue_age @test life.issue_age == SingleLife(t.select[30]).issue_age - @test life.issue_age == SingleLife(t.select,issue_age= 30).issue_age + @test life.issue_age == SingleLife(t.select, issue_age = 30).issue_age @test l(ins, 0) ≈ 1.0 @@ -81,10 +81,10 @@ @test reserve_premium_net(ins, 2) ≈ 0.0119711961204193 qs = t.select[30][30:55] - @test present_value(Insurance(ins, n=26)) ≈ sum(qs .* [1;cumprod(1 .- qs[1:25])] .* [1.05 ^ -t for t in 1:26]) - @test present_value(AnnuityDue(ins, n=26)) ≈ sum([1;cumprod(1 .- qs[1:25])] .* [1.05 ^ -t for t in 0:25]) + @test present_value(Insurance(ins, 26)) ≈ sum(qs .* [1; cumprod(1 .- qs[1:25])] .* [1.05^-t for t = 1:26]) + @test present_value(AnnuityDue(ins, 26)) ≈ sum([1; cumprod(1 .- qs[1:25])] .* [1.05^-t for t = 0:25]) - @test premium_net(ins, 26) ≈ LifeContingencies.A(ins, 26) / LifeContingencies.ä(ins, 26) + @test premium_net(ins, 26) ≈ LifeContingencies.A(ins, 26) / LifeContingencies.ä(ins, 26) end end \ No newline at end of file diff --git a/wip_benchmark.jl b/wip_benchmark.jl new file mode 100644 index 0000000..706ad62 --- /dev/null +++ b/wip_benchmark.jl @@ -0,0 +1,22 @@ +using LifeContingencies +using MortalityTables +using Yields +import LifeContingencies: V, ä # pull the shortform notation into scope +using BenchmarkTools + +# load mortality rates from MortalityTables.jl +vbt2001 = MortalityTables.table("2001 VBT Residual Standard Select and Ultimate - Male Nonsmoker, ANB") + +issue_age = 30 +life = SingleLife( # The life underlying the risk + mort = vbt2001.select[issue_age], # -- Mortality rates +) + +yield = Yields.Constant(0.05) # Using a flat 5% interest rate + +lc = LifeContingency(life, yield) # LifeContingency joins the risk with interest + +@benchmark LifeContingencies.N(lc, 10) + +ins = Insurance(lc) +premium_net(lc)