"""Recursion - Applies recursion, shortcut and actuarial formulas
MIT License. Copyright 2022-2023 Terence Lim
"""
from typing import Tuple, Any
import matplotlib.pyplot as plt
from actuarialmath import Reserves
from IPython.display import display_latex, display_pretty
from IPython import get_ipython
_depth = 3
class _Blog:
"""Helper to track and display recursion steps"""
_notebook: bool = False
_latex: bool = False
def __init__(self, label, *args, levels: int = _depth, verbose: bool = True,
width: int = 80):
self.title = f" *{label} {' '.join(args)} :=" # to identify this stack
self.levels = levels # maximum levels to indent
self.width = width # line width of display
self.verbose = verbose
self._history = []
self._rules = []
self._depths = []
def __call__(self, *args, depth: int | None = None, rule: str = ''):
"""Append next message to history"""
assert depth is not None and rule, "depth and rule must be specified"
msg = " ".join([a for a in args])
if msg not in self._history:
#if True:
self._history.insert(0, msg)
self._depths.insert(0, depth)
self._rules.insert(0, rule)
def pop(self, depth: int):
#"""
_last = 0
while (_last < len(self._depths) - 1 and
depth > self._depths[_last] and
self._depths[_last] >= self._depths[_last+1]):
_last = _last + 1
self._history = self._history[_last:]
self._depths = self._depths[_last:]
self._rules = self._rules[_last:]
#"""
#pass
def __len__(self) -> int:
return int(self.verbose and bool(self._history))
def __str__(self) -> str:
"""Display message history"""
newline = '\n'
if not len(self):
return ''
_str = self.title + newline
for msg, depth, rule in zip(self._history, self._depths, self._rules):
left = ' '*(3+max(self.levels-abs(depth), 0))
right = ' '*max(5, self.width - len(msg) - len(left) - len(rule))
_str += left + msg + right + '~' + rule + newline
return _str
def display(self, end='\n'):
_str = str(self)
if _str:
if _Blog._notebook and _Blog._latex:
display_latex(_str, raw=True)
elif _Blog._notebook:
display_pretty(_self, raw=True)
else:
print(_str, end=end)
@staticmethod
def q(x: int, s: int = 0, t: int = 1, u: int = 0) -> str:
"""Return string representation of mortality u|t_q_x term"""
if t == 0:
return "0"
if t < 0:
return "1"
out = []
if t != 1:
out.append(f"t={t}")
if u > 0:
out.append(f"defer={u}")
return f"q_{x+s}" + "("*bool(out) + ",".join(out) + ")"*bool(out)
@staticmethod
def p(x: int, s: int = 0, t: int = 1) -> str:
"""Return string representation of survival t_p_x term"""
if t == 0:
return "1"
if t < 0:
return "0"
out = []
if t != 1:
out.append(f"t={t}")
return f"p_{x+s}" + "("*bool(out) + ",".join(out) + ")"*bool(out)
@staticmethod
def e(x: int, s: int = 0, t: int = Reserves.WHOLE, curtate: bool = False,
moment: int = 1) -> str:
"""Return string representation of expected future lifetime t_e_[x+s] term"""
out = []
if t >= 0:
out.append(f"t={t}")
if moment != 1:
out.append(f"mom={moment}")
out.append('curtate' if curtate else 'complete')
return f"e_{x+s}" + "("*bool(out) + ",".join(out) + ")"*bool(out)
@staticmethod
def E(x: int, s: int = 0, t: int = 1, endowment: int = 1,
moment: int = 1) -> str:
"""Return string representation of pure endowment t_E_[x+s] term"""
out = []
out.append(f"t={t if t >= 0 else 'WL'}") # term or whole life
if moment != 1:
out.append(f"mom={moment}")
if endowment != 1:
out.append(f"endow={endowment}")
return f"E_{x+s}" + "("*bool(out) + ",".join(out) + ")"*bool(out)
@staticmethod
def IA(x: int, s: int = 0, t: int = Reserves.WHOLE, b: int = 1,
discrete: bool = True) -> str:
"""Return string representation of increasing insurance IA_[x+s]:t term"""
out = []
out.append(f"t={t if t >= 0 else 'WL'}") # term or whole life
if b != 1:
out.append(f"b={b}")
return f"IA_{x+s}" + "("*bool(out) + ",".join(out) + ")"*bool(out)
@staticmethod
def DA(x: int, s: int = 0, t: int = Reserves.WHOLE, b: int = 1,
discrete: bool = True) -> str:
"""Return string representation of decreasing insurance DA_[x+s]:t term"""
out = []
out.append(f"t={t if t >= 0 else 'WL'}") # term or whole life
if b != 1:
out.append(f"b={b}")
return f"DA_{x+s}" + "("*bool(out) + ",".join(out) + ")"*bool(out)
@staticmethod
def A(x: int, s: int = 0, t: int = Reserves.WHOLE, u: int = 0, b: int = 1,
moment: int = 1, endowment: int = 0, discrete: bool = True) -> str:
"""Return string representation of insurance u|_A_[x+s]:t term"""
out = []
out.append(f"t={t if t >= 0 else 'WL'}") # term or whole life
if u != 0:
out.append(f"u={u}")
if b != 1:
out.append(f"b={b}")
if endowment != 0:
out.append(f"endow={endowment}")
if moment != 1:
out.append(f"mom={moment}")
return f"A_{x+s}" + "("*bool(out) + ",".join(out) + ")"*bool(out)
@staticmethod
def a(x: int, s: int = 0, t: int = Reserves.WHOLE, u: int = 0, b: int = 1,
variance: bool = False, discrete: bool = True) -> str:
"""Return string representation of annuity u|_a_[x+s]:t term"""
out = []
out.append(f"t={t if t >= 0 else 'WL'}") # term or whole life
if u != 0:
out.append(f"u={u}")
if b != 1:
out.append(f"b={b}")
if variance:
out.append('var')
return f"a_{x+s}" + "("*bool(out) + ",".join(out) + ")"*bool(out)
@staticmethod
def m(moment: int, **kwargs) -> str:
"""Return string representation of moment exponent"""
out = f"^{moment}"*(moment != 1)
if not kwargs:
return out
args = [k for k,v in kwargs.items() if v]
if len(args) > 1:
return ("(" + "*".join(args) + ")")
else:
return args[0] + out if len(args) else "0"
[docs]class PPrint(_Blog):
"""Helper to display recursion steps as actuarial notation in latex format"""
def __init__(self, label: str, *args, **kwargs):
super().__init__(label, *args, **kwargs)
self.title = f"~\\texttt{{{label}}}{'~'.join(args)}~:=" # identify this stack
def __str__(self) -> str:
if len(self):
beg = "\\begin{array}{llll}\n"
end = "\\end{array}"
lines = [self.title]
for msg, depth, rule in zip(self._history, self._depths, self._rules):
left = '~~' * (1 + max(self.levels-abs(depth), 0))
line = left + msg + '& \\quad \\texttt{' + rule + '}'
lines.append(line) #.replace("=", "& ="))
s = beg + "\\\\\n".join(lines) + end
return s
return ""
[docs] @staticmethod
def q(x: int, s: int = 0, t: int = 1, u: int = 0) -> str:
"""Return latex string representation of mortality u|t_q_[x+s] term"""
if t == 0:
return "~0"
if t < 0:
return "~1"
left = '~'
if t != 1 or u > 0:
left += "_{{"
if u > 0:
left += f"{u}|"
if t != 1:
left += f"{t}"
left += "}}"
right = f"x+{x+s}" if x + s > 0 else "x"
return f"{left}q_{{{right}}}"
[docs] @staticmethod
def p(x: int, s: int = 0, t: int = 1) -> str:
"""Return latex string representation of survival t_p_[x+s] term"""
if t == 0:
return "~1"
if t < 0:
return "~0"
left = '~'
if t != 1:
left += f"_{{{t}}}"
right = f"x+{x+s}" if x + s > 0 else "x"
return f"{left}p_{{{right}}}"
[docs] @staticmethod
def e(x: int, s: int = 0, t: int = Reserves.WHOLE, curtate: bool = False,
moment: int = 1) -> str:
"""Return string representation of expected future lifetime e_[x+s]:t term"""
out = "e" if curtate else "\\overset{{\\circ}}{{e}}"
right = f"x+{x+s}" if x + s > 0 else "x"
right += f":\\overline{{{t}|}}" if t >= 0 else ""
out += "_{{" + right + "}}"
if moment < 0:
out = f"Var[{out}]"
if moment > 1:
out = f"E[{out}^{{{moment}}}]"
return "~" + out
[docs] @staticmethod
def E(x: int, s: int = 0, t: int = 1, endowment: int = 1,
moment: int = 1) -> str:
"""Return latex string representation of pure endowment t_E_[x+s]:t term"""
left = '~'
if t != 1:
left += f"_{{{t}}}"
right = f"x+{x+s}" if x + s > 0 else "x"
return f"{left}E_{{{right}}}"
[docs] @staticmethod
def IA(x: int, s: int = 0, t: int = Reserves.WHOLE, b: int = 1,
discrete: bool = True) -> str:
"""Return latex string representation of increasing insurance IA_[x+s]:t term"""
out = "(IA)"
right = f"x+{x+s}" if x + s > 0 else "x"
right += f":\\overline{{{t}|}}" if t >= 0 else ""
out += "_{{" + right + "}}"
return "~" + out + f"* {b}"*(b != 1)
[docs] @staticmethod
def DA(x: int, s: int = 0, t: int = Reserves.WHOLE, b: int = 1,
discrete: bool = True) -> str:
"""Return latex string representation of decreasing insurance DA_[x+s]:t term"""
out = "(DA)"
right = f"x+{x+s}" if x + s > 0 else "x"
right += f":\\overline{{{t}|}}" if t >= 0 else ""
out += "_{{" + right + "}}"
return "~" + out + f"* {b}"*(b != 1)
[docs] @staticmethod
def A(x: int, s: int = 0, t: int = Reserves.WHOLE, u: int = 0, b: int = 1,
moment: int = 1, endowment: int = 0, discrete: bool = True) -> str:
"""Return latex string representation of insurance u|_A_[x+s]:t term"""
out = "A" if discrete else "\\overline{{A}}"
if moment > 1:
out = f"^{moment}" + out
if u > 0:
out = f"_{{{u}|}}" + out
if endowment == 0 and t >= 0:
out += "^1"
right = f"x+{x+s}" if x + s > 0 else "x"
right += f":\\overline{{{t}|}}" if t >= 0 else ""
out += "_{{" + right + "}}"
return "~" + out + f"* {b}"*(b != 1)
[docs] @staticmethod
def a(x: int, s: int = 0, t: int = Reserves.WHOLE, u: int = 0, b: int = 1,
variance: bool = False, discrete: bool = True) -> str:
"""Return latex string representation of annuity u|_a_[x+s]:t term"""
out = f"\\ddot{{a}}" if discrete else "a"
if u > 0:
out = f"_{{{u}|}}" + out
right = f"x+{x+s}" if x + s > 0 else "x"
right += f":\\overline{{{t}|}}" if t >= 0 else ""
out += "_{{" + right + "}}"
out += f"* {b}"*(b != 1)
if variance:
out = f"Var[{out}]"
return "~" + out
[docs] @staticmethod
def m(moment: int, **kwargs) -> str:
"""Return latex string representation of moment exponent"""
out = f"^{{{moment}}}"*(moment != 1)
if not kwargs:
return out
args = [k for k,v in kwargs.items() if v]
if len(args) > 1:
return ("\\left(" + "*".join(args) + "\\right)")
else:
return args[0] + out if len(args) else "0"
[docs]class Recursion(Reserves):
"""Solve by appling recursive, shortcut and actuarial formulas repeatedly
Args:
depth : maximum depth of recursions (default is 3)
verbose : whether to echo recursion steps (True, default)
Notes:
7 types of function values can be loaded for recursion computations:
- 'q' : (deferred) probability (x) dies in t years
- 'p' : probability (x) survives t years
- 'e' : (temporary) expected future lifetime, or moments
- 'A' : deferred, term, endowment or whole life insurance, or moments
- 'IA' : decreasing life insurance of t years
- 'DA' : increasing life insurance of t years
- 'a' : deferred, temporary or whole life annuity of t years, or moments
"""
_Blog = _Blog
def __init__(self, depth: int = _depth, verbose: bool = True, **kwargs):
super().__init__(**kwargs)
self.db = {}
self._t = {'A': {1, 2}, 'a': {1, 2}, 'e': {1, 2}} # recursion periods to try
self.maxdepth = depth
self._verbose = verbose
self.pprint = Recursion._Blog
[docs] def Blog(self, *args, **kwargs):
"""Returns Blog instance to collect messages and display for this query"""
return Recursion._Blog(*args, **kwargs, verbose=self._verbose)
[docs] @staticmethod
def blog_options(latex: bool = False, notebook: bool = False):
"""Static method to change display options for tracing the recursion steps
Args:
latex: display actuarial notation in latex (True) or raw text (False)
notebook: display to jupyter or colab notebook (True) or terminal (False)
Notes:
latex and notebook options are set to True if notebook is auto-detected
Examples:
>>> Recursion.blog_options(latex=False) # display as raw text
>>> Recursion.blog_options(latex=True, notebook=True) # display latex format
"""
_Blog._notebook = notebook
_Blog._latex = latex
Recursion._Blog = PPrint if latex else _Blog
#
# helpers to store given input values
#
def _db_key(self, *args, **kwargs) -> Tuple:
"""Generate a unique key representing values of given arguments"""
assert args and kwargs
return tuple(list(args) + sorted(kwargs.items()))
def _db_put(self, key: Tuple, value: float | None) -> "Recursion":
"""Store the item's key and value; or remove if value is None
Args:
key : key of the item
value : value to store for item
"""
if value is None and key in self.db:
self.db.pop(key)
else:
self.db[key] = value
label = key[0]
t = [v for k,v in key[1:] if k == 't' and v > 0]
if label in self._t:
self._t[label] = self._t[label].union(t)
return self
def _db_print(self):
"""Display the stored keys and values"""
for k in sorted(self.db.keys()):
print(k, self.db[k])
#
# Formulas for Mortality: u|t_q_x
#
def _get_q(self, x: int, s: int = 0, t: int = 1,
u: int = 0) -> float | None:
"""Get mortality rate from key-value store
Args:
x : age of selection
s : years after selection
u : survive u years, then...
t : death within next t years
"""
key = self._db_key('q', x=x+s, u=u, t=t)
return self.db.get(key, None)
[docs] def set_q(self, val: float, x: int, s: int = 0, t: int = 1,
u: int = 0) -> "Recursion":
"""Set mortality rate u|t_q_[x+s] to given value
Args:
val : value to set
x : age of selection
s : years after selection
u : survive u years, then...
t : death within next t years
Examples:
>>> Recursion(depth=3).set_q(0.02, x=3)
"""
return self._db_put(self._db_key('q', x=x+s, u=u, t=t), val)
def _q_x(self, x: int, s: int = 0, t: int = 1, u: int = 0,
depth: int = 1) -> float:
"""Helper to compute mortality from recursive and alternate formulas"""
found = self._get_q(x, s=s, t=t, u=u)
if found is not None:
return found
if t == 0:
return 0
if t < 0:
return 1
if u > 0:
pu = self._p_x(x, s=s, t=u, depth=depth-1) #depth-1)
qt = self._get_q(x, s=s+u, t=t)
if pu is not None and qt is not None:
self.blog(self.pprint.q(x=x, s=s, t=t, u=u), '=',
self.pprint.p(x=x, s=s, t=u), '*',
self.pprint.q(x=x, s=s+u, t=t),
depth=depth, rule="defer mortality")
return pu * qt # (1) u_p_x * t_q_x+u
else:
self.blog.pop(depth=depth)
qu = self._get_q(x, s=s, t=u)
qt = self._get_q(x, s=s, t=u+t)
if qu is not None and qt is not None:
self.blog(self.pprint.q(x=x, s=s, t=t, u=u), '=',
self.pprint.q(x=x, s=s, t=t+u), '-',
self.pprint.q(x=x, s=s, t=u),
depth=depth, rule="limit mortality")
return qt - qu # (2) u+t_q_x - u_q_x
else:
self.blog.pop(depth=depth)
if depth <= 0:
return None
pu = self._p_x(x, s=s, t=u, depth=depth-1)
pt = self._p_x(x, s=s, t=u+t, depth=depth-1)
if pu is not None and pt is not None:
self.blog(self.pprint.q(x=x, s=s, t=t, u=u), '=',
self.pprint.p(x=x, s=s, t=u), '-',
self.pprint.p(x=x, s=s, t=t+u),
depth=depth, rule="complement survival")
return pu - pt # (3) u_p_x - u+t_p_x
else:
self.blog.pop(depth=depth)
[docs] def q_x(self, x: int, s: int = 0, t: int = 1, u: int = 0) -> float:
self.blog = self.Blog("Mortality",
self.pprint.q(x=x, s=s, t=t, u=u),
levels=self.maxdepth)
"""Compute mortality rate by calling recursion helper"""
q = self._q_x(x, s=s, t=t, u=u, depth=self.maxdepth)
if q is not None:
self.blog.display()
return q
#
# Formulas for Survival: t_p_x
#
def _get_p(self, x: int, s: int = 0, t: int = 1) -> float | None:
"""Get survival probability from key-value store
Args:
x : age of selection
s : years after selection
t : survives next t years
"""
if t == 0:
return 1
if t < 0:
return 0
key = self._db_key('p', x=x+s, t=t)
return self.db.get(key, None)
[docs] def set_p(self, val: float, x: int, s: int = 0, t: int = 1) -> "Recursion":
"""Set survival probability t_p_[x+s] to given value
Args:
val : value to set
x : age of selection
s : years after selection
t : survives next t years
Examples:
>>> Recursion(depth=3).set_p(0.99, x=0)\
"""
return self._db_put(self._db_key('p', x=x+s, t=t), val)
def _p_x(self, x: int, s: int = 0, t: int = 1, depth: int = 1) -> float:
"""Helper to compute survival from recursive and alternate formulas"""
found = self._get_p(x, s=s, t=t)
if found is not None:
return found
found = self._get_q(x, s=s, t=t)
if found is not None:
self.blog(self.pprint.p(x=x, s=s, t=t), '= 1 -',
self.pprint.q(x=x, s=s, t=t),
depth=depth, rule='complement of mortality')
return 1 - found # (1) complement of q_x
else:
self.blog.pop(depth=depth)
if depth <= 0:
return None
# (2a) inverse chain rule: p_x(t) = p_x-1(t+1) / p_x-1
found = self._p_x(x, s=s-1, t=t+1, depth=depth-1)
p = self._p_x(x, s=s-1, t=1, depth=depth-1)
if found is not None and p is not None:
self.blog(self.pprint.p(x=x, s=s, t=t), '=',
self.pprint.p(x=x, s=s-1, t=t+1), '/',
self.pprint.p(x=x, s=s-1, t=1),
depth=depth, rule="survival chain rule")
return found / p
else:
self.blog.pop(depth=depth)
# (2b) inverse chain rule: p_x(t) = p_x(t+1) / p_x+t
found = self._p_x(x, s=s, t=t+1, depth=depth-1)
p = self._p_x(x, s=s+t, t=1, depth=depth-1)
if found is not None and p is not None:
self.blog(self.pprint.p(x=x, s=s, t=t), '=',
self.pprint.p(x=x, s=s, t=t+1), '/',
self.pprint.p(x=x, s=s+t, t=1),
depth=depth, rule="survival chain rule")
return found / p
else:
self.blog.pop(depth=depth)
if t > 1:
# (3a) chain rule: p_x(t) = p_x * p_x+1(t-1)
found = self._p_x(x, s=s+1, t=t-1, depth=depth-1)
p = self._p_x(x, s=s, t=1, depth=depth-1)
if found is not None and p is not None:
self.blog(self.pprint.p(x=x, s=s, t=t), '=',
self.pprint.p(x=x, s=s+1, t=t-1), '*',
self.pprint.p(x=x, s=s, t=1),
depth=depth, rule="survival chain rule")
return found * p
else:
self.blog.pop(depth=depth)
# (3b) chain rule: p_x(t) = p_x+t-1 * p_x(t-1)
found = self._p_x(x, s=s, t=t-1, depth=depth-1)
p = self._p_x(x, s=s+t-1, t=1, depth=depth-1)
if found is not None and p is not None:
self.blog(self.pprint.p(x=x, s=s, t=t), '=',
self.pprint.p(x=x, s=s, t=t-1), '*',
self.pprint.p(x=x, s=s+t-1, t=1),
depth=depth, rule="survival chain rule")
return found * p
else:
self.blog.pop(depth=depth)
if t == 1:
E = self._E_x(x, s=s, t=1, depth=depth-1)
if E is not None:
self.blog(self.pprint.p(x=x, s=s, t=1), '=',
self.pprint.E(x=x, s=s, t=1), "/v",
depth=depth, rule="one-year pure endowment")
return E / self.interest.v
else:
self.blog.pop(depth=depth)
for _t in [self.WHOLE, 2, 3, 4]: # consider only WL, 2-, 3- and 4-term
# (4a) annuity recursion: p_x = [a_x(t) - 1] / [v a_x+1(t-1)
a = self._a_x(x, s=s, t=_t, depth=depth-1)
a1 = self._a_x(x, s=s+1, t=self.add_term(_t, -1), depth=depth-1)
if a is not None and a1 is not None:
self.blog(self.pprint.p(x=x, s=s, t=1), '= [',
self.pprint.a(x=x, s=s, t=_t), '- 1 ] / [ v *',
self.pprint.a(x=x, s=s+1, t=_t-1), ']',
depth=depth, rule="annuity recursion")
return (a - 1) / (self.interest.v * a1)
else:
self.blog.pop(depth=depth)
# (4b) insurance recursion: p_x = [v - A_x(t)] / [v (1 - A_x+1(t-1))]
for endowment in [0, 1]:
A = self._A_x(x, s=s, t=_t, endowment=endowment, depth=depth-1)
A1 = self._A_x(x, s=s+1, t=self.add_term(_t, -1), endowment=endowment,
depth=depth-1)
if A is not None and A1 is not None:
self.blog(self.pprint.p(x=x, s=s, t=1), '= [ v -',
self.pprint.A(x=x, s=s, t=_t, endowment=endowment),
'] / [v * [ 1 -',
self.pprint.A(x=x, s=s+1, t=_t-1, endowment=endowment),
']]',
depth=depth, rule="insurance recursion")
return (self.interest.v - A)/(self.interest.v * (1 - A1))
else:
self.blog.pop(depth=depth)
[docs] def p_x(self, x: int, s: int = 0, t: int = 1) -> float:
"""Compute survival probability by calling recursion helper
Args:
x : age of selection
s : years after selection
t : survives at least t years
"""
self.blog = self.Blog("Survival",
self.pprint.p(x=x, s=s, t=t),
levels=self.maxdepth)
p = self._p_x(x, s=s, t=t, depth=self.maxdepth)
if p is not None:
self.blog.display()
return p
#
# Formulas for Expected Future Lifetime: e_x
#
def _get_e(self, x: int, s: int = 0, t: int = Reserves.WHOLE,
curtate: bool = False, moment: int = 1) -> float | None:
"""Get expected future lifetime from key-value store
Args:
x : age of selection
s : years after selection
t : limit of expected future lifetime
curtate : curtate (True) or complete expectation (False)
moment : first or second moment of expected future lifetime
"""
key = self._db_key('e', x=x+s, t=t, curtate=curtate, moment=moment)
return self.db.get(key, None)
[docs] def set_e(self, val: float, x: int, s: int = 0, t: int = Reserves.WHOLE,
curtate: bool = False, moment: int = 1) -> "Recursion":
"""Set expected future lifetime e_[x+s]:t to given value
Args:
val : value to set
x : age of selection
s : years after selection
t : limit of expected future lifetime
curtate : curtate (True) or complete expectation (False)
moment : first or second moment of expected future lifetime
"""
return self._db_put(self._db_key('e', x=x+s, t=t, moment=moment,
curtate=curtate), val)
def _e_x(self, x: int, s: int = 0, t: int = Reserves.WHOLE,
curtate: bool = False, moment: int = 1,
depth: int = 1) -> float | None:
"""Helper to compute from recursive and alternate formulas"""
found = self._get_e(x, s=s, t=t, curtate=curtate, moment=moment)
if found is not None:
return found
if depth <= 0:
return None
if moment == 1:
if t > 0:
p_t = self._p_x(x, s=s, t=t)
if t == 1 and curtate:
self.blog(self.pprint.e(x=x, s=s, t=1, curtate=curtate), '=',
self.pprint.p(x=x, s=s, t=1),
#f"e_{x+s}:1",
depth=depth, rule='1-year curtate shortcut')
return p_t # (1) if curtate and t=1: e_x:1 = p_x
else:
self.blog.pop(depth=depth)
if t > 1:
e = self._e_x(x, s=s, t=Reserves.WHOLE, curtate=curtate,
moment=1, depth=depth-1)
e_t = self._e_x(x, s=s+t, t=Reserves.WHOLE, curtate=curtate,
moment=1, depth=depth-1)
if e is not None and e_t is not None and p_t is not None:
self.blog(self.pprint.e(x=x, s=s, t=t, curtate=curtate), '=',
self.pprint.e(x=x, s=s, curtate=curtate), '-',
self.pprint.p(x=x, s=s), '*',
self.pprint.e(x=x, s=s+t, curtate=curtate),
#"e_{x+s}:{t}: e_x - p_x e_x+t",
depth=depth, rule="temporary lifetime")
return e - p_t*e_t # (2) temporary = e_x - t_p_x e_x+t
else:
self.blog.pop(depth=depth)
for u in range(1, 50):
e = self._e_x(x, s=s-u, t=self.add_term(t, u), curtate=curtate,
moment=1, depth=depth-1)
e1 = self._e_x(x, s=s-u, t=u, curtate=curtate, moment=1,
depth=depth-1)
p = self._p_x(x, s=s-u, t=u)
if e is not None and e1 is not None and p is not None:
#_t = "" if t < 0 else ":" + str(t)
#msg = f"forward e_{x+s}{_t} = e_{x+s}:1 + p_{x+s} e_{x+s+1}"
self.blog(self.pprint.e(x=x, s=s, t=t, curtate=curtate), '= [',
self.pprint.e(x=x, s=s-1, t=self.add_term(t, 1),
curtate=curtate), '-',
self.pprint.e(x=x, s=s-1, t=1, curtate=curtate), '] /',
self.pprint.p(x=x, s=s-1, t=1),
depth=depth, rule='forward recursion')
return (e - e1) / p # (3) forward: (e_x-1 - e_x-1:1) / p_x-1
else:
self.blog.pop(depth=depth)
e = self._e_x(x, s=s, t=u, curtate=curtate, moment=1, depth=depth-1)
e_t = self._e_x(x, s=s+u, t=self.add_term(t, -u), curtate=curtate,
moment=1, depth=depth-1)
p = self._p_x(x, s=s, t=u)
if e is not None and e_t is not None and p is not None:
self.blog(self.pprint.e(x=x, s=s, t=t, curtate=curtate), '=',
self.pprint.e(x=x ,s=s, t=u, curtate=curtate), '+',
self.pprint.p(x=x, s=s, t=u), '*',
self.pprint.e(x=x, s=s+u, t=self.add_term(t, -u),
curtate=curtate),
#f"backward: e_x:1 + p_x e_x+1:{t}",
depth=depth, rule='backward recursion')
return e + p * e_t # (4) backward: e_x:1 + p_x e_x+1:t-1
else:
self.blog.pop(depth=depth)
[docs] def e_x(self, x: int, s: int = 0, t: int = Reserves.WHOLE,
curtate: bool = False, moment: int = 1) -> float:
"""Compute expected future lifetime by calling recursion helper
Args:
x : age of selection
s : years after selection
t : limited at t years
curtate : whether curtate (True) or complete (False) lifetime
moment : whether to compute first (1) or second (2) moment
"""
self.blog = self.Blog("Lifetime",
self.pprint.e(x=x, s=s, t=t, moment=moment,
curtate=curtate),
levels=self.maxdepth)
e = self._e_x(x, s=s, t=t, curtate=curtate, moment=moment,
depth=self.maxdepth)
if e is not None:
self.blog.display()
return e
#
# Formulas for Pure Endowment: t_E_x
#
def _get_E(self, x: int, s: int = 0, t: int = 1,
endowment: int = 1, moment: int = 1) -> float | None:
"""Get pure endowment from key-value store
Args:
x : age of selection
s : years after selection
t : death within next t years
endowment : endowment value
moment : first or second moment of pure endowment
"""
key = self._db_key('E', x=x+s, t=t, moment=moment)
val = self.db.get(key, None)
if val is not None:
return val * endowment # stored with benefit=1
[docs] def set_E(self, val: float, x: int, s: int = 0, t: int = 1,
endowment: int = 1, moment: int = 1) -> "Recursion":
"""Set pure endowment t_E_[x+s] to given value
Args:
val : value to set
x : age of selection
s : years after selection
t : death within next t years
endowment : endowment value
moment : first or second moment of pure endowment
"""
val /= endowment # store with benefit=1
return self._db_put(self._db_key('E', x=x+s, t=t, moment=moment), val)
def _E_x(self, x: int, s: int = 0, t: int = 1, endowment: int = 1,
moment: int = 1, depth: int = 1) -> float:
"""Helper to compute pure endowment from recursive and alternate formulas"""
E = self._get_E(x, s=s, t=t, endowment=endowment, moment=moment)
if E is not None:
return E
if t < 0: # t infinite => EPV(t) = 0
return 0
if t == 0: # t = 0 => EPV(0) = endowment**moment
return endowment**moment
if moment > 1:
E = self._E_x(x, s=s, endowment=endowment, depth=depth)
if E: # (1) Shortcut: 2E_x = v E_x
self.blog(self.pprint.E(x=x, s=s, t=t, moment=moment),
f"= " + self.pprint.m(moment*t, v="v"), '*',
self.pprint.E(x=x, s=s, t=t),
depth=depth, rule='moments of pure endowment')
return E * self.interest.v**(moment-1)
else:
self.blog.pop(depth=depth)
p = self._p_x(x, s=s, t=t, depth=depth-1) # depth-1)
#if t == 1 and x==0:
# print('p', x, s, t, depth)
if p is not None: # (2) E_x = p_x * v
#msg = f"pure endowment {t}_E_{x+s} = {t}_p_{x+s} * v^{t}"
self.blog(self.pprint.E(x=x, s=s, t=t), '=',
self.pprint.p(x=x, s=s, t=t),
f"*", self.pprint.m(moment*t, v="v"),
depth=depth, rule='pure endowment')
return p * (endowment * self.interest.v_t(t))**moment
else:
self.blog.pop(depth=depth)
if depth <= 0:
return None
At = self._A_x(x, s=s, t=t, moment=moment, b=endowment, endowment=0,
depth=depth-1) #depth-1)
A = self._A_x(x, s=s, t=t, b=endowment, endowment=endowment,
moment=moment, depth=depth-1)
if A is not None and At is not None:
self.blog(self.pprint.E(x=x, s=s, t=t), '=',
self.pprint.A(x=x, s=s, t=t, endowment=endowment), '-',
self.pprint.A(x=x, s=s, t=t, endowment=0),
#f"endowment - term insurance = {t}_E_{x+s}",
depth=depth, rule='endowment insurance minus term')
return A - At # (3) endowment insurance - term (helpful SULT)
else:
self.blog.pop(depth=depth)
E = self._E_x(x, s=s, moment=moment, depth=depth-1)
Et = self._E_x(x, s=s+1, t=t-1, moment=moment, depth=depth-1)
if E is not None and Et is not None:
msg = f"chain Rule: {t}_E_{x+s} = E_{x+s} * {t-1}_E_{x+s+1}"
self.blog(self.pprint.E(x=x, s=s, t=t, moment=moment), '=',
self.pprint.E(x=x, s=s, t=1, moment=moment), '*',
self.pprint.E(x=x, s=s+1, t=t-1, moment=moment),
# '*', self.pprint.m(moment, endow=endowment),
depth=depth, rule='pure endowment chain rule')
return E * Et * endowment**moment # (4) chain rule
else:
self.blog.pop(depth=depth)
[docs] def E_x(self, x: int, s: int = 0, t: int = 1,
endowment: int = 1, moment: int = 1) -> float:
"""Compute pure endowment by calling recursion helper
Args:
x : age of selection
s : years after selection
t : term of pure endowment
endowment : amount of pure endowment
moment : compute first or second moment
"""
self.blog = self.Blog("Pure Endowment",
self.pprint.E(x=x, s=s, t=t, moment=moment,
endowment=endowment),
levels=self.maxdepth)
if moment == self.VARIANCE: # Bernoulli shortcut for variance
found = self._get_E(x, s=s, t=t, endowment=endowment, moment=moment)
if found is not None:
return found
t_p_x = self.p_x(x, s=s, t=t)
return (endowment * self.interest.v_t(t))**2 * t_p_x * (1-t_p_x)
found = self._E_x(x, s=s, t=t, endowment=endowment, moment=moment,
depth=self.maxdepth)
if found is not None:
self.blog.display()
return found
#
# Formulas for Increasing Insurance: IA_x:t
#
def _get_IA(self, x: int, s: int = 0, t: int = Reserves.WHOLE,
b: int = 1, discrete: bool = True) -> float | None:
"""Get increasing insurance from key-value store
Args:
x : age of selection
s : years after selection
t : term of increasing insurance
b : benefit after year 1
discrete : discrete or continuous increasing insurance
"""
key = self._db_key('IA', x=x+s, t=t, discrete=discrete)
val = self.db.get(key, None)
if val is not None:
return val * b # stored with benefit=1
[docs] def set_IA(self, val: float, x: int, s: int = 0, t: int = Reserves.WHOLE,
b: int = 1, discrete: bool = True) -> "Recursion":
"""Set increasing insurance IA_[x+s]:t to given value
Args:
val : value to set
x : age of selection
s : years after selection
t : term of increasing insurance
b : benefit after year 1
discrete : discrete or continuous increasing insurance
"""
val /= b # store with benefit=1
return self._db_put(self._db_key('IA', x=x+s, t=t,
discrete=discrete), val)
def _IA_x(self, x: int, s: int = 0, t: int = Reserves.WHOLE, b: int = 1,
discrete: bool = True, depth: int = 1) -> float | None:
"""Helper to compute from recursive and alternate formulas"""
if t == 0:
return 0
found = self._get_IA(x=x, s=s, t=t, b=b, discrete=discrete)
if found is not None:
return found
if depth <= 0:
return None
if t > 0:
A = self._A_x(x=x, s=s, t=t, b=b, discrete=discrete, depth=depth-1)
n = t + int(discrete)
DA = self._DA_x(x=x, s=s, t=t, b=b, discrete=discrete, depth=depth-1)
if A is not None and DA is not None:
self.blog(self.pprint.IA(x=x, s=s, t=t), f'= {n}',
self.pprint.A(x=x, s=s, t=t), '-',
self.pprint.DA(x=x, s=s, t=t),
#f"identity IA_{x+s}:{t}: ({n})A - DA",
depth=depth, rule='varying insurance identity')
return A * n - DA # (1) Identity with term and decreasing
else:
self.blog.pop(depth=depth)
if discrete:
A = self._A_x(x=x, s=s, t=1, b=b, discrete=discrete, depth=depth-1)
IA = self._IA_x(x=x, s=s+1, t=self.add_term(t, -1), b=b, depth=depth-1)
p = self._p_x(x, s=s, t=1, depth=depth-1) # FIXED t=1
if A is not None and IA is not None and p is not None:
self.blog(self.pprint.IA(x=x, s=s, t=t), '=',
self.pprint.A(x=x, s=s, t=t), '+',
self.pprint.p(x=x, s=s), f"*", self.pprint.m(t, v="v"), "*",
self.pprint.IA(x=x, s=s+1, t=t-1),
#f"backward IA_{x+s}:{t}: A + IA_{x+s+1}:{t-1}",
depth=depth, rule='backward recursion')
return A + p * self.interest.v * IA # (2) backward recursion
else:
self.blog.pop(depth=depth)
[docs] def increasing_insurance(self, x: int, s: int = 0, t: int = Reserves.WHOLE,
b: int = 1, discrete: bool = True) -> float:
"""Compute increasing insurance with recursive helper
Args:
x : age of selection
s : years after selection
t : term of insurance
b : amount of benefit in first year
discrete : benefit paid year-end (True) or moment of death (False)
"""
self.blog = self.Blog("Increasing Insurance",
self.pprint.IA(x=x, s=s, t=t, b=b, discrete=discrete),
levels=self.maxdepth)
IA = self._IA_x(x, s=s, b=b, t=t, discrete=discrete, depth=self.maxdepth)
if IA is not None:
self.blog.display()
return IA
IA = super().increasing_insurance(x, s=s, b=b, t=t, discrete=discrete)
if IA is not None:
self.blog.display()
return IA
#
# Formulas for Decreasing insurance: DA_x:t
#
def _get_DA(self, x: int, s: int = 0, t: int = Reserves.WHOLE,
b: int = 1, discrete: bool = True) -> float | None:
"""Get decreasing insurance from key-value store
Args:
x : age of selection
s : years after selection
t : term of decreasing insurance
b : benefit after year 1
discrete : discrete or continuous decreasing insurance
"""
key = self._db_key('DA', x=x+s, t=t, discrete=discrete)
val = self.db.get(key, None)
if val is not None:
return val * b # stored with benefit=1
[docs] def set_DA(self, val: float, x: int, s: int = 0, t: int = Reserves.WHOLE,
b: int = 1, discrete: bool = True) -> "Recursion":
"""Set decreasing insurance DA_[x+s]:t to given value
Args:
val : value to set
x : age of selection
s : years after selection
t : term of decreasing insurance
b : benefit after year 1
discrete : discrete or continuous decreasing insurance
"""
val /= b # store with benefit=1
return self._db_put(self._db_key('DA', x=x+s, t=t,
discrete=discrete), val)
def _DA_x(self, x: int, s: int = 0, t: int = Reserves.WHOLE, b: int = 1,
discrete: bool = True, depth: int = 1) -> float | None:
"""Helper to compute from recursive and alternate formulas"""
found = self._get_DA(x=x, s=s, t=t, discrete=discrete)
if found is not None:
return found
if t == 0:
return 0
if depth <= 0:
return None
assert t < 0, "Decreasing insurance must be term insurance"
A = self._A_x(x=x, s=s, t=t, b=b, discrete=discrete, depth=depth-1)
n = t + int(discrete)
IA = self._DA_x(x=x, s=s, t=t, b=b, discrete=discrete, depth=depth-1)
if A is not None and IA is not None:
self.blog(self.pprint.DA(x=x, s=s, t=t), f'= {n}',
self.pprint.A(x=x, s=s, t=t), '-',
self.pprint.IA(x=x, s=s, t=t),
depth=depth, rule='varying insurance identity')
return A * n - IA # (1) identity with term and increasing
else:
self.blog.pop(depth=depth)
if discrete:
DA = self._IA_x(x=x, s=s+1, t=self.add_term(t, -1), b=b, depth=depth-1)
p = self._p_x(x, s=s, depth=depth-1)
if DA is not None and p is not None:
#f"backward DA_{x+s}:{t}: v(t q_{x+s} + p_{x+s} DA_{x+s+1}:{t-1})"
self.blog(self.pprint.DA(x=x, s=s, t=t), f"=",
self.pprint.m(t, v="v"), "* t *",
self.pprint.q(x=x, s=s), '+',
self.pprint.p(x=x, s=s), '*', self.pprint.DA(x=x, s=s+1, t=t-1),
depth=depth, rule='backward recursion')
return self.interest.v * ((1-p)*t + p*DA) # (2) backward recursion
else:
self.blog.pop(depth=depth)
[docs] def decreasing_insurance(self, x: int, s: int = 0, t: int = Reserves.WHOLE,
b: int = 1, discrete: bool = True) -> float:
"""Compute decreasing insurance by attempting recursive helper first
Args:
x : age of selection
s : years after selection
t : term of insurance
b : amount of benefit in first year
discrete : benefit paid year-end (True) or moment of death (False)
"""
self.blog = self.Blog("Increasing Insurance",
self.pprint.DA(x=x, s=s, t=t, b=b, discrete=discrete),
levels=self.maxdepth)
if t == 0:
return 0
A = self._DA_x(x=x, s=s, t=t, b=b, discrete=discrete, depth=self.maxdepth)
if A is not None:
self.blog.display()
return A
A = super().decreasing_insurance(x, s=s, b=b, t=t, discrete=discrete)
if A is not None:
self.blog.display()
return A
#
# Formulas for Insurance: A_x:t
#
def _get_A(self, x: int, s: int = 0, u: int = 0, t: int = Reserves.WHOLE,
b: int = 1, moment: int = 1, endowment: int = 0,
discrete: bool = True) -> float | None:
"""Get insurance from key-value store
Args:
x : age of selection
s : years after selection
u : defer u years
t : term of insurance
endowment : endowment amount
discrete : discrete (True) or continuous (False) insurance
moment : first or second moment of insurance
"""
if endowment < 0: # endowment insurance with equal benefits
endowment = b
if t == 0: # terminal value of insurance
return endowment
if b == 0: # normalize insurance factor by benefit amount
scale = endowment
endowment = 1
else:
scale = b
endowment = 1 if b == endowment else endowment / b
key = self._db_key('A', x=x+s, u=u, t=t, moment=moment,
endowment=endowment, discrete=discrete)
val = self.db.get(key, None)
if val is not None:
return val * scale # stored with benefit=1
[docs] def set_A(self, val: float, x: int, s: int = 0, t: int = Reserves.WHOLE,
u: int = 0, b: int = 1, moment: int = 1, endowment: int = 0,
discrete: bool = True) -> "Recursion":
"""Set insurance u|_A_[x+s]:t to given value
Args:
val : value to set
x : age of selection
s : years after selection
u : defer u years
t : term of insurance
endowment : endowment amount
discrete : discrete (True) or continuous (False) insurance
moment : first or second moment of insurance
Examples
>>> Recursion().set_interest(i=0.06).set_A(A, x=0)
"""
if endowment < 0: # endowment insurance with equal death and endow
endowment = b
if endowment == 0: # normalize insurance factor by benefit amount
val /= b
elif b == 0:
val /= endowment # store with benefit=1
else:
if b != 1:
val /= b
endowment /= b
return self._db_put(self._db_key('A', x=x+s, t=t, u=u,
moment=moment, endowment=endowment,
discrete=discrete), val)
def _A_x(self, x: int, s: int = 0, t: int = Reserves.WHOLE, u: int = 0,
b: int = 1, discrete: bool = True, endowment: int = 0,
moment: int = 1, depth: int = 1) -> float | None:
"""Helper to compute from recursive and alternate formulas"""
if endowment == b and t == 1 and discrete: # 1-year endow ins
# self.blog(self.pprint.A(x=x, s=s, t=1, endowment=b, b=b, moment=moment),
# '=', self.pprint.m(moment, v="v"),
# depth=depth, rule='one-year endowment insurance')
return (self.interest.v_t(1) * endowment)**moment
found = self._get_A(x=x, s=s, t=t, b=b, u=u, discrete=discrete,
moment=moment, endowment=endowment)
if found is not None:
return found
if depth <= 0:
return None
if discrete and u > 0: # (1) deferred insurance
A = self._A_x(x=x, s=s+1, t=t, b=b, u=u-1, discrete=discrete,
moment=moment, endowment=endowment, depth=depth-1)
E = self._E_x(x, s=s, t=1, moment=moment, depth=depth-1)
if A is not None and p is not None: # (1a) backward E_x * A
#msg = f"backward deferred {u}_A_{x+s}: {u}_E * A_{x+s+u}"
self.blog(self.pprint.A(x=x, s=s, t=t, u=u, b=b, moment=moment), '=',
self.pprint.E(x=x, s=s, moment=moment), '*',
self.pprint.A(x=x, s=s+1, t=t, u=u-1, moment=moment),
depth=depth, rule='backward recursion')
return E * A
else:
self.blog.pop(depth=depth)
A = self._A_x(x, s=s-1, t=t, b=b, u=u+1, discrete=discrete,
moment=moment, endowment=endowment, depth=depth-1)
E = self._E_x(x, s=s-1, t=1, moment=moment, depth=depth-1)
if A is not None and E is not None: # (1b) forward recursion
msg = f"forward deferred {u}_A_{x+s}: {u+1}A_{x+s-1} / E"
self.blog(self.pprint.A(x=x, s=s, t=t, u=u, b=b, moment=moment), '=',
self.pprint.A(x=x, s=s-1, t=t, u=u+1, moment=moment), '/',
self.pprint.E(x=x, s=s-1, moment=moment),
depth=depth, rule='forward recursion')
return A / E
else:
self.blog.pop(depth=depth)
return None
if endowment > 0: # (2a) endowment = term + E_x * endowment
A = self._A_x(x=x, s=s, t=t, b=b, discrete=discrete,
moment=moment, depth=depth-1)
E_x = self._E_x(x=x, s=s, t=t, moment=moment,
endowment=endowment, depth=depth-1)
if A is not None and E_x is not None:
# f"term + pure insurance = A_{x+s}:{t}",
self.blog(self.pprint.A(x=x, s=s, t=t, endowment=endowment,
moment=moment),
'=', self.pprint.A(x=x, s=s, t=t, moment=moment), '+',
self.pprint.E(x=x, s=s, t=t, endowment=endowment,
moment=moment),
depth=depth, rule='term plus pure endowment')
return A + E_x
else:
self.blog.pop(depth=depth)
elif t >= 0: # (2b) term = endowment insurance - E_x * endowment
A = self._A_x(x=x, s=s, t=t, b=b, discrete=discrete,
moment=moment, endowment=b, depth=depth-1)
E_x = self._E_x(x=x, s=s, t=t, moment=moment, endowment=b,
depth=depth-1)
if A is not None and E_x is not None:
#msg = f"endowment insurance - pure endowment = A_{x+s}^1:{t}"
self.blog(self.pprint.A(x=x, s=s, t=t, moment=moment), '=',
self.pprint.A(x=x, s=s, t=t, moment=moment, endowment=b), '-',
self.pprint.E(x=x, s=s, t=t, endowment=b, moment=moment),
depth=depth, rule='endowment insurance - pure')
return A - E_x
else:
self.blog.pop(depth=depth)
if not discrete: # recursions only for discrete insurance
return None
if t == 1: # special cases for discrete one-year insurance
if endowment == b: # (3a) discrete one-year endowment insurance
# self.blog(self.pprint.A(x=x, s=s, t=1, endowment=endowment,
# moment=moment), f"= v"+self.pprint.m(moment),
# "*", self.pprint.m(moment, endow=endowment),
# depth=depth, rule='one-year endowment insurance')
return (self.interest.v * endowment)**moment
p = self._p_x(x, s=s, t=1)
#print(p, x, s, t, b, endowment)
if p is not None: # (3b) one-year discrete insurance
self.blog(self.pprint.A(x=x, s=s, t=t, moment=moment,
endowment=endowment), "=",
self.pprint.m(moment, v="v"), "*",
self.pprint.q(x=x, s=s), f"*",
self.pprint.m(moment, v="v"), "+",
self.pprint.p(x=x, s=s), f"*".
self.pprint.m(moment, endow=endowment),
#f"discrete 1-year insurance: A_{x+s}:1 = qv",
depth=depth, rule='one-year discrete insurance')
return (self.interest.v**moment
* ((1 - p) * b**moment + p * endowment**moment))
else:
self.blog.pop(depth=depth)
# insurance recursions
# TODO: mo re general recursions u in [1, ..., 50]
# """
A = self._A_x(x=x, s=s+1, t=self.add_term(t, -1), b=b,
discrete=discrete, moment=moment,
endowment=endowment, depth=depth-1)
p = self._p_x(x, s=s, t=1, depth=depth-1) # (4) backward recursion
if A is not None and p is not None:
self.blog(self.pprint.A(x=x, s=s, t=t, b=b, moment=moment),
f"= v"+self.pprint.m(moment), "* [",
self.pprint.q(x=x,s=s), f"*",
self.pprint.m(moment, b="b"), "+",
self.pprint.p(x=x, s=s), '*',
self.pprint.A(x=x, s=s+1, t=t-1, b=b, moment=moment), ']',
#f"backward: A_{x+s} = qv + pvA_{x+s+1}",
depth=depth, rule='backward recursion')
return self.interest.v_t(1)**moment * ((1 - p)*b**moment + p*A)
else:
self.blog.pop(depth=depth)
"""
for y in [1]: #self._t['A']:
At = self._A_x(x=x, s=s, t=y, discrete=discrete, b=b,
moment=moment, depth=depth-1)
A = self._A_x(x=x, s=s+y, t=self.add_term(t, -y), b=b, discrete=discrete,
moment=moment, endowment=endowment, depth=depth-1)
E = self._E_x(x, s=s, t=y, moment=moment, depth=depth-1)
#print('*', At, A, E, x, s, t, y, b, endowment)
if A is not None and At is not None and E is not None: # (4a) backward recursion
self.blog(self.pprint.A(x=x, s=s, t=t, b=b, moment=moment), "=",
self.pprint.A(x=x, s=s, t=y, moment=moment, b=b), '+',
self.pprint.E(x=x, s=s, t=y, moment=moment), '*',
self.pprint.A(x=x, s=s+y, t=self.add_term(t, -y), b=b,
moment=moment, endowment=endowment),
#f"backward: A_{x+s} = qv + pvA_{x+s+1}",
depth=depth, rule='backward recursion')
return At + E * A
else:
self.blog.pop(depth=depth)
At = self._A_x(x=x, s=s-y, t=self.add_term(t, y), b=b, discrete=discrete,
moment=moment, endowment=endowment, depth=depth-1)
A = self._A_x(x=x, s=s-y, t=y, b=b, discrete=discrete, moment=moment,
depth=depth-1)
E = self._E_x(x, s=s-y, t=y, moment=moment, depth=depth-1)
#print('#', At, A, E, x, s, t, y, b, endowment)
if A is not None and At is not None and E is not None: # (5) forward recursion
self.blog(self.pprint.A(x=x, s=s, t=t, b=b,
moment=moment, endowment=endowment), '= [',
self.pprint.A(x=x, s=s-y, t=self.add_term(t, y),
b=b, endowment=endowment, moment=moment), '-',
self.pprint.A(x=x, s=s-y, t=y, b=b, moment=moment), '] /',
self.pprint.E(x=x, s=s-y, t=y, moment=moment),
#f"forward: A_{x+s} = (A_{x+s-1}/v - q) / p",
depth=depth, rule='forward recursion')
return (At - A) / E
else:
self.blog.pop(depth=depth)
"""
A = self._A_x(x=x, s=s-1, t=self.add_term(t, 1), b=b,
discrete=discrete, moment=moment,
endowment=endowment, depth=depth-1)
p = self._p_x(x, s=s-1, t=1, depth=depth-1)
if A is not None and p is not None: # (5) forward recursion
self.blog(self.pprint.A(x=x, s=s, t=t, b=b, moment=moment), '= [',
self.pprint.A(x=x, s=s-1, t=t+1, b=b, moment=moment),
f"/", self.pprint.m(moment, v="v"), "-",
self.pprint.q(x=x, s=s-1), f"*",
self.pprint.m(moment, b="b"), "] /", self.pprint.p(x=x, s=s-1),
#f"forward: A_{x+s} = (A_{x+s-1}/v - q) / p",
depth=depth, rule='forward recursion')
return (A/self.interest.v_t(1)**moment - (1-p)*b**moment) / p
else:
self.blog.pop(depth=depth)
# """
[docs] def whole_life_insurance(self, x: int, s: int = 0, b: int = 1,
discrete: bool = True, moment: int = 1) -> float:
"""Compute whole life insurance A_x by attempting recursion and twin first
Args:
x : age of selection
s : years after selection
b : amount of benefit
moment : compute first or second moment
discrete : benefit paid year-end (True) or moment of death (False)
"""
self.blog = self.Blog("Whole Life Insurance",
self.pprint.A(x=x, s=s, t=Reserves.WHOLE, b=b, u=0,
endowment=0, moment=moment,
discrete=discrete),
levels=self.maxdepth)
found = self._A_x(x, s=s, b=b, moment=moment, discrete=discrete,
depth=self.maxdepth)
if found is not None:
self.blog.display()
return found
if moment == 1 and self.interest.i > 0: # (1) twin annuity
a = self._a_x(x, s=s, b=b, discrete=discrete, depth=self.maxdepth)
if a is not None:
self.blog(self.pprint.a(x=x, s=s), '= [ 1 -',
self.pprint.A(x=x, s=s), f"] / d",
#"Annuity twin: a = (1 - A) / d",
depth=self.maxdepth, rule='annuity twin')
self.blog.display()
return self.insurance_twin(a=a, discrete=discrete)
A = super().whole_life_insurance(x, s=s, b=b, discrete=discrete,
moment=moment)
if A is not None:
self.blog.display()
return A
[docs] def term_insurance(self, x: int, s: int = 0, t: int = 1, b: int = 1,
moment: int = 1, discrete: bool = True) -> float:
"""Compute term life insurance A_x:t^1 by attempting recursion first
Args:
x : age of selection
s : years after selection
t : term of insurance
b : amount of benefit
moment : compute first or second moment
discrete : benefit paid year-end (True) or moment of death (False)
"""
self.blog = self.Blog("Term Insurance",
self.pprint.A(x=x, s=s, t=t, b=b, u=0, discrete=discrete,
endowment=0, moment=moment),
levels=self.maxdepth)
found = self._A_x(x, s=s, b=b, t=t, moment=moment, discrete=discrete,
depth=self.maxdepth)
if found is not None:
self.blog.display()
return found
A = super().term_insurance(x, s=s, b=b, t=t, discrete=discrete,
moment=moment)
if A is not None:
self.blog.display()
return A
[docs] def deferred_insurance(self, x: int, s: int = 0, b: int = 1, u: int = 0,
t: int = Reserves.WHOLE, moment: int = 1,
discrete: bool = True) -> float:
"""Compute deferred life insurance u|A_x:t^1 by attempting recursion first"""
self.blog = self.Blog("Deferred Insurance",
self.pprint.A(x=x, s=s, t=t, b=b, u=u,
discrete=discrete,
endowment=0, moment=moment),
levels=self.maxdepth)
A = self._get_A(x=x, s=s, t=t, b=b, u=u, discrete=discrete,
moment=moment)
if A is not None:
self.blog.display()
return A
A = super().deferred_insurance(x, s=s, b=b, t=t, u=u,
discrete=discrete, moment=moment)
if A is not None:
self.blog.display()
return A
[docs] def endowment_insurance(self, x: int, s: int = 0, t: int = 1, b: int = 1,
endowment: int = -1, moment: int = 1,
discrete: bool = True) -> float:
"""Compute endowment insurance u|A_x:t by attempting recursion first"""
self.blog = self.Blog("Endowment Insurance",
self.pprint.A(x=x, s=s, t=t, b=b, u=0,
discrete=discrete,
endowment=endowment, moment=moment),
levels=self.maxdepth)
assert t >= 0
if endowment < 0:
endowment = b
found = self._A_x(x, s=s, b=b, t=t, moment=moment, discrete=discrete,
endowment=endowment, depth=self.maxdepth)
if found is not None:
self.blog.display()
return found
if moment == 1 and endowment == b and self.interest.i > 0:
a = self._a_x(x, s=s, b=b, t=t, discrete=discrete,
depth=self.maxdepth)
if a is not None: # twin insurance
self.blog.display()
return self.insurance_twin(a=a, discrete=discrete)
A = super().endowment_insurance(x, s=s, b=b, t=t, discrete=discrete,
moment=moment, endowment=endowment)
if A is not None:
self.blog.display()
return A
#
# Formulas for Annuties: a_x:t
#
def _get_a(self, x: int, s: int = 0, u: int = 0, t: int = Reserves.WHOLE,
b: int = 1, variance: bool = False,
discrete: bool = True) -> float | None:
"""Get annuity from key-value store
Args:
x : age of selection
s : years after selection
u : defer u years
t : term of annuity
b : benefit amount
discrete : whether annuity due (True) or continuous (False)
variance : whether first moment (False) or variance (True)
"""
key = self._db_key('a', x=x+s, u=u, t=t,
variance=variance, discrete=discrete)
val = self.db.get(key, None)
if val is not None:
return val * b # stored with benefit=1
[docs] def set_a(self, val: float, x: int, s: int = 0, t: int = Reserves.WHOLE,
u: int = 0, b: int = 1, variance: bool = False,
discrete: bool = True) -> "Recursion":
"""Set annuity u|_a_[x+s]:t to given value
Args:
val : value to set
x : age of selection
s : years after selection
u : defer u years
t : term of annuity
b : benefit amount
discrete : whether annuity due (True) or continuous (False)
variance : whether first moment (False) or variance (True)
Examples:
>>> Recursion().set_interest(i=0.06).set_a(7, x=1)
"""
val /= b # store with benefit=1
return self._db_put(self._db_key('a', x=x+s, t=t, u=u,
variance=variance, discrete=discrete), val)
def _a_x(self, x: int, s: int = 0, t: int = Reserves.WHOLE,
u: int = 0, b: int = 1, discrete: bool = True,
variance: bool = False, depth: int = 1) -> float | None:
"""Helper to compute from recursive and alternate formulas"""
if t == 1 and not u and discrete:
# self.blog(self.pprint.a(x=x, s=s, t=1, discrete=discrete), '= 1',
# depth=depth, rule='one-year discrete annuity')
return b
if t == 0:
return 0
found = self._get_a(x=x, s=s, t=t, b=b, u=u, discrete=discrete,
variance=variance)
if found is not None:
return found
if depth <= 0:
return None
assert variance is False, "Annuity recursion requires variance=False"
if u > 0: # (1) deferred annuity
found = self._a_x(x=x, s=s+1, t=t, b=b, u=u-1, discrete=discrete,
variance=variance, depth=depth-1)
E = self._E_x(x, s=s, t=1, depth=depth-1)
if found is not None and E is not None:
#msg = f"backward {u}_a_{x+s} = {u}_E * a_{x+s+u}"
self.blog(self.pprint.a(x=x, s=s, u=u, t=t), '=',
self.pprint.a(x=x, s=s+1, t=t, u=u-1), '/',
self.pprint.E(x=x, s=s),
depth=depth, rule='backward deferred annuity')
return E * found # (1a) backward recusion
else:
self.blog.pop(depth=depth)
found = self._a_x(x=x, s=s-1, t=t, b=b, u=u+1, discrete=discrete,
depth=depth-1) # FIXED u=u+1
E = self._E_x(x, s=s-1, t=1, depth=depth-1)
if found is not None and E is not None: # (1b) forward
#msg = f"forward: {u}_a_{x+s} = {u+1}_a_{x+s-1}/E_{x+s-1}"
self.blog(self.pprint.a(x=x, s=s, u=u, t=t), '=',
self.pprint.a(x=x, s=s-1, t=t, u=u+1), '/',
self.pprint.E(x=x, s=s-1),
depth=depth, rule='forward deferred annuity')
return found / E
else:
self.blog.pop(depth=depth)
return None
# TODOS: more general recursions u in [1,...,50]?
if discrete: # recursions only for discrete annuities
found = self._a_x(x=x, s=s+1, t=self.add_term(t, -1), b=b, u=u,
discrete=discrete,
variance=variance, depth=depth-1)
E = self._E_x(x, s=s, t=1, depth=depth-1)
if found is not None and E is not None: # (2a) backward
#msg = (f"backward: a_{x+s}{'' if t < 0 else (':'+str(t))} = 1 + "
# + f"E_{x+s} a_{x+s+1}{'' if t < 0 else (':'+str(t-1))}")
#_t = "" if t < 0 else f":{t-1}"
self.blog(self.pprint.a(x=x, s=s, t=t), '= 1 +',
self.pprint.E(x=x, s=s, t=1), '*',
self.pprint.a(x=x, s=s+1, t=t-1),
depth=depth, rule='backward recursion')
return b + E * found
else:
self.blog.pop(depth=depth)
found = self._a_x(x=x, s=s-1, t=self.add_term(t, 1), b=b, u=u,
discrete=discrete, depth=depth-1)
E = self._E_x(x, s=s-1, t=1, depth=depth-1)
if found is not None and E is not None: # (2b) forward
_t = "" if t < 0 else f":{t-1}"
self.blog(self.pprint.a(x=x, s=s, t=t), '= [',
self.pprint.a(x=x, s=s-1, t=self.add_term(t, 1)), '- 1 ] /',
self.pprint.E(x=x, s=s-1, t=1),
#f"forward: a_{x+s}{_t} = (a_{x+s-1} - 1)/E",
depth=depth, rule='forward recursion')
return (found - b) / E
else:
self.blog.pop(depth=depth)
[docs] def whole_life_annuity(self, x: int, s: int = 0, b: int = 1,
variance: bool = False,
discrete: bool = True) -> float:
"""Compute whole life annuity a_x by attempting recursion then twin first
Args:
x : age of selection
s : years after selection
b : annuity benefit amount
variance (bool): return EPV (True) or variance (False)
discrete : annuity due (True) or continuous (False)
"""
self.blog = self.Blog("Whole Life Annuity",
self.pprint.a(x=x, s=s, t=Reserves.WHOLE, b=b, u=0,
discrete=discrete, variance=False),
levels=self.maxdepth)
found = self._a_x(x, s=s, b=b, variance=variance,
discrete=discrete, depth=self.maxdepth)
if found is not None:
self.blog.display()
return found
if not variance and self.interest.i > 0: # (1) twin insurance shortcut
A = self._A_x(x, s=s, b=b, discrete=discrete, depth=self.maxdepth)
if A is not None:
self.blog(self.pprint.a(x=x, s=s, discrete=discrete, variance=variance),
"= [1 -", self.pprint.A(x=x, s=s, discrete=discrete), "] / d",
depth=self.maxdepth, rule='insurance twin')
self.blog.display()
return self.annuity_twin(A=A, discrete=discrete)
a = super().whole_life_annuity(x, s=s, b=b, discrete=discrete,
variance=variance)
if a is not None:
self.blog.display()
return a
[docs] def temporary_annuity(self, x: int, s: int = 0, t: int = Reserves.WHOLE,
b: int = 1, variance: bool = False,
discrete: bool = True) -> float:
"""Compute temporary annuity a_x:t by attempting recursion then twin first
Args:
x : age of selection
s : years after selection
t : term of annuity in years
b : annuity benefit amount
variance (bool): return EPV (True) or variance (False)
discrete : annuity due (True) or continuous (False)
"""
self.blog = self.Blog("Temporary Annuity",
self.pprint.a(x=x, s=s, t=t, b=b, u=0,
discrete=discrete, variance=variance),
levels=self.maxdepth)
found = self._a_x(x, s=s, b=b, t=t, variance=variance,
discrete=discrete, depth=self.maxdepth)
if found is not None:
self.blog.display()
return found
if not variance and self.interest.i > 0: # (1) twin insurance shortcut
A = self._A_x(x, s=s, b=b, t=t, endowment=b, discrete=discrete,
depth=self.maxdepth)
if A is not None:
self.blog(self.pprint.a(x=x, s=s, t=t, b=b), '= [ 1 -',
self.pprint.A(x=x, s=s, t=t, b=b, endowment=b), f"] / d",
#"Annuity twin: a = (1 - A) / d",
depth=self.maxdepth, rule='annuity twin')
self.blog.display()
return self.annuity_twin(A=A, discrete=discrete)
a = super().temporary_annuity(x, s=s, b=b, t=t, discrete=discrete,
variance=variance)
if a is not None:
self.blog.display()
return a
[docs] def deferred_annuity(self, x: int, s: int = 0, t: int = Reserves.WHOLE,
u: int = 0, b: int = 1, discrete: bool = True) -> float:
"""Compute deferred annuity u|a_x:t by attempting recursion first
Args:
x : age of selection
s : years after selection
u : years deferred
t : term of annuity in years
b : annuity benefit amount
discrete : annuity due (True) or continuous (False)
"""
self.blog = self.Blog("Deferred Annuity",
self.pprint.a(x=x, s=s, t=t, b=b, u=u,
discrete=discrete, variance=False),
levels=self.maxdepth)
a = self._a_x(x, s=s, b=b, t=t, u=u,
discrete=discrete, depth=self.maxdepth)
if a is not None:
self.blog.display()
return a
a = self._a_x(x, s=s, b=b, t=self.add_term(u, t),
discrete=discrete, depth=self.maxdepth)
a_t = self._a_x(x, s=s, b=b, t=u,
discrete=discrete, depth=self.maxdepth)
if a is not None and a_t is not None:
self.blog.display()
return a - a_t
return super().deferred_annuity(x, s=s, b=b, t=t, discrete=discrete)
# to auto-detect notebook environment
try:
_shell = str(type(get_ipython())).lower()
if "colab" in _shell or "zmq" in _shell:
Recursion._Blog = PPrint
_Blog._notebook = True
_Blog._latex = True
else:
Recursion._Blog = _Blog
_Blog._notebook = False
_Blog._latex = False
except:
pass
if __name__ == "__main__":
from actuarialmath.constantforce import ConstantForce
from actuarialmath.policyvalues import Contract
print("SOA Question 6.10: (D) 0.91")
x = 0
life = Recursion().set_interest(i=0.06)\
.set_p(0.975, x=x)\
.set_a(152.85/56.05, x=x, t=3)\
.set_A(152.85, x=x, t=3, b=1000)
print(life.p_x(x=x+2))
#isclose(0.91, p, question="Q6.10")
raise Exception
print("AMLCR2 Exercise 2.6")
x = 0
life = Recursion(depth=3).set_interest(i=0.06)\
.set_p(0.99, x=x)\
.set_p(0.985, x=x+1)\
.set_p(0.95, x=x+1, t=3)\
.set_q(0.02, x=x+3)
print(life.p_x(x=x+3)) # 0.98
print(life.p_x(x=x, t=2)) # 0.97515
print(life.p_x(x=x+1, t=2)) # 0.96939
print(life.p_x(x=x, t=3)) # 0.95969
print(life.q_x(x=x, t=2, u=1)) # 0.03031
print("SOA Question 6.48: (A) 3195")
life = Recursion().set_interest(i=0.06)
x = 0
life.set_p(0.95, x=x, t=5)
life.set_q(0.02, x=x+5)
life.set_q(0.03, x=x+6)
life.set_q(0.04, x=x+7)
a = 1 + life.E_x(x, t=5)
A = life.deferred_insurance(x, u=5, t=3)
P = life.gross_premium(A=A, a=a, benefit=100000)
print(P)
print()
print("SOA Question 6.40: (C) 116 ")
# - standard formula discounts/accumulates by too much (i should be smaller)
x = 0
life = Recursion().set_interest(i=0.06).set_a(7, x=x+1).set_q(0.05, x=x)
a = life.whole_life_annuity(x)
A = 110 * a / 1000
print(a, A)
life = Recursion().set_interest(i=0.06).set_A(A, x=x).set_q(0.05, x=x)
A1 = life.whole_life_insurance(x+1)
P = life.gross_premium(A=A1 / 1.03, a=7) * 1000
print(P)
print()
print("SOA Question 6.17: (A) -30000")
x = 0
life = ConstantForce(mu=0.1).set_interest(i=0.08)
A = life.endowment_insurance(x, t=2, b=100000, endowment=30000)
a = life.temporary_annuity(x, t=2)
P = life.gross_premium(a=a, A=A)
print(A, a, P)
life1 = Recursion().set_interest(i=0.08)\
.set_q(life.q_x(x, t=1) * 1.5, x=x, t=1)\
.set_q(life.q_x(x+1, t=1) * 1.5, x=x+1, t=1)
contract = Contract(premium=P * 2, benefit=100000, endowment=30000)
L = life1.gross_policy_value(x, t=0, n=2, contract=contract)
print(L)
print()
life = Recursion(verbose=False).set_interest(i=0.05).set_E(0.95, 0,t=1)
E = life.E_x(0, t=1, moment=2)
print(E)