fastcashflow._paa의 소스 코드

"""IFRS 17 Premium Allocation Approach (PAA) -- the simplified measurement.

The PAA is the simplified model the standard permits for short-coverage
contracts -- IFRS 17 paragraphs 53-59 (eligibility, Sec. 53; the liability
for remaining coverage, Sec. 55; insurance revenue, Sec. B126). Instead of
the GMM's BEL / RA / CSM, the Liability for Remaining Coverage (LRC) is
measured like an unearned premium: premiums build it up, insurance revenue
draws it down as coverage is provided. There is no CSM -- profit emerges
as revenue is earned.

Scope and simplifications, each with the standard's basis:

* Acquisition cash flows are expensed as incurred -- the Sec. 59(a) option,
  available when the coverage period is one year or less -- so they are not
  held in the LRC.
* The LRC is held undiscounted: Sec. 56 does not require a financing
  adjustment when the time between providing service and the related
  premium due date is one year or less.
* Insurance revenue is allocated by ``revenue_basis``: Sec. B126(a)
  (passage of time -- premium earned straight-line over the coverage
  period, the default) or Sec. B126(b) (the expected timing of incurred
  claims and expenses).
* The onerous test (Sec. 57-58) is applied at inception. The loss is
  ``max(0, fulfilment cash flows for remaining coverage - LRC)``, which at
  inception equals ``max(0, the GMM fulfilment cash flows)``. It is
  reported separately rather than folded into the LRC carrying amount.
* The Liability for Incurred Claims (Sec. 59(b)) runs off a claims
  settlement pattern; with no pattern set, claims settle when incurred and
  it is zero. It is held undiscounted -- Sec. 59(b) permits this when
  claims are paid within a year of being incurred.
"""
from __future__ import annotations

from dataclasses import dataclass

import numpy as np

from fastcashflow._typing import FloatArray, IntArray
from fastcashflow.basis import Basis, _single_basis
from fastcashflow.io import write_measurement, _write_measurement_columns
from fastcashflow.curves import discount_monthly_curve
from fastcashflow.numerics import _norm_ppf, _rollforward_kernel, _settlement_lic
from fastcashflow.modelpoints import ModelPoints
from fastcashflow.projection import Cashflows, project_cashflows


[문서] @dataclass(frozen=True, slots=True, eq=False) class PAAMeasurement: """PAA measurement -- the Liability for Remaining Coverage and the underwriting result released from it. ``lrc`` is an ``(n_mp, n_time+1)`` trajectory; column 0 is the inception LRC. ``revenue`` and ``service_expense`` are ``(n_mp, n_time)`` -- the insurance revenue earned and the insurance service expense incurred each month. ``service_result`` (a property) is their difference. ``lic`` is the ``(n_mp, n_time+1)`` liability for incurred claims -- claims build it up as they are incurred and run it off as they are paid. """ # headline -- always present, shape (n_mp,) lrc: FloatArray # inception Liability for Remaining Coverage loss_component: FloatArray # onerous-contract loss at inception # inception fulfilment cash flows for remaining coverage (BEL + RA, signed: # negative for a profitable contract). The onerous-test input -- # loss_component = max(0, fcf) -- kept so grouping can net it on the group # aggregate. The PAA liability itself is the LRC, not this. fcf: FloatArray | None = None # trajectory -- full only (None on the headline-only path) lrc_path: FloatArray | None = None # (n_mp, n_time+1) -- LRC trajectory revenue: FloatArray | None = None # (n_mp, n_time) -- insurance revenue earned service_expense: FloatArray | None = None # (n_mp, n_time) -- claims + expenses incurred lic: FloatArray | None = None # (n_mp, n_time+1) -- liability for incurred claims cashflows: "Cashflows | None" = None model_points: "ModelPoints | None" = None # stamped by measure_paa, for group axes group_labels: "np.ndarray | None" = None # per-group label on a grouped result group_sizes: IntArray | None = None # model points per group, aligned with labels @property def service_result(self) -> FloatArray: """Insurance service result -- revenue less service expense.""" return self.revenue - self.service_expense def _columns(self): return [("LRC", self.lrc), ("loss", self.loss_component)] def __repr__(self) -> str: from fastcashflow._display import measurement_repr return measurement_repr("PAAMeasurement", self._columns()) def __str__(self) -> str: from fastcashflow._display import measurement_str return measurement_str("PAAMeasurement", self._columns())
@write_measurement.register def _(measurement: PAAMeasurement, path, *, ids=None): _write_measurement_columns( {"lrc": measurement.lrc, "loss_component": measurement.loss_component}, path, ids) def measure_paa( model_points: ModelPoints, basis: Basis, *, revenue_basis: str = "time", ) -> PAAMeasurement: """Measure a portfolio under the Premium Allocation Approach. The LRC rolls forward as ``LRC[t+1] = LRC[t] + premium[t] - revenue[t]`` from ``LRC[0] = 0`` -- premiums received build it up, insurance revenue releases it. A single-premium contract gives the textbook pro-rata unearned premium reserve. ``revenue_basis`` selects the Sec. B126 allocation of insurance revenue, which always sums to the total premium: * ``"time"`` -- B126(a), passage of time: the premium earned straight-line over the coverage period (the default). * ``"claims"`` -- B126(b), the expected timing of incurred claims and expenses; for when the release of risk differs significantly from the passage of time. A policy with no service expense has no such pattern and falls back to ``"time"``. The onerous test reuses the GMM fulfilment cash flows: a contract whose inception fulfilment cash flows are a net outflow carries that outflow as a loss component. """ basis = _single_basis(basis, entry="measure_paa") proj = project_cashflows(model_points, basis) premium_total = proj.premium_cf.sum(axis=1) # (n_mp,) service_expense = proj.claim_cf + proj.morbidity_cf + proj.expense_cf # Liability for incurred claims -- claims incurred build it up, claims # paid (spread over the settlement pattern) run it off. Held # undiscounted, consistent with the LRC. incurred = proj.claim_cf + proj.morbidity_cf if basis.settlement_pattern is None: lic = np.zeros((incurred.shape[0], incurred.shape[1] + 1)) else: lic = _settlement_lic(incurred, basis.settlement_pattern) # Insurance revenue -- total premium allocated across the periods of # service (Sec. B126), so total revenue equals total premium. if revenue_basis == "time": # B126(a): premium earned straight-line over the coverage period. in_coverage = np.arange(proj.n_time)[None, :] < model_points.term_months[:, None] weight = in_coverage.astype(np.float64) elif revenue_basis == "claims": weight = service_expense.copy() # B126(b) empty = weight.sum(axis=1) == 0.0 # no pattern -> B126(a) weight[empty] = proj.inforce[empty] else: raise ValueError( f"revenue_basis must be 'time' or 'claims', got {revenue_basis!r}" ) w_sum = weight.sum(axis=1, keepdims=True) w_sum = np.where(w_sum == 0.0, 1.0, w_sum) # safe divide; weight=0 → revenue=0 revenue = premium_total[:, None] * weight / w_sum # LRC roll-forward -- premiums build it up, revenue releases it. net = proj.premium_cf - revenue n_mp, n_time = net.shape lrc = np.zeros((n_mp, n_time + 1)) lrc[:, 1:] = np.cumsum(net, axis=1) # Onerous test -- the GMM inception fulfilment cash flows. bel, pv_claims, pv_morbidity, pv_disability, pv_survival = _rollforward_kernel( proj.claim_cf, proj.morbidity_cf, proj.disability_cf, proj.expense_cf, proj.premium_cf, proj.annuity_cf, proj.maturity_cf, proj.surrender_cf, model_points.contract_boundary_months, discount_monthly_curve(basis, proj.n_time), ) z = _norm_ppf(basis.ra_confidence) ra0 = z * (basis.mortality_cv * pv_claims[:, 0] + basis.morbidity_cv * pv_morbidity[:, 0] + basis.disability_cv * pv_disability[:, 0] + basis.longevity_cv * pv_survival[:, 0]) fcf = bel[:, 0] + ra0 loss_component = np.maximum(0.0, fcf) return PAAMeasurement( lrc=lrc[:, 0], loss_component=loss_component, fcf=fcf, lrc_path=lrc, revenue=revenue, service_expense=service_expense, lic=lic, cashflows=proj, model_points=model_points, )