fastcashflow.pricing.vnb의 소스 코드

"""Value of new business (VNB) -- the new business value net of the cost of capital.

VNB is the embedded value's new-business component -- the value created by writing
new business (insurer appraisal value, IR disclosure). A thin assembly over the
profit-testing layer: :func:`fastcashflow.pricing.csm_plus_ra` is pre-tax and
*pre-required-capital* -- the present value of future profit with no charge for the
capital the entity must hold behind the contract. This module adds that charge:

    VNB = PVFP - CoC - TVOG

* ``PVFP`` -- present value of future shareholder profit, the present value of a
  profit signature (the IFRS 17 :func:`~fastcashflow.pricing.signature` or the
  traditional :func:`~fastcashflow.pricing.statutory_profit_signature`).
* ``CoC``  -- the frictional cost of holding required capital: a spread charged on
  the required-capital trajectory and present-valued. This is the same arithmetic
  the engine's cost-of-capital risk adjustment uses (a spread times the
  capital held over the run-off); here the capital is supplied by the caller.
* ``TVOG`` -- the time value of options and guarantees (e.g. the interest-rate
  guarantee cost from :func:`~fastcashflow.pricing.interest_tvog`).

This is a traditional single-rate VNB (v1): one ``reference_rate`` discounts both
the profit and the capital-cost streams, with a ``frictional_spread`` charged on
the capital. A market-consistent (risk-free reference rate + CRNHR) decomposition, real regulatory
required capital, and tax are deferred follow-ups -- v1 keeps the required capital
caller-supplied and transparent.
"""
from __future__ import annotations

from dataclasses import dataclass

import numpy as np

from fastcashflow._typing import FloatArray
from fastcashflow.curves import discount_factors_from_curve
from fastcashflow.pricing.profit import ProfitSignature


[문서] @dataclass(frozen=True, slots=True, eq=False) class VNB: """The value of new business, split into its components (portfolio total). ``pvfp`` is the present value of future shareholder profit; ``cost_of_capital`` the frictional cost of holding the required capital; ``tvog`` the time value of options and guarantees. :attr:`value` is the value of new business ``pvfp - cost_of_capital - tvog`` (positive = value-creating). """ pvfp: float cost_of_capital: float tvog: float @property def value(self) -> float: """Value of new business: ``pvfp - cost_of_capital - tvog``.""" return self.pvfp - self.cost_of_capital - self.tvog
[문서] def vnb( profit_signature: ProfitSignature, *, reference_rate: float, discount_monthly: FloatArray | None = None, required_capital: FloatArray | float | None = None, reserve: FloatArray | None = None, frictional_spread: float = 0.0, tvog: float = 0.0, ) -> VNB: """Value of new business from a profit signature and a cost of capital. ``VNB = PVFP - CoC - TVOG`` (portfolio total). The function is basis-agnostic: pass the IFRS 17 :func:`~fastcashflow.pricing.signature` or the traditional :func:`~fastcashflow.pricing.statutory_profit_signature` as ``profit_signature`` -- only its present value is used. Parameters ---------- reference_rate The annual rate discounting both the profit stream (via :meth:`ProfitSignature.present_value`) and the capital-cost stream. discount_monthly ``(n_time,)`` per-month rate curve for the cost-of-capital present value (e.g. :func:`fastcashflow.curves.discount_monthly_curve`). Required when a non-zero capital charge is requested; otherwise the CoC is zero. required_capital The required-capital trajectory ``RC_t``, portfolio total. Either an explicit ``(n_time,)`` / ``(n_time+1,)`` array (the capital held at the start of each month -- e.g. the confidence-level ``measurement.ra_path.sum(0)`` as a risk-capital proxy, or a regulatory capital path), or a scalar capital factor applied to ``reserve``. ``None`` gives a zero capital charge. reserve ``(n_time+1,)`` reserve path, used only when ``required_capital`` is a scalar factor (the capital is ``required_capital * reserve``); e.g. ``statutory_reserve(...)[0].sum(0)`` or ``measurement.bel_path.sum(0)``. frictional_spread The annual spread charged on the required capital (the cost of locking it up). Zero gives a zero capital charge. tvog The time value of options and guarantees to deduct (e.g. ``interest_tvog(...).total_value``). Default 0. Returns ------- VNB ``pvfp``, ``cost_of_capital``, ``tvog`` and the derived ``value``. Notes ----- Double counting: pairing the traditional ``statutory_profit_signature`` (whose profit carries no risk adjustment) with any required capital is clean. Pairing the IFRS 17 ``signature`` (whose profit already includes the risk-adjustment release) with the confidence-level RA path as the capital is sound but mixes views -- the RA *release* is value flowing into the PVFP, the CoC is the frictional drag on holding capital, two distinct quantities -- so the traditional pairing is the cleaner default. The capital charge ``(frictional_spread / 12) * sum_t RC_t * df_bom(t)`` is the same inception value the engine's cost-of-capital risk adjustment produces for the same capital path and annual spread (the per-month-charged backward present value of the capital), so passing the confidence-level ``ra_path.sum(0)`` with ``frictional_spread = cost_of_capital_rate`` reproduces that figure exactly. """ pvfp = profit_signature.present_value(reference_rate) if (required_capital is None or discount_monthly is None or frictional_spread == 0.0): coc = 0.0 else: df_bom = discount_factors_from_curve( np.asarray(discount_monthly, dtype=np.float64))[0] # (n_time+1,) if np.ndim(required_capital) == 0: if reserve is None: raise ValueError( "a scalar required_capital is a capital factor and needs " "reserve= (the (n_time+1,) reserve path it scales)") rc = float(required_capital) * np.asarray(reserve, dtype=np.float64) else: rc = np.asarray(required_capital, dtype=np.float64) if rc.ndim != 1: raise ValueError( "required_capital must be 1-D (portfolio total); sum a per-model-" "point capital path over the model-point axis first") if rc.shape[0] > df_bom.shape[0]: raise ValueError( f"required_capital has {rc.shape[0]} entries but the discount " f"horizon is {df_bom.shape[0]} (n_time+1); they must align") # Capital held over each month, present-valued at the start of that month # (begin-of-month factor) and charged the annual spread for one month # (the 1/12 time step). Summing RC_t * df_bom(t) over the whole path is # the engine's cost-of-capital backward present value -- align to the RC # length, do NOT truncate to n_time (that would drop the boundary column). coc = float(frictional_spread / 12.0 * np.sum(rc * df_bom[:rc.shape[0]])) return VNB(pvfp=float(pvfp), cost_of_capital=coc, tvog=float(tvog))
__all__ = ["VNB", "vnb"]