Source code for actuneo.life.reserves

"""
Reserve Calculations

Provides calculations for policy reserves, prospective and retrospective reserves,
and related actuarial valuations.
"""

import numpy as np
from typing import Optional, Union, List, Dict
from ..mortality import MortalityTable, SurvivalFunctions


[docs] class Reserves: """ A class for calculating policy reserves and actuarial valuations. """
[docs] def __init__(self, mortality_table: MortalityTable, interest_rate: float = 0.05, expense_rate: float = 0.0, profit_margin: float = 0.0): """ Initialize Reserves calculator. Args: mortality_table: MortalityTable instance interest_rate: Annual interest rate for discounting expense_rate: Annual expense rate as percentage of premium profit_margin: Required profit margin """ self.mt = mortality_table self.sf = SurvivalFunctions(mortality_table, interest_rate) self.i = interest_rate self.v = 1 / (1 + interest_rate) self.expense_rate = expense_rate self.profit_margin = profit_margin
[docs] def prospective_reserve_whole_life(self, x: int, duration: int, annual_premium: float, sum_assured: float = 1000.0) -> float: """ Calculate prospective reserve for whole life assurance. Args: x: Original age at entry duration: Number of years in force annual_premium: Annual premium amount sum_assured: Sum assured amount Returns: Prospective reserve """ current_age = x + duration # Reserve = (A_{x+t} * sum_assured - P * ä_{x+t}) / ä_{x+t} # Where A is whole life assurance, ä is life annuity, P is annual premium remaining_assurance = self.sf.assurance(current_age) * sum_assured remaining_annuity = self.sf.annuity_immediate(current_age) if remaining_annuity == 0: return remaining_assurance - annual_premium reserve = (remaining_assurance - annual_premium * remaining_annuity) / remaining_annuity return max(0, reserve) # Reserve cannot be negative
[docs] def prospective_reserve_term(self, x: int, n: int, duration: int, annual_premium: float, sum_assured: float = 1000.0) -> float: """ Calculate prospective reserve for term assurance. Args: x: Original age at entry n: Original term duration: Number of years in force annual_premium: Annual premium amount sum_assured: Sum assured amount Returns: Prospective reserve """ current_age = x + duration remaining_term = n - duration if remaining_term <= 0: return 0.0 # Policy expired remaining_assurance = self.sf.assurance(current_age, remaining_term) * sum_assured remaining_annuity = self.sf.annuity_immediate(current_age, remaining_term) if remaining_annuity == 0: return remaining_assurance - annual_premium reserve = (remaining_assurance - annual_premium * remaining_annuity) / remaining_annuity return max(0, reserve)
[docs] def prospective_reserve_endowment(self, x: int, n: int, duration: int, annual_premium: float, sum_assured: float = 1000.0) -> float: """ Calculate prospective reserve for endowment assurance. Args: x: Original age at entry n: Original term duration: Number of years in force annual_premium: Annual premium amount sum_assured: Sum assured amount Returns: Prospective reserve """ current_age = x + duration remaining_term = n - duration if remaining_term <= 0: return 0.0 # Policy matured # Endowment reserve = [A_{x+t}:n¨ * SA + v^{n-t} * SA - P * ä_{x+t}:n¨] / ä_{x+t}:n¨ # Calculate remaining endowment assurance term_assurance = self.sf.assurance(current_age, remaining_term) * sum_assured pure_endowment = self.sf.npx(current_age, remaining_term) * self.v ** remaining_term * sum_assured remaining_assurance = term_assurance + pure_endowment remaining_annuity = self.sf.annuity_immediate(current_age, remaining_term) if remaining_annuity == 0: return remaining_assurance - annual_premium reserve = (remaining_assurance - annual_premium * remaining_annuity) / remaining_annuity return max(0, reserve)
[docs] def retrospective_reserve_whole_life(self, x: int, duration: int, annual_premium: float, sum_assured: float = 1000.0) -> float: """ Calculate retrospective reserve for whole life assurance. Args: x: Original age at entry duration: Number of years in force annual_premium: Annual premium amount sum_assured: Sum assured amount Returns: Retrospective reserve """ # Retrospective reserve = Accumulated premiums + interest - claims paid accumulated_premiums = 0.0 interest_factor = 1.0 for t in range(duration): age_at_premium = x + t survival_prob = self.sf.npx(x, t) # Probability of surviving to pay premium accumulated_premiums += annual_premium * survival_prob * interest_factor interest_factor *= (1 + self.i) # Claims paid (simplified - only death claims, ignoring surrenders, etc.) claims_paid = 0.0 interest_factor = 1.0 for t in range(1, duration + 1): # Probability of dying in year t q_t = 1 - self.sf.npx(x, t) + self.sf.npx(x, t-1) claims_paid += q_t * sum_assured * interest_factor interest_factor *= (1 + self.i) reserve = accumulated_premiums - claims_paid return max(0, reserve)
[docs] def net_level_premium_reserve(self, x: int, duration: int, net_premium: float, sum_assured: float = 1000.0) -> float: """ Calculate net level premium reserve. Args: x: Original age at entry duration: Number of years in force net_premium: Net level annual premium sum_assured: Sum assured amount Returns: Net level premium reserve """ # Reserve = v * [A_{x+t} * SA - P * ä_{x+t}] current_age = x + duration remaining_assurance = self.sf.assurance(current_age) * sum_assured remaining_annuity = self.sf.annuity_immediate(current_age) reserve = remaining_assurance - net_premium * remaining_annuity return max(0, reserve)
[docs] def gross_reserve(self, net_reserve: float, duration: int, expense_reserve: Optional[float] = None) -> float: """ Calculate gross reserve including expenses and contingencies. Args: net_reserve: Net mathematical reserve duration: Duration in years expense_reserve: Additional expense reserve Returns: Gross reserve """ if expense_reserve is None: # Estimate expense reserve as percentage of net reserve expense_reserve = net_reserve * self.expense_rate # Add profit margin profit_reserve = net_reserve * self.profit_margin gross_reserve = net_reserve + expense_reserve + profit_reserve return gross_reserve
[docs] def reserve_release(self, initial_reserve: float, final_reserve: float, duration: int) -> float: """ Calculate reserve release over a period. Args: initial_reserve: Reserve at start of period final_reserve: Reserve at end of period duration: Duration in years Returns: Reserve release amount """ return initial_reserve - final_reserve
[docs] def terminal_reserve(self, x: int, n: int, sum_assured: float = 1000.0) -> float: """ Calculate terminal reserve (reserve at maturity). Args: x: Original age at entry n: Term of policy sum_assured: Sum assured amount Returns: Terminal reserve (should be sum assured for endowment) """ # For endowment assurance, terminal reserve equals sum assured return sum_assured
[docs] def reserve_distribution(self, reserves: List[float], total_portfolio_value: float) -> Dict[str, float]: """ Analyze reserve distribution across portfolio. Args: reserves: List of individual policy reserves total_portfolio_value: Total portfolio value Returns: Dictionary with distribution statistics """ reserves_array = np.array(reserves) distribution = { 'mean_reserve': np.mean(reserves_array), 'median_reserve': np.median(reserves_array), 'min_reserve': np.min(reserves_array), 'max_reserve': np.max(reserves_array), 'total_reserves': np.sum(reserves_array), 'reserve_to_portfolio_ratio': np.sum(reserves_array) / total_portfolio_value if total_portfolio_value > 0 else 0, 'percentiles': { '25th': np.percentile(reserves_array, 25), '75th': np.percentile(reserves_array, 75), '90th': np.percentile(reserves_array, 90), '95th': np.percentile(reserves_array, 95) } } return distribution
[docs] def zillmerized_reserve(self, net_reserve: float, initial_expenses: float, duration: int, amortization_period: int = 10) -> float: """ Calculate Zillmerized reserve (reserve net of unamortized acquisition costs). Args: net_reserve: Net mathematical reserve initial_expenses: Initial acquisition expenses duration: Duration in years amortization_period: Period over which to amortize expenses Returns: Zillmerized reserve """ if duration >= amortization_period: # Fully amortized return net_reserve # Amortize initial expenses over the period amortized_expenses = initial_expenses * (amortization_period - duration) / amortization_period zillmer_reserve = net_reserve - amortized_expenses return max(0, zillmer_reserve)
[docs] def contingency_reserve(self, base_reserve: float, risk_factor: float = 0.05) -> float: """ Calculate contingency reserve for adverse deviations. Args: base_reserve: Base mathematical reserve risk_factor: Risk factor for contingency Returns: Contingency reserve """ return base_reserve * risk_factor