fastcashflow._measurement.recognition의 소스 코드
"""CSM recognition -- IFRS 17 paragraph-109 maturity-band disclosure.
The closing CSM allocated to maturity bands by expected coverage-unit
recognition. Shared by the GMM (paragraph 44) and VFA (paragraph 45) settlement
schedules -- the para-109 allocation is identical, only each model's public
``recognition_schedule`` (its own source settle) differs. CSM-domain code (PAA
has no CSM), kept as a distinct submodule of the shared measurement layer.
"""
from __future__ import annotations
from dataclasses import dataclass
import numpy as np
from fastcashflow._typing import FloatArray
[문서]
@dataclass(frozen=True, slots=True, eq=False)
class CSMRecognitionSchedule:
"""IFRS 17 paragraph-109 disclosure: the closing CSM allocated to maturity
bands by expected coverage-unit recognition.
``band_edges_months`` are the band boundaries in months from the valuation
date (default 12 / 36 / 60, the four-band disclosure axis); the bands are
``[0, e0), [e0, e1), ..., [e_last, end)``. ``csm[b]`` is the closing CSM
expected to be recognised in band ``b`` -- allocated by each contract's
forward coverage-unit fraction, so the bands SUM TO ``closing_csm``. It is
an allocation of the remaining balance, not the accreted nominal release;
the coverage-unit proxy is the in-force count, undiscounted, matching the
B119 amortisation kernel, so the schedule tracks the actual release pattern.
"""
band_edges_months: tuple
csm: FloatArray # (n_bands,) -- sums to closing_csm
closing_csm: float
@property
def labels(self) -> tuple:
"""Band labels, in years when the edges are whole years."""
def fmt(m):
return f"{m // 12}y" if m % 12 == 0 else f"{m}m"
edges = self.band_edges_months
out = [f"<= {fmt(edges[0])}"]
out += [f"{fmt(lo)} - {fmt(hi)}" for lo, hi in zip(edges[:-1], edges[1:])]
out.append(f"{fmt(edges[-1])} +")
return tuple(out)
def _validate_band_edges(band_edges_months) -> tuple:
"""Coerce / validate paragraph-109 band edges -- strictly ascending
positive integer months from the valuation date. Shared by the GMM and VFA
recognition schedules so the edge contract cannot drift between them."""
edges = tuple(int(e) for e in band_edges_months)
if (not edges or any(e <= 0 for e in edges)
or list(edges) != sorted(set(edges))):
raise ValueError(
"band_edges_months must be strictly ascending positive integers "
f"(months from the valuation date), got {band_edges_months!r}")
return edges
def _build_schedule(csm_closing, inforce, em, boundary, edges):
"""Allocate the per-MP closing CSM to maturity bands by each contract's
forward coverage-unit (in-force) fraction; the bands sum to the closing
CSM. Onerous contracts (CSM <= 0) contribute nothing. Shared by the GMM
(paragraph 44) and VFA (paragraph 45) settlement schedules -- the
paragraph-109 allocation is identical, only the source settle differs."""
bounds = (0,) + edges
n_bands = len(bounds)
band = np.zeros(n_bands)
for i in range(csm_closing.shape[0]):
csm_i = float(csm_closing[i])
if csm_i <= 0.0: # onerous / no CSM -> nothing to recognise
continue
cu = inforce[i, em[i]:boundary[i]] # forward coverage units (in-force)
total = cu.sum()
if total <= 0.0: # guarded by settle (em < boundary); belt-and-braces
continue
for b in range(n_bands):
lo = bounds[b]
hi = bounds[b + 1] if b + 1 < n_bands else cu.shape[0]
band[b] += csm_i * cu[lo:hi].sum() / total
return CSMRecognitionSchedule(
band_edges_months=edges, csm=band,
closing_csm=float(csm_closing[csm_closing > 0.0].sum()))