"""Reserves - Computes recursive, interim or modified reserves
MIT License. Copyright 2022-2023 Terence Lim
"""
import math
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from typing import Callable, Dict, Any
from actuarialmath import PolicyValues, Contract
[docs]class Reserves(PolicyValues):
"""Compute recursive, interim or modified reserves"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._reserves = {'V': {}}
self.T = 0
#
# Set up reserves table for recursion
#
[docs] def set_reserves(self, T: int = 0, endowment: int | float= 0,
V: Dict[int, float] | None = None) -> "Reserves":
"""Set values of the reserves table and the endowment benefit amount
Args:
T : max term of policy
V : reserve values, keyed by time t
endowment : endowment benefit amount
Examples:
>>> life = Reserves().set_reserves(T=3)
"""
if T:
self.T = T
if V:
self._reserves['V'].update(V)
self.T = max(len(V) - 1, self.T)
self._reserves['V'][0] = 0 # initial reserve is 0 by equivalence
self._reserves['V'][self.T] = endowment # n_V is 0 or endowment
return self
[docs] def fill_reserves(self, x: int, s: int = 0, reserve_benefit: bool = False,
contract: Contract | None = None) -> "Reserves":
"""Iteratively fill in missing values in reserves table
Args:
x : age selected
s : starting from s years after selection
reserve_benefit : whether benefit includes value of reserves
contract : policy contract terms and expenses
"""
contract = contract or Contract()
for _ in range(2):
for t in range(self.T + 1):
if self._reserves['V'].get(t, None) is not None:
continue
if t == contract.T:
v = self.t_V(x=x, s=s, t=t, premium=0, benefit=lambda t: 0,
per_policy = -contract.endowment)
elif t == 1:
v = self.t_V(x=x, s=s, t=t, premium=contract.premium,
benefit=lambda t: contract.benefit,
reserve_benefit=reserve_benefit,
per_premium=contract.initial_premium,
per_policy=contract.initial_policy)
elif t == 0:
v = 0
else:
v = self.t_V(x=x, s=s, t=t, premium=contract.premium,
benefit=lambda t: contract.benefit,
reserve_benefit=reserve_benefit,
per_premium=contract.renewal_premium,
per_policy=contract.renewal_policy)
if v is not None:
self._reserves['V'][t] = v
return self
[docs] def V_plot(self, ax: Any = None, color: str = 'r', title: str = ''):
"""Plot values from reserves tables
Args:
title : title to display
color : color to plot curve
Examples:
>>> life.V_plot(title=f"Reserves for term insurance")
"""
if ax is None:
fig, ax = plt.subplots(1, 1)
y = [self._reserves['V'].get(t, None) for t in range(self.T + 1)]
ax.plot(list(range(self.T + 1)), y, '.', color=color)
ax.set_title(title)
ax.set_ylabel(f"$_tV$", color=color)
ax.set_xlabel(f"t")
[docs] def reserves_frame(self):
"""Returns reserves table as a DataFrame"""
return pd.DataFrame(self._reserves)\
.rename_axis('t')\
.rename(columns={'V':'V_t'})
#
# Reserves recursion
#
[docs] def t_V_backward(self, x: int, s: int = 0, t: int = 0, premium: float = 0,
benefit: Callable = lambda t: 1,
per_premium: float = 0, per_policy: float = 0,
reserve_benefit: bool = False) -> float | None:
"""Backward recursion (with optional reserve benefit)
Args:
x : age selected
s : starting s years after selection
t : year of reserve to solve
benefit : benefit amount at t+1
premium : amount of premium paid just after t
per_premium : expense per $ premium
per_policy : expense per policy
reserve_benefit : whether reserve value at t+1 included in benefit
"""
if t+1 not in self._reserves['V']:
return None
V = self._reserves['V'][t+1]
b = benefit(t+1) + V * reserve_benefit # total death benefit
if V == b: # special case if death benefit == forward reserve
V = b
else:
if V:
V *= self.p_x(x=x, s=s+t)
if b:
V += self.q_x(x=x, s=s+t) * b
V = V * self.interest.v - (premium*(1 - per_premium) - per_policy)
return V
[docs] def t_V_forward(self, x: int, s: int = 0, t: int = 0, premium: float = 0,
benefit: Callable = lambda t: 1,
per_premium: float = 0, per_policy: float = 0,
reserve_benefit: bool = False) -> float | None:
"""Forward recursion (with optional reserve benefit)
Args:
x : age selected
s : starting s years after selection
t : year of reserve to solve
benefit : benefit amount at t
premium : amount of premium paid just after t-1
per_premium : expense per $ premium
per_policy : expense per policy
reserve_benefit : whether reserve value at t included in benefit
"""
if t-1 not in self._reserves['V']:
return None
V = self._reserves['V'][t-1]
V = (V + premium*(1 - per_premium) - per_policy) / self.interest.v
if benefit(t):
V -= self.q_x(x=x, s=s+t-1) * benefit(t)
if not reserve_benefit:
V /= self.p_x(x=x, s=s+t-1)
return V
[docs] def t_V(self, x: int, s: int = 0, t: int = 0,
premium: float = 0, benefit: Callable = lambda t: 1,
reserve_benefit: bool = False,
per_premium: float = 0, per_policy: float = 0) -> float | None:
"""Solve year-t reserves by forward or backward recursion
Args:
x : age selected
s : starting s years after selection
t : year of reserve to solve
benefit : benefit amount
premium : amount of premium
per_premium : expense per $ premium
per_policy : expense per policy
reserve_benefit : whether reserve value included in benefit
Examples:
>>> G, x = 368.05, 0
>>> def fun(P): # solve net premium from expense reserve equation
>>> return life.t_V(x=x, t=2, premium=G-P, benefit=lambda t: 0,
>>> per_policy=5+.08*G)
>>> P = life.solve(fun, target=-23.64, grid=[.29, .31]) / 1000
"""
if t in self._reserves['V']: # already solved in reserves table
return self._reserves['V'][t]
V = self.t_V_forward(x=x, s=s, t=t, premium=premium,
benefit=benefit,
reserve_benefit=reserve_benefit,
per_premium=per_premium,
per_policy=per_policy)
if V is not None:
return V
V = self.t_V_backward(x=x, s=s, t=t, premium=premium,
benefit=benefit,
reserve_benefit=reserve_benefit,
per_premium=per_premium,
per_policy=per_policy)
if V is not None:
return V
#
# Interim reserves
#
[docs] def r_V_backward(self, x: int, s: int = 0, r: float = 0,
benefit: int = 1) -> float | None:
"""Backward recursion for interim reserves
Args:
x : age of selection
s : years after selection
r : solve for interim reserve at fractional year x+s+r
benefit : benefit amount in year x+s+1
"""
s = int(s + r)
r = r - math.floor(r)
if s+1 not in self._reserves['V']: # forward recursion
return None
V = self._reserves['V'][s+1]
if V:
V *= self.p_r(x, s=s, r=r, t=1-r)
if benefit:
V += self.q_r(x, s=s, r=r, t=1-r) * benefit
V = V * self.interest.v_t(1-r)
return V
[docs] def r_V_forward(self, x: int, s: int = 0, r: float = 0,
premium: float = 0, benefit: int = 1) -> float | None:
"""Forward recursion for interim reserves
Args:
x : age of selection
s : years after selection
r : solve for interim reserve at fractional year x+s+r
benefit : benefit amount in year x+s+1
premium : premium amount just after year x+s
"""
s = int(s + r)
r = r - math.floor(r)
if s not in self._reserves['V']:
return None
V = self._reserves['V'][s]
V = (V + premium) / self.interest.v_t(r)
if benefit:
V -= self.q_r(x, s=s, t=r) * benefit * self.interest.v_t(1-r)
V /= self.p_r(x, s=s, t=r)
return V
#
# Full Preliminary Term (FPT) modified reserves
#
[docs] def FPT_premium(self, x: int, s: int = 0, n: int = PolicyValues.WHOLE,
b: int = 1, first: bool = False) -> float:
"""Initial or renewal Full Preliminary Term premiums
Args:
x : age of selection
s : years after selection
n : term of insurance
b : benefit amount in year x+s+1
first : calculate year 1 (True) or year 2+ (False) FPT premium
"""
if first:
return self.net_premium(x, s=s, b=b, t=1)
else:
return self.net_premium(x, s=s+1, b=b, t=self.add_term(n, -1))
[docs] def FPT_policy_value(self, x: int, s: int = 0, t: int = 0, b: int = 1,
n: int = PolicyValues.WHOLE,
endowment: int = 0, discrete: bool = True) -> float:
"""Compute Full Preliminary Term policy value at time t
Args:
x : age of selection
s : years after selection
n : term of insurance
t : year of policy value to calculate
b : benefit amount in year x+s+1
endowment : endowment amount
discrete : fully discrete (True) or continuous (False) insurance
"""
if t in [0, 1]: # FPT is 0 at t = 0 or 1
return 0
else:
return self.net_policy_value(x, s=s+1, t=t-1, n=self.add_term(n,-1),
b=b, endowment=endowment,
discrete=discrete)
if __name__ == "__main__":
from actuarialmath.sult import SULT
from actuarialmath.policyvalues import Contract
life = SULT()
x, T, b = 50, 20, 500000 # $500K 20-year term insurance for (50)
P = life.net_premium(x=x, t=T, b=b)
life.set_reserves(T=T)\
.fill_reserves(x=x, contract=Contract(premium=P, benefit=b))
life.V_plot(title=f"Reserves for ${b} {T}-year term insurance issued to ({x})")
if False:
print("SOA Question 7.31: (E) 0.310")
x = 0
life = Reserves().set_reserves(T=3)
print(life._reserves)
G = 368.05
def fun(P): # solve net premium from expense reserve equation
return life.t_V(x=x, t=2, premium=G-P, benefit=lambda t: 0,
per_policy=5+.08*G)
P = life.solve(fun, target=-23.64, grid=[.29, .31]) / 1000
print(P)
print()
from actuarialmath.sult import SULT
print("SOA Question 7.13: (A) 180")
life = SULT()
V = life.FPT_policy_value(40, t=10, n=30, endowment=1000, b=1000)
print(V)
print()