Migrating to 3.0

3.0 preserves the public textbook constructor names and rebuilds their probability and valuation calculations over a shared finite-state engine. Where results change, 3.0 is correcting them; each change is listed below. Use this guide alongside NEWS.

Environment and fractional assumptions

2.x3.0
Julia 1.6 floorJulia 1.10 or newer
MortalityTables 2MortalityTables 3
Uniform()UniformDeaths()
Constant()ConstantForce()
Balducci()Balducci()

FinanceCore 3 is required. Transducers is no longer a direct dependency. Rename explicit fractional-assumption arguments as well as any imports. Historical 2.x snippets on this page illustrate the old API and are not run by the documentation build.

# 2.x
SingleLife(rates; fractional_assump=Uniform())
using LifeContingencies, MortalityTables
rates = UltimateMortality([0.5, 0.5, 1.0]; start_age=60)
life = SingleLife(rates; fractional_assump=UniformDeaths())
survival(life, 0.5)
0.75

Conditional fractional survival now composes consistently. Recheck fractional-age reference values rather than retaining results from the earlier conditional formulas.

Products and guarantees

Old behaviorNew behavior and action
Due guarantee included one extra installmentcertain*f guaranteed installments; check due-annuity results
certain=0 could guarantee a deferred first paymentZero guarantees none
certain=nothing used for no guaranteeOmit it or pass certain=0
Zero term produced an internal ZeroBenefitEmpty stream, PV zero; use the public views instead of inspecting concrete types
Invalid parameters could fail during projectionInvalid term, frequency, guarantee or deferral errors at construction

The guarantee remains unconditional during deferral, and it continues after extinction or observed death. This is separate from survival:

lc = LifeContingency(life, 0.0)
@assert probability(AnnuityDue(lc, 3; certain=1)) == [1.0, 0.5, 0.25]
@assert probability(AnnuityDue(lc, 2; start_time=1, certain=0)) == [0.5, 0.25]
@assert isempty(cashflows(Insurance(lc, 0)))
(zero_term_pv = present_value(Insurance(lc, 0)),
 guaranteed_annuity_cashflows = cashflows(AnnuityDue(lc, 3; certain=1)))
(zero_term_pv = 0.0, guaranteed_annuity_cashflows = [1.0, 0.5, 0.25])

Product survival, probability, cashflows and discount views now return vectors; timepoints remains a range. Downstream code depending on iterator types must adapt.

Mortality support and statuses

ChangeAction
alive=false takes effectExpect zero survival and state-contingent benefits; guarantees remain payable
Last-survivor horizon follows the longer lifeRecheck joint-life tails previously cut off
Only Independent() (alias Frasier()) is implementedOther joint assumptions now error instead of silently using independence
Conditional joint survival at an impossible in-force date errorsHandle a DomainError instead of a NaN
A rate vector ending below 1 is incompleteSupply longer rates, or an explicit supported term

Appending a terminal rate of 1 imposes certain death in that last year; it is an assumption change, not a neutral way to fix an incomplete table. Choose a term if cover actually ends with the available data:

short = SingleLife(UltimateMortality([0.1, 0.2]; start_age=60))
# Whole life on `short` errors: survival after the known rates is unspecified.
present_value(Insurance(short, 0.05, 2))
0.25850340136054417

omega(life) is duration from issue, whereas omega(rate_vector) is the last attained age with a rate. Parametric lives require an explicit issue age and textbook products need a supported term. Their horizon can be infinite or fractional; do not blindly cast it to Int. See Mortality support.

Future PVs and reserves

present_value(ins, s) now includes annuity-due installments at s; end-period payments have settled. Pass phase=:after for the old strict-future cutoff. It retains the issue distribution unless state is supplied. It now discounts using discount(curve, s, t). Results change on non-flat curves where the old shifted-time discount factors differed. It remains an unconditional tail, not a conditional reserve.

Custom non-flat curves need their own interval method. If your 2.x extension defines only FinanceCore.discount(curve, t), add FinanceCore.discount(curve, from, to) = discount(curve, to) / discount(curve, from) for your curve type, using its existing issue-date factors. FinanceCore 3 has no generic interval fallback for custom curves: omitting the method raises a MethodError. The engine uses interval discounting even for issue-date valuation, including the identity factor discount(curve, 0, 0). See the runnable custom-curve example.

reserve_premium_net now values the remaining net stream prospectively. Its anniversary convention is unchanged: deaths through s settled, premium at s included. It takes an integer duration and retains the issue premium. Late-duration results can change because the old subtraction lost precision.

lc_interest = LifeContingency(life, 0.05)
(unconditional_tail = present_value(Insurance(lc_interest), 1),
 in_force_reserve = reserve_premium_net(lc_interest, 1),
 known_alive_reserve = reserve_premium_net(lc_interest, 1; state=:alive))
(unconditional_tail = 0.46485260770975056, in_force_reserve = 0.13315579227696395, known_alive_reserve = 0.13315579227696395)

Joint reserves condition on the contingency alone by default. Use state=(:alive, :dead) or another observed pair when individual statuses are known. See Joint lives.

Names and extension code

The unexported internal types WholeLife, Term, ZeroBenefit and annuity payable types were removed; use constructors and documented views.

The unexported E(lc, t, s) was removed. Replace it with APV(lc, s + t) / APV(lc, s), provided the denominator is nonzero. Here s is a duration from issue, not an attained age. This is the conditional pure endowment over the next t years, on the original mortality and curve basis:

s, t = 1, 1
conditional_endowment = APV(lc_interest, s + t) / APV(lc_interest, s)
@assert conditional_endowment ≈ survival(life, s, s+t) * discount(lc_interest, s, s+t)
conditional_endowment
0.4761904761904762

The exports insurance, annuity_due and annuity_immediate were removed because those names were never defined.

New

  • FirstToDie() adds first-death joint-life benefits.
  • Lives, products and LifeContingency now broadcast as scalars.
  • LifeContingencies.Experimental adds the finite-state engine.

For the experimental engine, import its interfaces explicitly, and expect possible changes in minor releases. Start with Building multi-state models, rather than extending the old product hierarchy's death-benefit defaults.

Additional 3.0 conventions

  • Every JointLife call must specify contingency=FirstToDie() or contingency=LastSurvivor().
  • Independent() names the independence assumption; Frasier remains an alias.
  • N and M now reject incomplete mortality and uncovered partial terminal years.
  • Endowment(lc, n) expresses explicit maturity without changing mortality rates.
  • present_value(product, t; state=...) values known statuses, including dead annuitants with guarantees.

Priced policies and policy-value columns

PricedPolicy(benefit, premiums; premium=...) stores the established premium; omission prices it once by equivalence. policy_value(policy, times; basis=...) uses a complete new LifeContingency without changing that premium. premium_net(benefit, premiums) explicitly calculates an equivalence premium. policy_value takes a priced policy, with no product-pair reserve shortcut. reserve_premium_net(lc, times) establishes its premium on the supplied lc basis and accepts only state and phase keywords. Both column methods return numeric vectors. Settled contracts now return zero, including at and after endowment or term maturity. See Valuation and reserves for worked examples.