Source code for annuallife.TradLife_A_EX1.Projection.InnerProj

# modelx: pseudo-python
# This file is part of a modelx model.
# It can be imported as a Python module, but functions defined herein
# are model formulas and may not be executable as standard Python.

"""Inner projection that re-runs the cashflows under a single life stress.

This Space is the engine behind the Solvency II life-risk results in
:mod:`~annuallife.TradLife_A_EX1.Projection`. Each ItemSpace
``InnerProj[t0, risk, shock]`` re-runs the per-policy projection from the
valuation time ``t0`` under one prescribed life stress, so that the
stressed present value of net cashflows can be compared with the
unstressed (baseline) one by
:func:`~annuallife.TradLife_A_EX1.Projection.risk_life_sub`.

It inherits all cashflow and present-value Cells from
:mod:`TradLife_A.BaseProj <annuallife.TradLife_A.BaseProj>` and
:mod:`TradLife_A.PV <annuallife.TradLife_A.PV>`, and overrides only the Cells needed to
anchor the projection at ``t0`` and to apply the stress. The unstressed
mortality, lapse and renewal-commission rates are taken from the outer
projection and scaled by the shock; the acquisition and maintenance
expenses are recomputed locally so they pick up the stressed
:func:`inflation_factor`.

Parameters and References
-------------------------

Attributes:
    t0(:obj:`int`): Valuation time the inner projection is anchored at;
        at ``t0`` the in-force equals that of the outer projection.
    risk(:obj:`int`, optional): A ``LifeRiskID`` code selecting the life
        sub-risk to stress. Defaults to ``BASE`` (0), the unstressed run.
    shock(:obj:`int`, optional): A ``LapseShockID`` code selecting the
        lapse shock (``UP``, ``DOWN`` or ``MASS``) when ``risk`` is
        ``LAPSE``. Defaults to 0.

Cells Summary
-------------

Overridden Cells
^^^^^^^^^^^^^^^^

The following Cells override their
:mod:`TradLife_A.BaseProj <annuallife.TradLife_A.BaseProj>` counterparts to anchor the
projection at ``t0`` and to apply the life shocks.

.. autosummary::

   ~pols_if
   ~pols_if_beg1
   ~pols_lapse_mass
   ~claims_surr
   ~claims_surr_mass_pp
   ~mort_rate
   ~lapse_rate
   ~expense_acq_pp
   ~expense_maint_pp
   ~commissions_ren_pp
   ~inflation_factor

"""

from modelx.serialize.jsonvalues import *

_formula = lambda t0, risk=0, shock=0: None

_bases = [
    "..BaseProj",
    "..PV"
]

_allow_none = None

_spaces = []

# ---------------------------------------------------------------------------
# Cells

[docs] def pols_if(t): """Number of policies: End of period. The inner projection is anchored at the valuation time ``t0`` (where this returns the outer projection's in-force) and is defined only for ``t >= t0``. Present values are taken at ``t0`` (e.g. :func:`TradLife_A.PV.pv_net_cf <annuallife.TradLife_A.PV.pv_net_cf>` at ``t0``), so the ``t < t0`` region is never reached in normal use; evaluating any in-force cell there would recurse without terminating. """ if t == t0: return _space._parent._parent.pols_if(t) #_parent._parent # pol.policy_count else: return pols_if_beg1(t-1) - pols_death(t-1) - pols_lapse(t-1)
[docs] def pols_lapse_mass(t): """Number of policies that instantly surrender under the mass-lapse shock. Solvency II models mass lapse as an instantaneous discontinuance at the valuation time ``t0`` rather than an elevated surrender rate spread over the year. At ``t == t0`` a segment-dependent fraction (the ``LAPSE`` / ``MASS`` factor from the ``LifeShocks`` input, selected by :func:`~annuallife.TradLife_A_EX1.PolicyAttrs.segment`) of the policies in force at the beginning of the period instantly surrenders. The amount is removed from :func:`pols_if_beg1` so those policies neither pay premiums nor are exposed to mortality or ongoing lapse during the period, and is added to the surrender benefit in :func:`claims_surr`. It is zero for every other risk / shock and for ``t != t0``. Mirrors ``Override.LapseMass.PolsSurrMass`` in the ``solvency2`` library. """ if t == t0 and risk == LifeRiskID.LAPSE and shock == LapseShockID.MASS: factor = asmp.life_shock_param(risk, shock, pol.segment()[idx]) else: factor = 0 return (pols_if_beg(t) + pols_renewal(t) + pols_if_init(t)) * factor
[docs] def pols_if_beg1(t): """Number of policies: Beginning of period 1. Overrides the base cell to remove the instantaneous mass-lapse surrenders (:func:`pols_lapse_mass`) from the in-force at the start of the period, so the mass lapsers drop out instantly under the Solvency II mass-lapse shock. Identical to the base cell for every other risk / shock, where :func:`pols_lapse_mass` is zero. """ return pols_if_beg(t) + pols_renewal(t) + pols_if_init(t) - pols_lapse_mass(t)
[docs] def claims_surr_mass_pp(t): """Surrender benefit per policy for the instantaneous mass-lapse surrenders. The ongoing surrenders are assumed to occur throughout the period, so :func:`TradLife_A.BaseProj.claims_surr_pp <annuallife.TradLife_A.BaseProj.claims_surr_pp>` pays the mid-period average of the cash value at ``t`` and ``t+1``. The mass-lapse surrenders, by contrast, occur instantaneously at the valuation time, so the benefit is the cash value at time ``t`` only (:func:`TradLife_A.BaseProj.cash_value_rate <annuallife.TradLife_A.BaseProj.cash_value_rate>` at ``t``). """ return sum_assured(t) * cash_value_rate(t)
[docs] def claims_surr(t): """Surrender benefits. Overrides the base cell so the surrender benefit is paid on both the ongoing surrenders (:func:`pols_lapse`) and the instantaneous mass-lapse surrenders (:func:`pols_lapse_mass`). The ongoing surrenders receive the mid-period average cash value (:func:`TradLife_A.BaseProj.claims_surr_pp <annuallife.TradLife_A.BaseProj.claims_surr_pp>`), while the mass-lapse surrenders receive the cash value at time ``t`` (:func:`claims_surr_mass_pp`). """ return (claims_surr_pp(t) * pols_lapse(t) + claims_surr_mass_pp(t) * pols_lapse_mass(t))
[docs] def lapse_rate(t): """Surrender Rate The unstressed rate is taken from the outer projection (:func:`TradLife_A.BaseProj.lapse_rate <annuallife.TradLife_A.BaseProj.lapse_rate>`); the lapse-up / lapse-down shocks scale it (capped by the ``LIMIT`` factor), while the mass-lapse shock leaves the ongoing rate unchanged (it is modelled by :func:`pols_lapse_mass` instead). """ base = _space._parent._parent.lapse_rate(t) if not risk == LifeRiskID.LAPSE: return base elif shock == LapseShockID.UP: shock_factor = 1 + asmp.life_shock_param(risk, shock) shock_limit = asmp.life_shock_param(risk, shock, extra_key=ExtraKeyID.LIMIT) return min(shock_factor * base, shock_limit) elif shock == LapseShockID.DOWN: shock_factor = 1 - asmp.life_shock_param(risk, shock) shock_limit = asmp.life_shock_param(risk, shock, extra_key=ExtraKeyID.LIMIT) return max(shock_factor * base, base - shock_limit) elif shock == LapseShockID.MASS: # The mass-lapse shock is an instantaneous discontinuance at t0 # modelled by pols_lapse_mass / pols_if_beg1, not an elevated # surrender rate; ongoing surrenders stay at the base rate. return base else: raise ValueError(f'invalid lapse shock id: {shock}')
[docs] def mort_rate(x): """Mortality rate at age ``x`` with the mortality / longevity shock applied. The unstressed rate is taken from the outer projection (:func:`TradLife_A.BaseProj.mort_rate <annuallife.TradLife_A.BaseProj.mort_rate>`). Under the ``MORT`` risk it is increased and under the ``LONGV`` risk it is decreased, by the factor read from the ``LifeShocks`` input via :func:`~annuallife.TradLife_A_EX1.Assumptions.life_shock_param`. For any other risk the unstressed rate is returned unchanged. """ base = _space._parent._parent.mort_rate(x) if risk == LifeRiskID.MORT: return base * (1 + asmp.life_shock_param(risk)) elif risk == LifeRiskID.LONGV: return base * (1 - asmp.life_shock_param(risk)) else: return base
[docs] def expense_acq_pp(t): """Acquisition expense per policy with the expense shock applied. Recomputes the acquisition expense locally (mirroring :func:`TradLife_A.BaseProj.expense_acq_pp <annuallife.TradLife_A.BaseProj.expense_acq_pp>`) so it uses this Space's stressed :func:`inflation_factor`, then under the ``EXPS`` risk increases it by the factor read from the ``LifeShocks`` input via :func:`~annuallife.TradLife_A_EX1.Assumptions.life_shock_param`. For any other risk the unstressed amount is returned. """ if t == 0: base = (ann_prem_pp(t) * asmp.exps_acq_ann_prem()[idx] + (sum_assured(t) * asmp.exps_acq_sa()[idx] + asmp.exps_acq_pol()[idx]) * inflation_factor(t) / inflation_factor(0)) else: base = 0 if risk == LifeRiskID.EXPS: return base * (1 + asmp.life_shock_param(risk)) else: return base
[docs] def expense_maint_pp(t): """Maintenance expense per policy with the expense shock applied. Recomputes the maintenance expense locally (mirroring :func:`TradLife_A.BaseProj.expense_maint_pp <annuallife.TradLife_A.BaseProj.expense_maint_pp>`) so it uses this Space's stressed :func:`inflation_factor`, then under the ``EXPS`` risk increases it by the factor read from the ``LifeShocks`` input via :func:`~annuallife.TradLife_A_EX1.Assumptions.life_shock_param`. For any other risk the unstressed amount is returned. """ base = (ann_prem_pp(t) * asmp.exps_maint_ann_prem()[idx] + (sum_assured(t) * asmp.exps_maint_sa()[idx] + asmp.exps_maint_pol()[idx]) * inflation_factor(t)) if risk == LifeRiskID.EXPS: return base * (1 + asmp.life_shock_param(risk)) else: return base
[docs] def commissions_ren_pp(t): """Renewal commission per policy with the expense shock applied. The unstressed renewal commission is taken from the outer projection (:func:`TradLife_A.BaseProj.commissions_ren_pp <annuallife.TradLife_A.BaseProj.commissions_ren_pp>`); under the ``EXPS`` risk it is increased by the factor read from the ``LifeShocks`` input via :func:`~annuallife.TradLife_A_EX1.Assumptions.life_shock_param`. For any other risk the unstressed amount is returned unchanged. """ base = _space._parent._parent.commissions_ren_pp(t) if risk == LifeRiskID.EXPS: return base * (1 + asmp.life_shock_param(risk)) else: return base
[docs] def inflation_factor(t): """Expense inflation factor with the expense-inflation shock applied. Overrides :func:`TradLife_A.BaseProj.inflation_factor <annuallife.TradLife_A.BaseProj.inflation_factor>` to compound at a stressed inflation rate under the ``EXPS`` risk: the base rate plus the ``INFL`` factor read from the ``LifeShocks`` input via :func:`~annuallife.TradLife_A_EX1.Assumptions.life_shock_param` (a +1 percentage-point stress). The locally recomputed expenses (:func:`expense_maint_pp`, :func:`expense_acq_pp`) therefore inflate faster. For any other risk it equals the unstressed factor. """ if t == 0: return 1 rate = asmp.inflation_rate() if risk == LifeRiskID.EXPS: rate += asmp.life_shock_param(risk, extra_key=ExtraKeyID.INFL) return inflation_factor(t - 1) * (1 + rate)
# --------------------------------------------------------------------------- # References LifeRiskID = ("Interface", ("...", "Enums", "LifeRiskID"), "auto") LapseShockID = ("Interface", ("...", "Enums", "LapseShockID"), "auto") LapseScopeID = ("Interface", ("...", "Enums", "LapseScopeID"), "auto") ExtraKeyID = ("Interface", ("...", "Enums", "ExtraKeyID"), "auto")