InterestRate#

InterestRate represents a constant or piecewise term-structured interest rate curve used for discounting actuarial cash-flows. It supports:

  • Constant rates: a single effective annual rate \(i\).

  • Piecewise curves: segment rates \(i_1, i_2, \ldots\) with durations \(t_1, t_2, \ldots\) (the last rate applies indefinitely).

  • Multi-scenario containers: a named set of constant or piecewise curves with an active-scenario switch for stress testing.

Discount factors \(v^n = (1+i)^{-n}\) are computed via vn() (direct \(n\)-year discount factor) and vx() (discount factor for \(x - x_0\) years, equivalent to vn(x - x0)). Term lengths can be expressed in years, months, weeks, or days.

See also

Interest Rates — Full guide to interest rate curves and scenarios.

class lactuca.InterestRate(rate: float | int | floating | integer | dict[str, float | int | floating | integer | tuple[Sequence[float | int | floating | integer] | NDArray[floating] | NDArray[integer], Sequence[float | int | floating | integer] | NDArray[floating] | NDArray[integer]]] | None = None, terms: Sequence[float | int | floating | integer] | NDArray[floating] | NDArray[integer] | None = None, rates: Sequence[float | int | floating | integer] | NDArray[floating] | NDArray[integer] | None = None, term_unit: Literal['years', 'months', 'weeks', 'days'] = 'years')#

Bases: object

Interest rate curves with efficient discount computations.

Represents a single InterestRate curve (constant or piecewise) or a multi-scenario container (named scenarios with an active scenario).

Notes

  • Constant or piecewise term-structured rates; the last rate applies indefinitely.

  • Fully vectorized discount factor computation (vn(), vx()) for piecewise curves.

  • Numeric stability: all internal computations use np.float64.

  • export() supports "dict", "json", and "regulatory" formats.

  • Public APIs normalize and validate inputs. shifted() implements temporal displacement with tolerance-aware boundary handling.

  • Prefer float64 inputs to avoid implicit precision loss in actuarial calculations.

Examples

Constant rate (scalar):

>>> from lactuca import InterestRate
>>> ir = InterestRate(0.03)
>>> ir.vn(2)
0.942595909133754...

Piecewise rates (years); terms=[1.0, 1.0] means two 1-year segments:

>>> from lactuca import InterestRate
>>> ir = InterestRate(terms=[1.0, 1.0], rates=[0.01, 0.015, 0.02])
>>> ir.vn(2.5)  # 1 year at 1%, 1 year at 1.5%, 0.5 year at 2%
0.9659...

Months/days/weeks term units:

>>> from lactuca import InterestRate
>>> ir = InterestRate(terms=[6, 12], rates=[0.01, 0.012, 0.013], term_unit="months")
>>> ir.vn(1.0)  # 1 year
0.9876...

Multi-scenario usage:

>>> from lactuca import InterestRate
>>> ir = InterestRate({"base": 0.01, "stress": ([5], [0.02, 0.03])})
>>> ir.active_scenario = "stress"
>>> ir.vn(3.0)
0.9423...

See also

InterestRate.vn

Discount factor over duration n.

InterestRate.vx

Discount factor at time t.

InterestRate.copy

Return an independent deep copy of this instance.

InterestRate.export

Serialize the curve to dict, JSON, or regulatory format.

a(*, ts: object = 0.0, d: object = 0.0, n: object = None, m: Literal[1, 2, 3, 4, 6, 12, 14, 24, 26, 52, 365] | Sequence[int] | NDArray[int64] = 1, gr: GrowthRate | float | None | Sequence[GrowthRate | float | None] = None, cashflow_times: Sequence[float] | None = None, cashflow_amounts: Sequence[float] | None = None, benefits: Sequence[float] | NDArray[float64] | None = None, return_flows: bool = False, t_output: NDArray[float64] | None = None, on_error: Literal['raise', 'nan'] = 'raise', record_ids: Sequence[Any] | None = None) float | NDArray[float64] | dict | BatchResult#

Present value of an annuity-immediate (postpayable) using the current curve.

Accepts scalar or array inputs for n, d, and ts. When any of these parameters is a sequence or array, the method operates in batch mode and returns an NDArray[np.float64] of shape (N,).

Parameters:
  • ts (float, list, NDArray[np.float64], or Pandas/Polars Series, optional) – Forward shift (years) applied to the schedule before valuation. Accepts a scalar or an array/Series of shape (N,) for per-policy values. Pandas and Polars Series are accepted without .to_numpy(). Default 0.0.

  • d (float, list, NDArray[np.float64], or Pandas/Polars Series, optional) – Deferment (years) before payments start. Accepts a scalar or an array/Series of shape (N,) for per-policy values. Pandas and Polars Series are accepted without .to_numpy(). Default 0.0.

  • n (float, None, list, NDArray[np.float64], or Pandas/Polars Series, optional) – Term in years. If None (scalar) or np.inf (batch entry) the annuity is treated as a perpetuity. Accepts a scalar or an array/Series of shape (N,) for per-policy terms. Pandas and Polars Series are accepted without .to_numpy().

  • m (PaymentFrequencyLiteral, sequence of int, NDArray[np.int64], or Pandas/Polars Series of int, optional) – Payments per year (payment frequency). Default 1 (annual). In batch mode accepts a scalar or a per-policy array of shape (N,). Pandas and Polars Series are accepted without .to_numpy(); they are converted internally via the array protocol. When heterogeneous values are provided, return_flows=True raises ValueError; all other combinations are supported.

  • gr (GrowthRate, float, None, sequence thereof, or Pandas/Polars Series of float, optional) – Growth rate applied to annuity payments. Default None (no growth). A scalar float is auto-wrapped as GrowthRate(gr) (geometric growth). In batch mode accepts a shared scalar/object or a per-policy list of length N. Pandas and Polars Series of floats are accepted without .to_numpy(); each element is treated as a plain float growth rate (no GrowthRate objects inside a Series). Distinct per-policy values are processed in sub-groups; with return_flows=True portfolio flows are aggregated across groups (same contract as LifeTable.ax).

  • cashflow_times (sequence of float or None, optional) – Explicit payment times (years). When provided, n is ignored and payments are taken from these times (after applying shift/deferment). In batch mode defines the shared payment schedule applied to all policies. Pandas and Polars Series are accepted; converted to NDArray[np.float64] internally.

  • cashflow_amounts (sequence of float or None, optional) – Payment amounts aligned with cashflow_times (or generated schedule). If provided, length must match the number of payments. Pandas and Polars Series are accepted; converted to NDArray[np.float64] internally.

  • benefits (sequence of float or NDArray[np.float64] or None, optional) – Per-policy benefit weights, shape (N,). Each policy’s cashflow contribution to the aggregate portfolio is scaled by its benefit value. Requires batch mode (array n, d, ts, m, or gr). With return_flows=False, per-policy output follows the batch unit-PV finalizer (round(unit_pv, decimals) * benefits — exact equality with a(n) * benefits). Any sequence is accepted, including Pandas and Polars Series; values are converted to float64 internally. Compatible with return_flows=False (returns NDArray[np.float64] with per-policy scaled PVs) and on_error='nan'. All values must be finite and non-negative. Default None (unit weights — equivalent to scaling by 1).

  • return_flows (bool, optional) – If True returns detailed cash-flow information instead of a scalar. In scalar mode: engine-specific dict. In batch mode: aggregate portfolio dict with keys time_grid, expected_cf, pv_cf, total_pv. Default False.

  • t_output (NDArray[np.float64] or None, optional) – External time grid onto which aggregate cash flows are projected. Pandas and Polars Series are accepted; converted to NDArray[np.float64] before use. Only effective with return_flows=True in batch mode; raises ValueError if return_flows=False. When provided, flows['time_grid'] equals t_output and flows['pv_cf'] / flows['expected_cf'] are binned accordingly. Default None (natural union grid of all payment times).

  • on_error ({'raise', 'nan'}, optional) – Error-handling policy for batch mode (ignored in scalar mode). 'raise' (default): raise ValueError on the first invalid record. 'nan': return a BatchResult with np.nan for each invalid record and a structured BatchErrorReport.

  • record_ids (sequence or None, optional) – Policy identifiers of length N, propagated into the BatchErrorReport when on_error='nan'. Ignored in scalar mode. Any sequence is accepted, including Pandas and Polars Series; elements are accessed by positional integer index.

Returns:

  • float – When n, d, and ts are all scalar and return_flows=False.

  • NDArray[np.float64] – Shape (N,) when any of n, d, ts is a sequence or array and return_flows=False.

  • dict – When return_flows=True. Scalar mode: engine dict. Batch mode: {'time_grid', 'expected_cf', 'pv_cf', 'total_pv'}.

  • BatchResult – When batch mode and on_error='nan': namedtuple with values (NDArray, NaN for invalid) and errors (BatchErrorReport).

Notes

Boolean values for ts, d, or n raise TypeError (they are not accepted as numeric durations or shifts). Accepted types for each parameter are described in Parameters above.

The present value of an annuity-immediate paying at frequency \(m\) per year for \(n\) years is:

\[a^{(m)}_{\overline{n}|} = \frac{1 - v^n}{i^{(m)}}\]

where \(v^n = (1+i)^{-n}\) and \(i^{(m)} = m\left[(1+i)^{1/m} - 1\right]\). For \(m = 1\) this reduces to \(a_{\overline{n}|} = (1 - v^n)/i\).

Batch mode

When any of n, d, ts, m, or gr is a sequence or array, the method computes present values for all N policies under the same interest rate (self). Broadcasting rules: a length-1 array broadcasts against any other length. Incompatible non-unit lengths raise ValueError.

Pandas and Polars Series are accepted for n, d, ts, m, and gr without calling .to_numpy(); they are converted element-wise to the appropriate dtype internally.

Since there is no mortality, all policies sharing the same (n, d, ts) triplet produce an identical result. The implementation groups unique triplets (O(K) dispatches, K ≤ N) and broadcasts the result — maximally efficient for homogeneous portfolios.

return_flows=True with a batch input returns an aggregate portfolio cash-flow dictionary. Supported for all four calculation modes. The expected_cf column reflects the pure payment schedule without mortality discounting. When benefits is provided, each policy’s contribution to expected_cf and pv_cf is scaled by its benefit value: total_pv = sum_i(benefits_i * unit_pv_i). With return_flows=False, per-policy values use the batch unit-PV finalizer (round unit PV, then multiply by benefits).

This method does not mutate the original curve.

on_error=’nan’ and return_flows=True

Combining on_error='nan' with return_flows=True in batch mode always raises ValueError. This matches the behaviour of LifeTable.ax.

t_output — external time grid

When t_output is provided with return_flows=True in batch mode, the aggregate cash-flow arrays are projected onto that grid instead of the natural union grid of all payment times. Useful for IFRS 17 balance-sheet dates, ALM reporting grids, or quarterly/annual alignment. Matches the t_output semantics of LifeTable.ax. Passing t_output without return_flows=True raises ValueError.

Raises:
  • TypeError – If ts, d, or n is bool or otherwise non-numeric in scalar mode.

  • ValueError – If any input parameter is invalid (e.g., negative d, n, or ts; incompatible gr and cashflow_amounts; incompatible batch array lengths; heterogeneous per-policy m combined with return_flows=True; on_error='nan' combined with return_flows=True in batch mode; t_output set with return_flows=False; t_output set in scalar (non-batch) mode; on_error='nan' in scalar (non-batch) mode; record_ids in scalar (non-batch) mode).

See also

InterestRate.ä

Present value of an annuity-due (payments at period start).

InterestRate.vn

Discount factor used in the present-value calculation.

LifeTable.ax

Mortality-weighted annuity-immediate with per-policy ir, m, gr, and t_output support.

Examples

Simple 10-year annual immediate annuity at 3%:

>>> from lactuca import InterestRate
>>> ir = InterestRate(0.03)
>>> ir.a(n=10)
8.5302...

Batch over terms — returns NDArray shape (3,):

>>> import numpy as np
>>> ir = InterestRate(0.03)
>>> ir.a(n=np.array([10.0, 20.0, 30.0]))
array([ 8.53..., 14.87..., 19.60...])

Per-policy deferment (broadcast scalar n):

>>> ir.a(n=20.0, d=np.array([0.0, 5.0, 10.0]))
array([14.87..., 12.83..., 11.07...])

Aggregate portfolio cash flows (IFRS 17 / ALM use case):

>>> flows = ir.a(n=np.array([15.0, 20.0, 25.0]), return_flows=True)
>>> sorted(flows.keys())
['expected_cf', 'pv_cf', 'time_grid', 'total_pv']

Duration analysis — PV over a range of maturities:

>>> maturities = np.arange(1.0, 11.0, 1.0)
>>> pv_curve = ir.a(n=maturities)
>>> pv_curve.shape
(10,)

Benefit-weighted portfolio cash flows (ALM / bond portfolio):

>>> import numpy as np
>>> ir = InterestRate(0.03)
>>> n_arr = np.array([10.0, 20.0, 30.0])
>>> benefits = np.array([50_000.0, 100_000.0, 75_000.0])
>>> flows = ir.a(n=n_arr, benefits=benefits, return_flows=True)
>>> sorted(flows.keys())
['expected_cf', 'pv_cf', 'time_grid', 'total_pv']

Per-policy payment frequency:

>>> import numpy as np
>>> ir = InterestRate(0.03)
>>> result = ir.a(n=np.array([10.0, 10.0, 10.0]), m=[1, 2, 12])
>>> result.shape
(3,)

Project cash flows onto an annual reporting grid (IFRS 17):

>>> import numpy as np
>>> ir = InterestRate(0.03)
>>> t_out = np.arange(1.0, 21.0, 1.0)
>>> flows = ir.a(n=np.array([10.0, 20.0]), return_flows=True, t_output=t_out)
>>> len(flows['time_grid']) == len(t_out)
True
add_scenario(name: str, scenario: InterestRate) None#

Add a named scenario to this instance.

Parameters:
  • name (str) – Name of the scenario to add. Must be unique among existing scenarios.

  • scenario (InterestRate) – An InterestRate instance (constant or piecewise). Nested multi-scenario InterestRate instances are not allowed.

Raises:
  • TypeError – If scenario is not an InterestRate instance.

  • ValueError – If scenario contains its own scenarios (nested multi-scenario).

Notes

  • If no active scenario is set, the newly added scenario becomes active.

Examples

>>> from lactuca import InterestRate
>>> ir = InterestRate({'base': 0.01})
>>> ir.add_scenario('optimistic', InterestRate(0.012))
copy() InterestRate#

Return a deep copy of this instance.

Returns:

Independent copy with all state and caches duplicated. The Config singleton is shared; _cache_lock is a fresh lock.

Return type:

InterestRate

Examples

>>> from lactuca import InterestRate
>>> ir = InterestRate(0.03)
>>> ir2 = ir.copy()
curve_analysis() dict#

Compute a concise analytical summary of the curve for diagnostics and reporting.

Returns:

Structured summary with top-level keys:

  • "type": "constant" or "piecewise"

  • "curve_policy": dict of factual discount-engine scope flags

Constant curves include these additional keys:

  • "rate": float

  • "rate_properties": dict (is_zero, is_negative, is_positive, absolute_value)

  • "key_discount_factors": {“10y”: float, “30y”: float}

Piecewise curves include these additional keys:

  • "curve_length": float (sum of terms)

  • "segment_count": int

  • "rate_range": {“min”: float, “max”: float, “spread”: float}

  • "rate_properties": counts and finiteness info

  • "curve_properties": monotonicity, zero/negative counts, volatility, mean_rate

  • "key_discount_factors": dict of selected vn values (vectorized)

Returned numeric values are Python floats/ints (cast from float64) for easy serialization.

For multi-scenario containers, returns a dict mapping each scenario name to its individual curve_analysis() result; no top-level "type" or "curve_policy" keys are present in that case.

Return type:

dict

Notes

  • This method does not mutate instance state.

  • Designed for quick diagnostics; curve_policy states what the discount engine supports — not regulatory compliance. For exhaustive reports use validate() and export() (format="regulatory").

  • Outputs use native Python types (float, int, bool).

Examples

>>> from lactuca import InterestRate
>>> ir = InterestRate(0.03)
>>> ir.curve_analysis()["type"]
'constant'
>>> ir = InterestRate(terms=[5, 5], rates=[0.02, 0.03, 0.04])
>>> ir.curve_analysis()["segment_count"]
2
d_m(m, t: object = 0.0) float#

Compute the nominal discount rate convertible m-thly, \(d^{(m)}\).

The nominal discount rate \(d^{(m)}\) is the per-period discount rate, applied \(m\) times per year, that is equivalent to the effective annual rate \(i\):

\[d^{(m)} = m \left[1 - (1 + i)^{-1/m}\right]\]

For piecewise term structures the segment rate \(i(t)\) at time t is used for the conversion.

Parameters:
  • m (int) – Discounting frequency per year. Must be a positive integer — any value is valid (e.g. 12 monthly, 4 quarterly, 1 annual, 5 or 24 for non-standard frequencies). This m is not restricted to the payment frequencies accepted by annuity methods (a, ä); it is a pure rate-conversion parameter.

  • t (float or int, optional) – Time in years used to select the applicable segment rate for piecewise curves. Has no effect for constant-rate instances. Must be non-negative. Default 0.0.

Returns:

Nominal discount rate convertible m-thly, \(d^{(m)}\).

Return type:

float

Raises:
  • TypeError – If m is bool, not a plain int, or if t is not numeric.

  • ValueError – If m is not positive, if t is negative, or if the segment rate \(i\) satisfies \(i \leq -1\).

Notes

  • bool is rejected explicitly even though it is a subclass of int in Python.

Key identities:

  • \(d^{(1)} = d = i / (1+i)\) (annual effective discount rate).

  • For \(i > 0\) and \(m > 1\): \(d^{(m)} > d\) (more frequent discounting requires a higher nominal rate).

  • Ordering: \(d < d^{(m)} < \delta < i^{(m)} < i\) for \(i > 0\), \(m > 1\).

  • Relationship with nominal interest rate: \(d^{(m)} = i^{(m)} \big/ (1 + i^{(m)}/m)\).

  • Limiting case: \(\lim_{m \to \infty} d^{(m)} = \delta = \ln(1+i)\).

The notation \(d^{(m)}\) (coded as d_m) is standard in actuarial literature: Bowers et al., Dickson et al., and other international actuarial literature.

Examples

Monthly nominal discount rate from 3 % effective annual:

>>> from lactuca import InterestRate
>>> ir = InterestRate(0.03)
>>> ir.d_m(12)
0.029522...

Annual (m=1) returns the annual effective discount rate \(d = i/(1+i)\):

>>> ir.d_m(1)
0.029126...

Ordering \(d^{(m)} < i^{(m)}\):

>>> ir = InterestRate(0.03)
>>> ir.d_m(12) < ir.i_m(12)
True

Piecewise:

>>> ir = InterestRate(terms=[5, 5], rates=[0.02, 0.03, 0.04])
>>> ir.d_m(12, t=6.0)
0.029522...

See also

InterestRate.i_m

Nominal interest rate \(i^{(m)}\).

InterestRate.delta

Force of interest \(\delta = \lim_{m \to \infty} d^{(m)}\).

InterestRate.sn

Accumulation factor \((1+i)^n\).

InterestRate.get_effective_rate

Effective rate over a custom time interval.

delta(t: object = None) object#

Compute the instantaneous force of interest (\(\delta\)) at the given time(s).

Parameters:

t (float, int, list, NDArray[np.float64], Pandas/Polars Series, or None, optional) – Time(s) in years at which to compute the force. If t is None and the curve is constant, the scalar constant force is returned. For piecewise curves t must be provided.

Returns:

Force of interest \(\delta = \ln(1+i)\), returning 0.0 where i == 0. A Python float for scalar input (or when t is None for a constant curve); NDArray[np.float64] for array-like input.

Return type:

float or NDArray[np.float64]

Raises:
  • TypeError – If t is boolean or otherwise non-numeric (when provided).

  • ValueError – If any queried instantaneous rate \(i\) satisfies \(i \leq -1\) (which makes the logarithm undefined). If t is None for a piecewise curve.

Notes

The force of interest is defined by:

\[\delta = \ln(1 + i)\]

For zero rates (\(i = 0\)), the force of interest is exactly \(0\). This method supports negative rates per modern regulatory practice and only rejects values that make \(\ln(1 + i)\) undefined (\(i \leq -1\)).

Examples

Constant curve:

>>> from lactuca import InterestRate
>>> ir = InterestRate(0.03)
>>> ir.delta()
0.0295588...

Piecewise scalar:

>>> ir = InterestRate(terms=[5], rates=[0.02, 0.03])
>>> ir.delta(2.0)
0.0198026...

Vectorized:

>>> ir.delta([0.5, 1.5, 6.0])
export(format: Literal['dict', 'json', 'regulatory'] = 'dict') dict | str#

Serialize the InterestRate instance for API integration or regulatory reporting.

Three output formats are supported:

  • "dict" (default): plain Python dict with curve data and minimal metadata.

  • "json": JSON-formatted string (indent=2) suitable for storage or HTTP transport.

  • "regulatory": enriched dict with additional metadata required for regulatory reporting (timestamps, compliance notes, per-scenario summary).

Payload structure by curve type:

  • Constant curve: {"type": "constant", "rate": <float>}.

  • Piecewise curve: {"type": "piecewise", "terms": [...], "rates": [...]}.

  • Multi-scenario container: maps scenario names to each scenario’s dict export.

  • When format == "regulatory", extra keys "_regulatory_metadata" (single curves) and "_metadata" (multi-scenario) are added, both including an ISO 8601 export_timestamp.

Use "dict" when downstream code needs to inspect or transform the payload programmatically; use "json" for serialisation to storage or HTTP transport.

Parameters:

format (str, optional) – Output format. One of "dict", "json", or "regulatory". Default "dict".

Returns:

The exported payload as a dict (for "dict" and "regulatory") or a JSON string (for "json").

Return type:

Union[dict, str]

Notes

  • For piecewise curves, term lengths and rates are rounded to 6 decimal places in the exported payload for stable serialisation; constant curve rates are exported at full float64 precision. Instance state is unaffected in all cases.

  • Use the "dict" payload to reconstruct an equivalent InterestRate programmatically. For complex roundtrips (scenarios, metadata) reconstruct by creating InterestRate objects from the exported scenario entries.

Examples

Constant curve:

>>> from lactuca import InterestRate
>>> ir = InterestRate(0.03)
>>> ir.export("dict")
{'type': 'constant', 'rate': 0.03}

Piecewise curve to JSON:

>>> ir = InterestRate(terms=[1.0, 2.0], rates=[0.01, 0.015, 0.02])
>>> s = ir.export("json")  # JSON string

Multi-scenario regulatory export:

>>> multi = InterestRate({'base': 0.01, 'stress': ([5], [0.02, 0.03])})
>>> multi.export("regulatory")
{...}

See also

InterestRate.validate

Validate the curve and return a structured report.

InterestRate.curve_analysis

Quick analytical summary for diagnostics.

get_average_force(n: object) float#

Compute the time-average (mean) force of interest over \([0, n]\).

The average force \(\bar{\delta}\) is defined so that \(\exp(-n \bar{\delta}) = v^n\), where \(v^n\) is the discount factor for duration \(n\). Equivalently:

\[\bar{\delta} = -\frac{\ln(v^n)}{n}\]
Parameters:

n (float or int) – Positive duration in years over which to compute the average force.

Returns:

Time-average force of interest \(\bar{\delta}\) as a Python float.

Return type:

float

Raises:
  • TypeError – If n is boolean or not numeric.

  • ValueError – If n <= 0 or the resulting discount factor \(v^n \leq 0\) (which makes the average force undefined).

Notes

The average force satisfies the following identities:

  • \(\exp(-n\bar{\delta}) = v^n\) where \(v^n\) is the n-year discount factor.

  • \(\bar{\delta} = \frac{1}{n}\int_0^n \delta(t)\,dt\) (true time-average of the instantaneous force).

  • For constant-rate curves: \(\bar{\delta} = \delta = \ln(1+i)\).

  • For piecewise curves: \(\bar{\delta}\) captures the weighted-average effect of all segments.

Implementation preserves float64 precision throughout. Supports zero and negative nominal rates per modern regulatory practice; only rejects cases where \(v^n \leq 0\).

Examples

Constant rate:

>>> from lactuca import InterestRate
>>> ir = InterestRate(0.03)
>>> round(ir.get_average_force(10), 6)
0.029559

Piecewise curve:

>>> ir = InterestRate(terms=[5, 5], rates=[0.02, 0.04, 0.03])
>>> ir.get_average_force(10)
get_effective_rate(t_start: object, t_end: object) float#

Compute the effective periodic interest rate between two time points.

The effective rate \(r_\text{eff}\) for period \([t_\text{start},\, t_\text{end}]\) is the single-period compound rate that satisfies:

\[\left(1 + r_\text{eff}\right) = \left(\frac{v(t_\text{end})}{v(t_\text{start})}\right)^{-1/\tau}\]

where \(v(t)\) is the present-value discount factor at time \(t\) and \(\tau = t_\text{end} - t_\text{start}\) is the period length in years.

Parameters:
  • t_start (float or int) – Start time in years (must be < t_end).

  • t_end (float or int) – End time in years (must be > t_start).

Returns:

Effective rate for the interval [t_start, t_end]. Returns 0.0 when there is no discounting over the interval (i.e. v_end == v_start).

Return type:

float

Raises:
  • TypeError – If t_start or t_end is boolean or otherwise non-numeric.

  • ValueError – If t_end <= t_start, or if the discount factor at t_start or t_end is zero (which makes the effective rate undefined).

Notes

  • Preserves float64 precision throughout.

  • Supports zero and negative rate environments per Solvency II / IFRS 17.

  • Prefer delta() for force-of-interest calculations; this method returns the discrete (effective) rate for the requested interval.

  • The method is robust against common numerical issues.

Examples

>>> from lactuca import InterestRate
>>> ir = InterestRate(0.03)
>>> round(ir.get_effective_rate(0.0, 1.0), 8)
0.03
>>> ir = InterestRate(terms=[5, 5], rates=[0.02, 0.03, 0.04])
>>> ir.get_effective_rate(2.0, 7.0)
get_rate(t: object) object#

Retrieve the instantaneous rate(s) at time t.

Parameters:

t (float, int, list, NDArray[np.float64], or Pandas/Polars Series) – Time(s) in years at which to obtain the spot rate. Accepts a scalar numeric (int/float) or an array-like of numeric values. Pandas and Polars Series are accepted; values are converted to NDArray[np.float64] internally.

Returns:

Rate applying at time t. A Python float for scalar input; NDArray[np.float64] for array-like input.

Return type:

float or NDArray[np.float64]

Raises:

TypeError – If t is boolean or otherwise non-numeric.

Notes

  • If this InterestRate contains named scenarios, the call is delegated to the currently active scenario.

  • For constant curves this method returns the constant rate (scalar or an array filled with the constant value).

  • For piecewise curves the rate for each queried time is the rate of the segment that contains it; the final rate applies indefinitely beyond the last term.

  • At an exact cumulative junction \(t = T_k\), the rate of the segment ending at \(T_k\) is returned (left-closed intervals \([T_{k-1}, T_k]\)). Implementation: numpy.searchsorted(cum_terms, t, side='left'), consistent with get_segment_info() and vn().

Examples

>>> from lactuca import InterestRate
>>> ir = InterestRate(terms=[1.0, 2.0], rates=[0.01, 0.015, 0.02])
>>> ir.get_rate(0.5)
0.01
>>> ir.get_rate([0.5, 1.5, 5.0])
array([0.01 , 0.015, 0.02 ])
get_segment_info(t: object) dict#

Return detailed information about the rate segment that applies at time t.

Parameters:

t (float or int) – Query time in years (must be non-negative).

Returns:

A dictionary describing the segment at time t. Keys present for both constant and piecewise curves:

  • type (str) – "constant" or "piecewise".

  • rate (float) – The rate applying at time t.

  • rate_properties (dict) – Booleans {'is_zero', 'is_negative', 'is_positive'}.

  • segment_start (float) – Start time of the segment (years).

  • segment_end (float) – End time of the segment (float('inf') for the last or only segment).

  • time_queried (float) – The original t value.

Additional keys for piecewise curves only:

  • segment_index (int) – Zero-based index of the segment.

  • segment_duration (float) – Duration of the segment (or float('inf') for the last segment).

  • position_in_segment (float) – Offset of t from segment_start.

Return type:

dict

Raises:

TypeError – If t is boolean or not a numeric scalar.

Notes

  • For multi-scenario containers, the result describes the active scenario.

  • For constant curves the segment covers \([0, \infty)\).

  • For piecewise curves the final rate applies indefinitely beyond the last cumulative term.

  • All numeric values are returned as native Python floats for easy serialization and display.

Examples

>>> from lactuca import InterestRate
>>> ir = InterestRate(terms=[2.0, 3.0], rates=[0.01, 0.02, 0.03])
>>> info = ir.get_segment_info(1.5)
>>> info['segment_index']
0
>>> info['position_in_segment']  # 1.5 years from origin, first segment starts at 0.0
1.5
i_m(m, t: object = 0.0) float#

Compute the nominal interest rate convertible m-thly, \(i^{(m)}\).

The nominal rate \(i^{(m)}\) is the per-period rate, compounded \(m\) times per year, that is equivalent to the effective annual rate \(i\):

\[i^{(m)} = m \left[(1 + i)^{1/m} - 1\right]\]

For piecewise term structures the segment rate \(i(t)\) at time t is used for the conversion.

Parameters:
  • m (int) – Compounding frequency per year. Must be a positive integer — any value is valid (e.g. 12 monthly, 4 quarterly, 2 semi-annual, 1 annual, 5 or 24 for non-standard frequencies). This m is not restricted to the payment frequencies accepted by annuity methods (a, ä); it is a pure rate-conversion parameter.

  • t (float or int, optional) – Time in years used to select the applicable segment rate for piecewise curves. Has no effect for constant-rate instances. Must be non-negative. Default 0.0.

Returns:

Nominal interest rate convertible m-thly, \(i^{(m)}\).

Return type:

float

Raises:
  • TypeError – If m is bool, not a plain int, or if t is not numeric.

  • ValueError – If m is not positive, if t is negative, or if the segment rate \(i\) satisfies \(i \leq -1\) (which makes \((1+i)^{1/m}\) undefined for real arithmetic).

Notes

  • bool is rejected explicitly even though it is a subclass of int in Python.

Key identities:

  • \(i^{(1)} = i\) (annual, no conversion).

  • For \(i > 0\) and \(m > 1\): \(d^{(m)} < \delta < i^{(m)} < i\) (more frequent compounding requires a lower nominal rate).

  • Relationship with nominal discount rate: \(d^{(m)} = i^{(m)} \big/ \left(1 + i^{(m)}/m\right)\).

  • Limiting case: \(\lim_{m \to \infty} i^{(m)} = \delta = \ln(1+i)\).

For piecewise term structures this method returns the point-in-time conversion at the single segment containing t, not a weighted average over the full curve. To derive a representative nominal rate over a multi-segment horizon, use get_effective_rate() to obtain the period effective rate first, then apply the formula above.

The notation \(i^{(m)}\) (coded as i_m) is standard in actuarial literature: Bowers et al., Dickson et al., and other international actuarial literature.

Examples

Monthly nominal rate from 3 % effective annual:

>>> from lactuca import InterestRate
>>> ir = InterestRate(0.03)
>>> ir.i_m(12)
0.029595...

Annual (m=1) returns the effective rate unchanged:

>>> ir.i_m(1)
0.03

Piecewise — rate at the segment containing t=6.0:

>>> ir = InterestRate(terms=[5, 5], rates=[0.02, 0.03, 0.04])
>>> ir.i_m(12, t=6.0)
0.029595...

See also

InterestRate.d_m

Nominal discount rate \(d^{(m)}\).

InterestRate.delta

Force of interest \(\delta = \lim_{m \to \infty} i^{(m)}\).

InterestRate.sn

Accumulation factor \((1+i)^n\).

InterestRate.get_effective_rate

Effective rate over a custom time interval.

shifted(ts: object) InterestRate#

Shift the interest-rate curve forward by ts years and return the adjusted curve.

Parameters:

ts (float or int) – Forward displacement in years. If ts > 0 the curve is shifted forward (initial segments are shortened or removed). If ts <= 0 the method returns an independent copy of this instance (no temporal change).

Returns:

A new InterestRate instance representing the original curve displaced forward by ts years. If ts exceeds the total term length the result is a constant-rate curve using the final rate. When ts <= 0 or the curve is constant, returns self.copy() so callers never share mutable state.

Return type:

InterestRate

Raises:

TypeError – If ts is boolean or not a numeric type.

Notes

  • For multi-scenario containers, the result applies to the active scenario.

  • Boundary handling: when the remaining first-term duration after the shift is at or below the configured boundary tolerance, that segment is skipped and the next rate takes effect immediately.

  • Returned curves use 'years' as the internal term unit.

  • This method does not mutate the original instance.

Examples

>>> from lactuca import InterestRate
>>> ir = InterestRate(terms=[5, 5], rates=[0.02, 0.03, 0.04])
>>> ir.shifted(0.0) is not ir  # zero shift returns independent copy
True
>>> shifted = ir.shifted(2.5)  # new curve with first term shortened
>>> shifted.is_constant
False
sn(n: object) object#

Compute the accumulation factor \(s_n = (1+i)^n\).

The accumulation factor is the reciprocal of the discount factor \(v^n\). For piecewise term structures the formula compounds correctly across segment boundaries, delegating to the optimised prefix-product implementation in vn().

Parameters:

n (float, int, list, NDArray[np.float64], or Pandas/Polars Series) – Duration(s) in years. Accepts a scalar, a list, or a NumPy array. Pandas and Polars Series are accepted; values are converted to float64 internally.

Returns:

Accumulation factor(s) \((1+i)^n\). Returns float('inf') (element-wise for arrays) when the corresponding discount factor underflows to exactly zero (may occur at extremely long durations or very high rates).

Return type:

float or NDArray[np.float64]

Raises:

TypeError – If n is boolean or otherwise non-numeric.

Notes

For a constant rate \(i\):

\[s_n = (1 + i)^n\]

For a piecewise term structure with segment lengths \(\Delta_1, \Delta_2, \ldots\) and effective annual rates \(i_1, i_2, \ldots\):

\[s_n = \prod_{k} (1 + i_k)^{\min(\Delta_k,\, \max(0,\, n - T_{k-1}))}\]

Supports zero and negative rates per modern European regulatory practice (Solvency II / IFRS 17).

The sn / vn duality holds exactly up to float64 precision: sn(n) * vn(n) == 1.0.

Examples

Constant rate:

>>> from lactuca import InterestRate
>>> ir = InterestRate(0.03)
>>> ir.sn(5)
1.159274074...

Vectorized:

>>> ir.sn([1, 5, 10])
array([1.03      , 1.15927407, 1.34391638])

Piecewise:

>>> ir = InterestRate(terms=[5, 5], rates=[0.02, 0.03, 0.04])
>>> ir.sn(7.5)

Duality with vn():

>>> ir = InterestRate(0.03)
>>> abs(ir.sn(10) * ir.vn(10) - 1.0) < 1e-12
True

See also

InterestRate.vn

Discount factor \(v^n = 1 / s_n\).

InterestRate.delta

Force of interest \(\delta = \ln(1+i)\).

InterestRate.get_average_force

Time-average force of interest over \([0, n]\).

summary() str#

Alias for __str__ for compatibility.

Returns:

Human-readable string representation, equivalent to calling str() on the curve.

Return type:

str

validate() dict#

Validate this InterestRate instance and return a structured report.

Perform structural and numeric checks for the current curve (or all named scenarios) and produce a deterministic, machine-friendly validation report. The method does not raise on validation issues; it returns a dict describing findings so callers can decide whether to raise, log, or continue.

Returns:

Hierarchical report whose structure depends on the curve type.

Single curves (constant or piecewise) always include:

  • "type" (str): "constant" or "piecewise".

  • "status" (str): "valid", "warning", or "invalid".

Constant curves add: "rate", "rate_properties", "curve_policy".

Piecewise curves add: "structure", "rates", "rate_statistics", "curve_policy", "actuarial_metrics".

Multi-scenario containers return a dict with:

  • "scenarios" (dict): mapping of scenario name to each scenario’s validate() report.

  • "scenario_summary" (dict): "total_scenarios", "active_scenario", "scenario_names".

  • "cross_scenario_analysis" (dict): aggregate statistics across all scenarios (counts, min/max rates, spread).

Intended for programmatic consumption and regulatory exports.

Return type:

dict

Notes

  • Validation preserves float64 precision throughout; zero and negative rates are allowed (modern European curve practice).

  • curve_policy describes discount-engine capabilities only — not Solvency II or IFRS 17 regulatory sign-off.

  • Callers that require exceptions on invalid data should inspect the returned status and raise accordingly.

Examples

>>> from lactuca import InterestRate
>>> ir = InterestRate(terms=[5, 5], rates=[0.02, 0.03, 0.04])
>>> report = ir.validate()
>>> report["status"] == "valid"
True
>>> multi = InterestRate({'base': 0.01, 'shock': ([5], [0.02, 0.03])})
>>> multi_report = multi.validate()
>>> "scenarios" in multi_report
True

See also

InterestRate.curve_analysis

Quick analytical summary for diagnostics.

InterestRate.export

Serialize the curve for API integration or regulatory reporting.

vn(n: object) object#

Compute the discount factor(s) \(v^n = (1+i)^{-n}\) for a given duration.

For constant curves this equals \((1+i)^{-n}\) directly; for piecewise term structures the formula compounds correctly across segment boundaries.

Parameters:

n (float, int, list, NDArray[np.float64], or Pandas/Polars Series) – Duration(s) in years for which the discount factor(s) are calculated. Pandas and Polars Series are accepted; values are converted to float64 internally. Scalar inputs return a Python float; array-like inputs return a NDArray[np.float64].

Returns:

Discount factor(s) corresponding to the given duration(s) n. A Python float for scalar input; NDArray[np.float64] for array-like input.

Return type:

float or NDArray[np.float64]

Raises:
  • TypeError – If n is boolean or otherwise non-numeric.

  • ValueError – If any rate in the curve satisfies \(i \leq -1\), because \((1+i)^{-n}\) requires \((1+i) > 0\). Rates in \((-1, 0)\) are valid (e.g. negative ECB deposit rates) and do not raise. For piecewise curves, the message includes the offending array indices.

Notes

  • If the interest rate is zero, the discount factor is always 1.0, regardless of n.

  • For piecewise curves, non-positive durations return 1.0. For constant curves, the formula \((1+i)^{-n}\) is applied as-is; negative n yields an accumulation factor greater than 1.

  • Rate validation for \(i \leq -1\) is performed on this public path only; internal callers with validate=False bypass the check for performance.

Examples

Constant rate:

>>> from lactuca import InterestRate
>>> ir = InterestRate(0.03)
>>> ir.vn(5)
0.862608784384163...
>>> ir.vn([1, 5, 10])  # Vectorized array calculation
array([0.97087379, 0.86260878, 0.74409391])

Negative but valid rate (deflation scenario):

>>> ir_neg = InterestRate(-0.005)  # -0.5 %, valid per ECB practice
>>> ir_neg.vn(1)
1.005025...

Invalid rate raises ValueError:

>>> InterestRate(-1.0).vn(5)
ValueError: Rate -1.0 ≤ -1: (1+i) must be positive ...

Piecewise rate:

>>> ir = InterestRate(terms=[5, 5], rates=[0.02, 0.03, 0.04])
>>> ir.vn(7.5)
>>> ir.vn([2.5, 7.5, 12.5])

Zero rate:

>>> ir = InterestRate(0.0)
>>> ir.vn(10)  # Always returns 1.0 for zero rates
1.0

Multi-scenario:

>>> ir = InterestRate({'base': 0.03, 'stress': ([5, 5], [0.02, 0.03, 0.04])})
>>> ir.active_scenario = 'stress'
>>> ir.vn(7.5)

See also

InterestRate.vx

Relative discount factor between two time points.

InterestRate.sn

Accumulation factor \((1+i)^n\), the reciprocal of \(v^n\).

InterestRate.delta

Force of interest; raises on the same rate ≤ -1 condition.

vx(x: object, x0: object = 0.0) object#

Compute the discount factor for a duration of \(x - x_0\) years.

Applies the interest rate curve over the duration \(x - x_0\), starting from the curve origin. For constant-rate curves this equals \((1+i)^{-(x-x_0)}\); for piecewise term structures the segments are traversed from the curve origin over \(x - x_0\) years.

This is equivalent to vn(x - x0) in all cases. The parameter x0 is the temporal anchor of the curve (for example, the reference age for commutation functions), so the first rate segment is applied starting at x0 and the discount factor covers the period \([x_0,\, x]\).

Parameters:
  • x (float, int, list, NDArray[np.float64], or Pandas/Polars Series) – Target time(s) in years. Scalar, sequence, or array-like input. Pandas and Polars Series are accepted; values are converted to float64 internally.

  • x0 (float, int, list, NDArray[np.float64], or Pandas/Polars Series, optional) – Initial time(s) in years. Default is 0.0. Scalar, sequence, or array-like input. Pandas and Polars Series are accepted; values are converted to float64 internally.

Returns:

Discount factor(s) for the duration \(x - x_0\). A Python float when both x and x0 are scalars; NDArray[np.float64] when either is array-like.

Return type:

float or NDArray[np.float64]

Raises:
  • TypeError – If x or x0 is boolean or otherwise non-numeric.

  • ValueError – If x or x0 contains invalid values (e.g., negative times). If x and x0 have incompatible shapes for broadcasting. If any value in x is less than the corresponding value in x0.

Examples

Valid inputs:

>>> from lactuca import InterestRate
>>> ir = InterestRate(0.03)
>>> ir.vx(10, 5)
0.862608784384163...
>>> ir.vx([10, 15], 5)
array([0.86260878, 0.74409391])

Broadcasting:

>>> ir.vx([10, 15], [5, 10])
array([0.86260878, 0.86260878])

Invalid inputs raise ValueError:

>>> ir.vx(-10, 5)
>>> ir.vx([10, 15], [5, 20])

See also

InterestRate.vn

Absolute discount factor for a given duration.

InterestRate.sn

Accumulation factor \((1+i)^n\), the reciprocal of \(v^n\).

ä(*, ts: object = 0.0, d: object = 0.0, n: object = None, m: Literal[1, 2, 3, 4, 6, 12, 14, 24, 26, 52, 365] | Sequence[int] | NDArray[int64] = 1, gr: GrowthRate | float | None | Sequence[GrowthRate | float | None] = None, cashflow_amounts: Sequence[float] | None = None, benefits: Sequence[float] | NDArray[float64] | None = None, return_flows: bool = False, t_output: NDArray[float64] | None = None, on_error: Literal['raise', 'nan'] = 'raise', record_ids: Sequence[Any] | None = None) float | NDArray[float64] | dict | BatchResult#

Present value of an annuity-due (prepayable) using the current interest curve.

Accepts scalar or array inputs for n, d, and ts. When any of these parameters is a sequence or array, the method operates in batch mode and returns an NDArray[np.float64] of shape (N,).

Parameters:
  • ts (float, list, NDArray[np.float64], or Pandas/Polars Series, optional) – Forward shift (years) applied to the schedule before valuation. Accepts a scalar or an array/Series of shape (N,) for per-policy values. Pandas and Polars Series are accepted without .to_numpy(). Default 0.0.

  • d (float, list, NDArray[np.float64], or Pandas/Polars Series, optional) – Deferment (years) before payments start. Accepts a scalar or an array/Series of shape (N,) for per-policy values. Pandas and Polars Series are accepted without .to_numpy(). Default 0.0.

  • n (float, None, list, NDArray[np.float64], or Pandas/Polars Series, optional) – Term in years. If None (scalar) or np.inf (batch entry) the annuity is treated as a perpetuity. Accepts a scalar or an array/Series of shape (N,) for per-policy terms. Pandas and Polars Series are accepted without .to_numpy().

  • m (PaymentFrequencyLiteral, sequence of int, NDArray[np.int64], or Pandas/Polars Series of int, optional) – Payments per year (payment frequency). Default 1 (annual). In batch mode accepts a scalar or a per-policy array of shape (N,). Pandas and Polars Series are accepted without .to_numpy(); they are converted internally via the array protocol. When heterogeneous values are provided, return_flows=True raises ValueError; all other combinations are supported.

  • gr (GrowthRate, float, None, sequence thereof, or Pandas/Polars Series of float, optional) – Growth rate applied to annuity payments. Default None (no growth). A scalar float is auto-wrapped as GrowthRate(gr) (geometric growth). In batch mode accepts a shared scalar/object or a per-policy list of length N. Pandas and Polars Series of floats are accepted without .to_numpy(); each element is treated as a plain float growth rate (no GrowthRate objects inside a Series). Distinct per-policy values are processed in sub-groups; with return_flows=True portfolio flows are aggregated across groups (same contract as LifeTable.äx).

  • cashflow_amounts (sequence of float or None, optional) – Payment amounts for the regular due-timing schedule generated by n and m. If provided, length must match the number of scheduled payments (n * m). Mutually exclusive with gr. Note: cashflow_times is intentionally absent — supplying explicit payment times makes the due/immediate distinction void. Use a() with cashflow_times when payment timing is fully specified by the caller.

  • benefits (sequence of float or NDArray[np.float64] or None, optional) – Per-policy benefit weights, shape (N,). Each policy’s cashflow contribution to the aggregate portfolio is scaled by its benefit value. Requires batch mode (array n, d, ts, m, or gr). With return_flows=False, per-policy output follows the batch unit-PV finalizer (round(unit_pv, decimals) * benefits — exact equality with a(n) * benefits). Any sequence is accepted, including Pandas and Polars Series; values are converted to float64 internally. Compatible with return_flows=False (returns NDArray[np.float64] with per-policy scaled PVs) and on_error='nan'. All values must be finite and non-negative. Default None (unit weights — equivalent to scaling by 1).

  • return_flows (bool, optional) – If True returns detailed cash-flow information instead of a scalar. In scalar mode: engine-specific dict. In batch mode: aggregate portfolio dict with keys time_grid, expected_cf, pv_cf, total_pv. Default False.

  • t_output (NDArray[np.float64] or None, optional) – External time grid onto which aggregate cash flows are projected. Pandas and Polars Series are accepted; converted to NDArray[np.float64] before use. Only effective with return_flows=True in batch mode; raises ValueError if return_flows=False. When provided, flows['time_grid'] equals t_output and flows['pv_cf'] / flows['expected_cf'] are binned accordingly. Default None (natural union grid of all payment times).

  • on_error ({'raise', 'nan'}, optional) – Error-handling policy for batch mode (ignored in scalar mode). 'raise' (default): raise ValueError on the first invalid record. 'nan': return a BatchResult with np.nan for each invalid record and a structured BatchErrorReport.

  • record_ids (sequence or None, optional) – Policy identifiers of length N, propagated into the BatchErrorReport when on_error='nan'. Ignored in scalar mode. Any sequence is accepted, including Pandas and Polars Series; elements are accessed by positional integer index.

Returns:

  • float – When n, d, and ts are all scalar and return_flows=False.

  • NDArray[np.float64] – Shape (N,) when any of n, d, ts is a sequence or array and return_flows=False.

  • dict – When return_flows=True. Scalar mode: engine dict. Batch mode: {'time_grid', 'expected_cf', 'pv_cf', 'total_pv'}.

  • BatchResult – When batch mode and on_error='nan': namedtuple with values (NDArray, NaN for invalid) and errors (BatchErrorReport).

Notes

Boolean values for ts, d, or n raise TypeError (they are not accepted as numeric durations or shifts). Accepted types for each parameter are described in Parameters above.

The present value of an annuity-due paying at frequency \(m\) per year for \(n\) years is:

\[\ddot{a}^{(m)}_{\overline{n}|} = \frac{1 - v^n}{d^{(m)}}\]

where \(v^n = (1+i)^{-n}\) and \(d^{(m)} = m\left[1 - (1+i)^{-1/m}\right]\). For \(m = 1\): \(\ddot{a}_{\overline{n}|} = (1 - v^n)/d\) where \(d = i/(1+i)\).

This is the prepayable (annuity-due) variant: payments occur at the start of each period.

Batch mode

When any of n, d, ts, m, or gr is a sequence or array, the method computes present values for all N policies under the same interest rate (self). Broadcasting rules: a length-1 array broadcasts against any other length. Incompatible non-unit lengths raise ValueError.

Pandas and Polars Series are accepted for n, d, ts, m, and gr without calling .to_numpy(); they are converted element-wise to the appropriate dtype internally.

Since there is no mortality, all policies sharing the same (n, d, ts) triplet produce an identical result. The implementation groups unique triplets (O(K) dispatches, K ≤ N) and broadcasts the result — maximally efficient for homogeneous portfolios.

return_flows=True with a batch input returns an aggregate portfolio cash-flow dictionary. The expected_cf column reflects the pure payment schedule without mortality discounting. When benefits is provided, each policy’s contribution is scaled by its benefit value: total_pv = sum_i(benefits_i * unit_pv_i). With return_flows=False, per-policy values use the batch unit-PV finalizer (round unit PV, then multiply by benefits).

Does not mutate the original curve.

on_error=’nan’ and return_flows=True

Combining on_error='nan' with return_flows=True in batch mode always raises ValueError. This matches the behaviour of LifeTable.äx.

t_output — external time grid

When t_output is provided with return_flows=True in batch mode, the aggregate cash-flow arrays are projected onto that grid instead of the natural union grid of all payment times. Useful for IFRS 17 balance-sheet dates, ALM reporting grids, or quarterly/annual alignment. Matches the t_output semantics of LifeTable.äx. Passing t_output without return_flows=True raises ValueError.

Raises:
  • TypeError – If ts, d, or n is bool or otherwise non-numeric in scalar mode.

  • ValueError – If any input parameter is invalid (e.g., negative d, n, or ts; incompatible gr and cashflow_amounts; incompatible batch array lengths; heterogeneous per-policy m combined with return_flows=True; on_error='nan' combined with return_flows=True in batch mode; t_output set with return_flows=False; t_output set in scalar (non-batch) mode; on_error='nan' in scalar (non-batch) mode; record_ids in scalar (non-batch) mode).

See also

InterestRate.a

Present value of an annuity-immediate (payments at period end).

InterestRate.vn

Discount factor used in the present-value calculation.

LifeTable.äx

Mortality-weighted annuity-due with per-policy ir, m, gr, and t_output support.

Examples

Simple 5-year annual annuity-due at 3%:

>>> from lactuca import InterestRate
>>> ir = InterestRate(0.03)
>>> ir.ä(n=5)
4.71709...

Batch over terms — returns NDArray shape (3,):

>>> import numpy as np
>>> ir = InterestRate(0.03)
>>> ir.ä(n=np.array([5.0, 10.0, 20.0]))
array([ 4.71...,  8.78..., 15.32...])

Per-policy deferment:

>>> ir.ä(n=20.0, d=np.array([0.0, 5.0, 10.0]))
array([15.32..., 13.21..., 11.40...])

Custom per-payment amounts on a 3-year annual due schedule:

>>> ir = InterestRate(0.03)
>>> ir.ä(n=3, cashflow_amounts=[100, 110, 121])
320.8...

Benefit-weighted portfolio cash flows (deferred pension portfolio):

>>> import numpy as np
>>> ir = InterestRate(0.03)
>>> n_arr = np.array([15.0, 20.0, 25.0])
>>> annual_pensions = np.array([12_000.0, 18_000.0, 24_000.0])
>>> flows = ir.ä(n=n_arr, benefits=annual_pensions, return_flows=True)
>>> sorted(flows.keys())
['expected_cf', 'pv_cf', 'time_grid', 'total_pv']

Per-policy payment frequency:

>>> import numpy as np
>>> ir = InterestRate(0.03)
>>> result = ir.ä(n=np.array([10.0, 10.0, 10.0]), m=[1, 2, 12])
>>> result.shape
(3,)

Project cash flows onto an annual reporting grid (IFRS 17):

>>> import numpy as np
>>> ir = InterestRate(0.03)
>>> t_out = np.arange(0.0, 21.0, 1.0)
>>> flows = ir.ä(n=np.array([10.0, 20.0]), return_flows=True, t_output=t_out)
>>> len(flows['time_grid']) == len(t_out)
True
property active_scenario: str | None#

Name of the currently active scenario.

Returns None for single-curve instances (no scenarios defined).

Returns:

Active scenario name, or None when no scenarios are present.

Return type:

Union[str, None]

See also

InterestRate.scenario_names

List of all stored scenario names.

InterestRate.add_scenario

Add a named scenario to this container.

Examples

>>> from lactuca import InterestRate
>>> ir = InterestRate({'base': 0.01, 'stress': 0.03})
>>> ir.active_scenario
'base'
>>> ir.active_scenario = 'stress'
>>> ir.active_scenario
'stress'
property calculation_mode: Literal['discrete_precision', 'discrete_simplified', 'continuous_precision', 'continuous_simplified']#

Annuity calculation mode used by a() and ä().

Returns an internal metadata override when present; otherwise mirrors the global Config.calculation_mode setting. The public InterestRate(...) constructor does not accept calculation_mode; change the mode via Config or the config alias before calling annuity methods (same source as LifeTable).

Returns:

One of 'discrete_precision', 'discrete_simplified', 'continuous_precision', or 'continuous_simplified'.

Return type:

CalculationModeLiteral

See also

Config.calculation_mode : Global calculation mode setting. Interest Rates : Calculation mode section.

Examples

>>> from lactuca import InterestRate
>>> ir = InterestRate(0.03)
>>> ir.calculation_mode
'discrete_precision'
property days_per_year: float#

Number of days per year used for day-denominator term conversions.

Returns the instance-level override (set at construction) when provided; falls back to the global Config setting otherwise.

Returns:

Days per year (one of 360.0, 365.0, or 365.25).

Return type:

float

See also

InterestRate.weeks_per_year

Equivalent setting for week-denominator conversions.

Examples

>>> from lactuca import InterestRate
>>> ir = InterestRate(0.03)
>>> ir.days_per_year
365.0
property is_constant: bool#

Whether the active curve (or this instance) is a constant-rate curve.

For multi-scenario containers, reflects the is_constant flag of the currently active scenario.

Returns:

True if the active curve has a single constant rate; False for piecewise term-structured curves.

Return type:

bool

Examples

>>> from lactuca import InterestRate
>>> InterestRate(0.03).is_constant
True
>>> InterestRate(terms=[5], rates=[0.02, 0.03]).is_constant
False
property rates: NDArray[float64] | None#

Return a copy of the normalized rates array for this piecewise curve.

Returns:

A copy of the one-dimensional float64 rates array for each segment. None if curve is constant or rates have not been set.

Return type:

Union[NDArray[np.float64], None]

Notes

For piecewise curves, the last rate applies indefinitely (perpetuity behavior).

See also

InterestRate.terms

Corresponding segment term lengths.

property scenario_names: list[str]#

List of all stored scenario names.

Returns:

Names of all stored scenarios in insertion order. Returns an empty list for single-curve instances.

Return type:

list[str]

See also

InterestRate.active_scenario

Currently active scenario name.

InterestRate.add_scenario

Add a named scenario to this container.

Examples

>>> from lactuca import InterestRate
>>> ir = InterestRate({'base': 0.01, 'stress': 0.03})
>>> ir.scenario_names
['base', 'stress']
property terms: NDArray[float64] | None#

Return a copy of the normalized segment term lengths for this piecewise curve.

Returns:

A copy of the one-dimensional float64 term-length array (in years). None if curve is constant or terms have not been set.

Return type:

Union[NDArray[np.float64], None]

Notes

Terms represent the duration of each rate segment. For a piecewise curve with \(n\) terms, there must be \(n+1\) rates (the last rate applies beyond the final term).

See also

InterestRate.rates

Corresponding interest rates for each segment.

property weeks_per_year: float#

Number of weeks per year used for week-denominator term conversions.

Returns the instance-level override (set at construction) when provided; falls back to the global Config setting otherwise.

Returns:

Weeks per year (52.0 or 52.1775).

Return type:

float

See also

InterestRate.days_per_year

Equivalent setting for day-denominator conversions.

Examples

>>> from lactuca import InterestRate
>>> ir = InterestRate(0.03)
>>> ir.weeks_per_year
52.0