Prospective Reserves and the ts Parameter#
The prospective reserve \({}_{t}V\) at elapsed time \(t\) for a contract issued to a life aged \(x\) is the difference between the actuarial present value of all future benefits and the actuarial present value of all future net premiums, both evaluated from the current attained age \(x + t\):
In Lactuca, both present values are computed by calling the same äx, ax, Ax (or
joint-life equivalents) with the effective age \(x + t\). The ts parameter is the
mechanism that shifts the calculation to that age without requiring the caller to
recompute \(x + t\) and adjust the remaining term manually.
The ts parameter (ts is short for time shift) represents the number of years
elapsed since the reference age x. Two equivalent parameterisations are equally valid:
x = inception age, ts = total elapsed time (years since the contract start).
x = age at the last elapsed whole-year anniversary, ts = fractional remainder of the current policy year.
Both produce identical results because the engine only uses \(x + ts\) internally.
It is available on all main life-contingency methods: äx, ax, Ax, joint-life
annuities (äxy, äxyz, ajoint, …), first-death insurances (Axy, Axyz,
Afirst), as well as the pure endowment nEx.
ts enables prospective reserve calculations at any point in the life of a
contract:
Integer
ts(e.g.ts=3): the valuation falls exactly on a policy anniversary. This is the standard case for annual statutory reserves.Fractional
ts(e.g.ts=3.5): the valuation falls between two anniversaries. The survival and discounting calculations are mathematically exact. When aGrowthRateis also provided, Lactuca applies the actuarial convention that benefit revaluations occur at whole-year anniversaries; aUserWarningis emitted so the user can review that convention before accepting the result (see Growth rates andts).
Note
ts vs t — two orthogonal concepts
In actuarial notation the symbol \(t\) typically denotes a future duration — the
time span measured forward from a given age, as in the survival probability
\({}_{t}p_x = l_{x+t}/l_x\). The parameter name ts in Lactuca is deliberately
distinct: it represents elapsed time measured backward, from the reference point
to the current valuation date. The engine converts ts into an age shift and a
duration reduction before any survival calculation is performed. Once the
effective age and duration are established, the internal survival functions use t
(duration) in the classical sense.
How ts transforms the calculation#
The caller always passes the reference age x and the full remaining term
n measured from that reference. The engine applies four internal transformations:
The shift first absorbs any deferment: if \(ts \le d\), the deferment decreases and the duration is unchanged (\(ts_{\text{eff}} = 0\), \(n_{\text{eff}} = n\)). Only the portion of \(ts\) that exceeds \(d\) shortens the remaining duration. In the common case \(d = 0\) this simplifies to \(n_{\text{eff}} = n - ts\).
Note
Pure endowments (nEx, nExy, nExyz, nEjoint) do not accept a deferment
parameter, so the formula simplifies to \(x_{\text{eff}} = x + ts\) and
\(n_{\text{eff}} = \max(n - ts, 0)\).
The call signature is therefore:
lt.äx(x, n=n, ts=ts, ir=ir) # d=0 → n_eff = n - ts
lt.äx(x, n=n, d=d, ts=ts, ir=ir) # general case
When ts=0 (the default) no shift is applied; the result is the standard actuarial
present value at age x — the initial cost of the contract.
Note
If \(ts \ge d + n\) the contract has fully elapsed and the present value is zero.
Lactuca returns 0.0 immediately in that case without raising an error. Passing a
negative ts raises ValueError.
For whole-life calculations (n=None) the effective duration is computed as
\(n_{\text{eff}} = \omega - x_{\text{eff}} - d_{\text{eff}}\), where \(\omega\) is the
terminal age of the table. The shift shortens the residual lifetime automatically;
no finite n is required.
Basic usage#
The most common case is an integer ts: the valuation falls on a future policy
anniversary. The calculation reduces to a standard annuity starting from the attained
age \(x + ts\):
from lactuca import LifeTable, config
config.decimals.annuities = 4
lt = LifeTable("GRMF95", "m", interest_rate=0.03)
# Standard anniversary reserve (ts = 0, no shift)
reserve_ann = lt.äx(50, n=15)
print(reserve_ann)
# APV of premium annuity-due from attained age 53 (remaining term 12) — one leg of
# the prospective reserve formula, not ${}_t V$ by itself (see worked example below)
reserve_3 = lt.äx(50, n=15, ts=3)
print(reserve_3)
config.reset_to_defaults()
Fractional ts is used when the valuation date falls between two anniversaries.
The survival and discounting are computed exactly for the fractional age:
from lactuca import LifeTable, config
config.decimals.annuities = 4
lt = LifeTable("GRMF95", "m", interest_rate=0.03)
# Mid-year valuation: 0.5 years since last anniversary
reserve_mid = lt.äx(50, n=15, ts=0.5)
print(reserve_mid)
# Quarter-year valuation: 0.25 years since last anniversary
reserve_off = lt.äx(50, n=10, ts=0.25)
print(reserve_off)
# 2.75 years since inception: 2 full anniversaries elapsed, 0.75 yr into the third year
reserve_multi = lt.äx(50, n=10, ts=2.75)
print(reserve_multi)
config.reset_to_defaults()
ts vs d — key distinction#
See also
For a standalone introduction to the d parameter — concept, formulas, code examples,
and joint-life products — see Deferred Life Contingencies.
ts and d look similar but serve different roles:
Parameter |
Meaning |
Affects |
|---|---|---|
|
Time already elapsed since last anniversary |
Starting age, payment-grid origin, growth schedule |
|
Future deferral before payments begin |
First payment is at \(t = d_{\text{eff}}\) after current valuation age |
The crucial algebraic interaction: \(ts\) consumes deferment first. The deferment that remains after the shift is \(d_{\text{eff}} = \max(d - ts, 0)\), and only the portion of \(ts\) beyond \(d\) reduces the term.
\(ts\) vs \(d\) |
\(d_{\text{eff}}\) |
\(n_{\text{eff}}\) |
|---|---|---|
\(ts < d\) (shift is within deferment) |
\(d - ts\) |
\(n\) (unchanged) |
\(ts = d\) (shift exactly exhausts deferment) |
\(0\) |
\(n\) (unchanged) |
\(ts > d\) (shift extends past deferment) |
\(0\) |
\(n - (ts - d)\) |
Example — a 5-year-deferred, 20-year annuity evaluated at ts=0.5 (mid-year):
from lactuca import LifeTable, config
config.decimals.annuities = 4
lt = LifeTable("GRMF95", "m", interest_rate=0.03)
# ts=0.5 < d=5 → deferment is only partially consumed: d_eff = 4.5, n_eff = 20
reserve_combined = lt.äx(50, n=20, d=5, ts=0.5)
print(reserve_combined)
# ts=6 > d=5 → deferment fully consumed: d_eff = 0, n_eff = 20 - (6-5) = 19
reserve_past_d = lt.äx(50, n=20, d=5, ts=6)
print(reserve_past_d)
config.reset_to_defaults()
Note
ts with explicit cashflow_times
For methods that accept an explicit payment schedule (cashflow_times), ts still
applies but with two differences:
Each payment time is shifted relative to the valuation date: payments at times \(< ts_{\text{eff}}\) (in the past) are silently dropped.
Integer enforcement (
config.force_integer_ts) is bypassed — when the caller controls the exact payment grid, fractional shifts never raise or warn.
Integer-only mode (force_integer_ts)#
By default, Lactuca accepts any non-negative float as ts. Without a GrowthRate,
fractional ts is mathematically exact: survival probabilities and discount factors
are computed at the precise fractional attained age. When a GrowthRate is active,
the growth schedule is advanced by int(ts) complete anniversaries (see
Growth rates and ts below). For portfolios where only
anniversary-date reserves are required (e.g. end-of-year statutory convention), set
config.force_integer_ts = True to make Lactuca raise a ValueError on any
fractional ts:
from lactuca import LifeTable, config
config.force_integer_ts = True # fractional ts now raises ValueError
config.decimals.annuities = 4
lt = LifeTable("GRMF95", "m", interest_rate=0.03)
lt.äx(50, n=15, ts=0.5)
# ValueError: [äx] Parameter 'ts' (shift) must be an integer value (got ts=0.5).
# To allow fractional shifts, set 'Config.force_integer_ts = False'. Note that GrowthRate
# schedules use whole-year anniversary indices, consistent with standard actuarial convention.
config.reset_to_defaults() # restore defaults after the example above
The following table summarises all input combinations:
|
|
|
Behaviour |
|---|---|---|---|
Integer |
|
Any |
Accepted silently |
Fractional |
|
No |
Accepted silently |
Fractional |
|
Yes |
|
Fractional |
|
Any |
|
Note
force_integer_ts is enforced by annuity and insurance methods (ax, äx,
Ax, and all joint-life variants) in both scalar and batch mode.
Pure endowment methods (nEx, nExy, nExyz, nEjoint) accept fractional ts in
scalar calls regardless of force_integer_ts (no GrowthRate on endowments; the
endowment dispatcher does not pass ts_integer to shift validation). In batch mode
(array x or array ages), the same force_integer_ts check applies as for annuities
and insurances.
Piecewise interest rate with ts#
For non-constant InterestRate term structures, the engine automatically calls
ir.shifted(ts) to align the yield curve with the valuation date. No user action
is needed:
from lactuca import LifeTable, InterestRate, config
config.decimals.annuities = 4
# Piecewise curve: 2.5% for years 0–5, 3.0% for years 5–10, 3.5% thereafter
# 2 terms → 3 rates required (last rate applies indefinitely)
ir = InterestRate(terms=[5, 5], rates=[0.025, 0.030, 0.035])
lt = LifeTable("GRMF95", "m")
# The yield curve is automatically shifted by ts=2.5 yr internally:
# effective curve from valuation date = 2.5% for 2.5 yr, 3.0% for 5 yr, 3.5% thereafter
reserve = lt.äx(50, n=10, ts=2.5, ir=ir)
print(reserve)
config.reset_to_defaults()
Note
ir.shifted(ts) is only invoked when ts > 0 and the interest rate is non-constant.
A flat rate is translation-invariant and is never shifted.
Growth rates and ts#
When a GrowthRate schedule is combined with ts, the engine calls gr.shifted(ts)
internally. Benefit revaluations follow the actuarial convention of whole-year policy
anniversaries: shifted() advances the schedule by int(ts) completed anniversaries,
not by the raw ts value. This is correct and expected for integer ts:
from lactuca import LifeTable, GrowthRate, config
config.decimals.annuities = 4
lt = LifeTable("GRMF95", "m", interest_rate=0.03)
# Growth schedule: 3% for first 2 years, then 2% indefinitely
gr = GrowthRate(rates=[0.03, 0.02], terms=[2])
# ts=2: exactly 2 anniversaries elapsed → gr.shifted(2) consumes the first segment
# → remaining schedule from valuation date: 2% (constant second segment)
reserve = lt.äx(50, n=8, ts=2, gr=gr)
print(reserve)
config.reset_to_defaults()
With fractional ts and a GrowthRate, the same integer-anniversary convention
applies: ts=2.5 advances the schedule by int(2.5) = 2 anniversaries, not 2.5.
Because this truncation may be unexpected, Lactuca emits a UserWarning to prompt
the user to confirm the convention is appropriate for their calculation:
from lactuca import LifeTable, GrowthRate, config
config.decimals.annuities = 4
lt = LifeTable("GRMF95", "m", interest_rate=0.03)
gr = GrowthRate(rate=0.02) # constant 2% growth schedule
reserve = lt.äx(50, n=15, ts=0.5, gr=gr)
# UserWarning: [äx] Fractional 'ts' (shift) detected (got ts=0.5).
# GrowthRate schedules use whole-year anniversary indices, consistent with
# actuarial convention. Set 'Config.force_integer_ts = True' to reject
# fractional shifts.
print(reserve)
config.reset_to_defaults()
Note
For fractional ts with a GrowthRate:
Survival and discounting are computed exactly at the fractional attained age.
Growth indexing follows the convention documented in Growth Rate Conventions: the schedule is advanced by
int(ts)complete anniversaries. The growth factor in force during the fractional year before the next anniversary is that of the last whole-year milestone — consistent with the actuarial standard that benefit revaluations (CPI indexation, salary scales) operate on complete policy years.
To require strictly integer ts and bypass this convention entirely, set
config.force_integer_ts = True.
For further detail on growth shifts see Growth Rate Conventions.
Mode compatibility#
|
Integer |
Fractional |
|---|---|---|
|
✅ |
✅ |
|
✅ |
✅ |
|
✅ |
✅ |
|
✅ |
✅ |
All four modes support fractional ts. For highest accuracy at fractional ages use
discrete_precision (exact fractional-\(\ell_x\) interpolation via config.lx_interpolation,
default "linear" / UDD) or continuous_precision (numerical integration over the
unrounded \(\ell_x\) curve — respects whichever lx_interpolation is active).
force_mortality_method applies only in continuous modes; see Force of Mortality Methods.
Portfolio reserves#
Compute reserves for multiple policies in one vectorized batch call or policy-by-policy.
For each policy pass the reference age, the full remaining term from that reference, and
the elapsed fraction — the engine applies the shift internally. When every policy shares
one LifeTable, pass aligned arrays for x, n, and ts (see Batch Calculations).
For joint-life reserves with aligned ts arrays, see Joint-Life Calculations § Batch
joint-life calculations.
import numpy as np
from lactuca import LifeTable, config
config.decimals.annuities = 4
lt = LifeTable("GRMF95", "m", interest_rate=0.03)
# Reference age (last anniversary), full remaining term, elapsed fraction this year
ages = [50, 55, 60, 65]
terms = [20, 15, 10, 5]
shifts = [0.0, 0.25, 0.5, 0.75]
# Vectorized batch (preferred when all policies use the same table)
reserves = lt.äx(ages, n=terms, ts=shifts)
print(reserves)
# Equivalent list comprehension (same inputs, one call per policy)
reserves_loop = [
lt.äx(age, n=term, ts=ts_val)
for age, term, ts_val in zip(ages, terms, shifts)
]
assert np.allclose(reserves, reserves_loop)
config.reset_to_defaults()
Methods that support ts#
ts is available on all annuity, insurance, and pure-endowment methods:
Category |
Methods |
|---|---|
Annuities (single-life) |
|
Annuities (joint-life) |
|
Insurances (single-life) |
|
Insurances (first-death) |
|
Pure endowments |
|
Probability, commutation, and life-expectancy methods do not accept ts and
always operate at the age passed directly. To evaluate the \(t\)-year survival
probability at the attained age after a shift, pass x + ts explicitly and use t
as the forward duration:
# t-year survival from the shifted attained age (t here is a duration, not ts)
survival = lt.tpx(x + ts, t=n)
Here t=n is the future duration (years to survive), wholly distinct from ts
(years already elapsed).
See also#
Life Annuities —
äx,ax: the premium annuity in \(P = A_x / \ddot{a}_x\)Life Insurances —
Ax,nEx: the insurance numerator in the premium equationGrowth Rate Conventions — how
GrowthRate.shifted()aligns benefit revaluation with anniversariesInspecting Cash Flows — examine the cash-flow arrays underlying the reserve
Calculation Modes — precision trade-offs for fractional-age calculations
Batch Calculations — compute \(_tV\) for a full portfolio in one vectorized call (building block for BEL / IFRS 17 FCF — not full GMM, RA, or CSM; see Regulatory reporting scope in Calculation Modes)
Actuarial Formulas — formal prospective reserve formula