fastcashflow.pricing.profit의 소스 코드

"""Profit testing -- value and emergence of new business.

A thin pricing layer over the GMM measurement: the present-value metrics
(``csm_plus_ra``, ``profit_margin``) and the period-by-period profit signature,
plus the rate metrics (IRR, break-even) on a shareholder cash-flow stream. It
assembles already-computed pieces -- the inception BEL/RA/CSM, ``report`` (the
IFRS 17 P&L emergence) and the discount curve -- rather than re-projecting
anything.

Conventions (v1, pre-tax, pre-required-capital):
* ``csm_plus_ra`` = CSM + RA - loss component = the present value, at issue, of
  the profit a contract is expected to release. It equals -BEL. IFRS 17 has no
  single defined term for CSM + RA (the two release differently -- RA on risk
  run-off, CSM on service), so the function names what it returns. This is NOT
  the value of new business (``pricing.vnb``), which is net of the cost of
  capital.
* Profit signature = the per-period insurance service result -- on a
  best-estimate run that is the CSM release plus the RA release recognised each
  period; its present value at the locked-in rate is ``csm_plus_ra``.
* IRR / break-even act on a shareholder cash-flow stream the caller supplies
  (the profit signature net of the day-0 new-business strain); they only carry
  meaning once that strain makes the stream change sign (the statutory profit
  test, where the reserve strain is explicit).
"""
from __future__ import annotations

from dataclasses import dataclass

import numpy as np

from fastcashflow._typing import FloatArray, IntArray
from fastcashflow.reporting.report import report as _report


[문서] @dataclass(frozen=True, slots=True, eq=False) class ProfitSignature: """Period-by-period shareholder profit emergence of a book. ``profit`` is ``(n_periods,)`` -- the profit recognised in each reporting period of ``period_months`` months (the portfolio total). ``month_end`` is the elapsed month at the end of each period. ``present_value`` discounts the stream; ``total`` is its undiscounted sum. """ period_months: int month_end: IntArray # (n_periods,) elapsed month at each period end profit: FloatArray # (n_periods,) profit recognised per period @property def total(self) -> float: """Undiscounted lifetime profit.""" return float(np.sum(self.profit))
[문서] def present_value(self, annual_rate: float) -> float: """Present value of the profit stream at a flat annual ``annual_rate``, each period discounted to issue from its mid-point (the standard mid-year convention -- the period's profit emerges over the period). This approximately reconciles to :func:`csm_plus_ra`; the exact figure is ``csm_plus_ra`` (CSM + RA), not this aggregated-and-re-discounted stream.""" mid = (np.asarray(self.month_end, np.float64) - 0.5 * self.period_months) / 12.0 return float(np.sum(self.profit / (1.0 + annual_rate) ** mid))
[문서] def csm_plus_ra(measurement) -> FloatArray: """CSM + RA per model point -- the present value at issue of the profit the contract is expected to release: ``CSM + RA - loss component`` (equivalently ``-BEL``). Pre-tax and pre-required-capital. NOT the value of new business (:func:`~fastcashflow.pricing.vnb`), which nets the cost of capital.""" return measurement.csm + measurement.ra - measurement.loss_component
def _pv_premium(measurement) -> FloatArray: """Present value of the premium stream per model point, on the locked-in discount curve, beginning-of-month (premiums fall at the start of a month).""" if measurement.cashflows is None or measurement.discount_factor_bom is None: raise ValueError( "profit metrics need a full=True measurement (the cash flows and " "discount factors); the headline-only fast path does not carry them.") premium = measurement.cashflows.premium_cf # (n_mp, n_time) dfb = measurement.discount_factor_bom # (n_time+1,) or (n_mp, n_time+1) df = dfb[..., :premium.shape[1]] # align to n_time return np.sum(premium * df, axis=1)
[문서] def profit_margin(measurement) -> FloatArray: """Profit margin per model point -- ``csm_plus_ra`` over the present value of premiums (the PVNBP margin). Zero-premium contracts return 0.""" pvp = _pv_premium(measurement) safe = np.where(pvp != 0.0, pvp, 1.0) return np.where(pvp != 0.0, csm_plus_ra(measurement) / safe, 0.0)
[문서] def signature(measurement, period_months: int = 12) -> ProfitSignature: """The IFRS 17 profit signature -- the per-period insurance service result (CSM release + RA release on a best-estimate run), summed over the book. Built from :func:`~fastcashflow.reporting.report`; the present value of the signature at the locked-in rate approximately reconciles to the portfolio :func:`csm_plus_ra` total (the exact figure is ``csm_plus_ra``; the annual signature is an aggregated presentation that re-discounts a year's profit from its mid-point). """ rep = _report(measurement) by = rep.by_period(period_months) profit = np.asarray(by["insurance_service_result"], np.float64) n_periods = profit.shape[0] month_end = (np.arange(1, n_periods + 1) * period_months).astype(np.int64) return ProfitSignature(period_months=period_months, month_end=month_end, profit=profit)
[문서] def irr(cashflows: FloatArray, *, period_months: int = 12, low: float = -0.99, high: float = 10.0) -> float: """Internal rate of return of a shareholder cash-flow stream (one entry per period of ``period_months`` months, period 0 first). The rate ``r`` (annual) at which the net present value is zero, found by bisection. The stream must change sign (a day-0 outgo / strain followed by profit), else there is no internal rate and a ``ValueError`` is raised -- an all-positive IFRS 17 signature has none; pair it with the new-business strain (the statutory profit test) for a meaningful IRR. """ cf = np.asarray(cashflows, np.float64) step = period_months / 12.0 t = np.arange(cf.shape[0]) * step def npv(r): return float(np.sum(cf / (1.0 + r) ** t)) f_lo, f_hi = npv(low), npv(high) if f_lo == 0.0: return low if f_lo * f_hi > 0.0: raise ValueError( "no internal rate of return in [-0.99, 10]: the cash-flow stream " "does not change sign (an IRR needs an outgo followed by income).") for _ in range(200): mid = 0.5 * (low + high) f_mid = npv(mid) if abs(f_mid) < 1e-10 or (high - low) < 1e-12: return mid if f_lo * f_mid < 0.0: high = mid else: low, f_lo = mid, f_mid return 0.5 * (low + high)
[문서] def break_even_year(cashflows: FloatArray, *, period_months: int = 12) -> int: """The first period (1-based, in ``period_months`` units) at which the cumulative shareholder cash flow turns non-negative -- the payback point. Returns -1 if it never recovers.""" cum = np.cumsum(np.asarray(cashflows, np.float64)) hit = np.nonzero(cum >= 0.0)[0] return int(hit[0] + 1) if hit.size else -1
__all__ = ["ProfitSignature", "csm_plus_ra", "profit_margin", "signature", "irr", "break_even_year"]