Source code for ratingmodels.constraints

r"""Constraints applied to indicated rates before they become rate actions.

Indicated rates are rarely charged as-is. Common adjustments:

* **Caps / floors** on the rate change to limit renewal shock.
* **Banding** -- snapping small changes to zero, or to discrete steps.
* **Rounding** to a filed precision.
* **Corridors** -- limiting how far a rate may move over successive renewals.
"""
from __future__ import annotations

import numpy as np


[docs] def cap_change(change: float, cap: float | None = None, floor: float | None = None) -> float: """Clip a proportional rate change to ``[floor, cap]`` (either may be None).""" out = float(change) if cap is not None: out = min(out, float(cap)) if floor is not None: out = max(out, float(floor)) return out
[docs] def apply_cap( current_rate: float, indicated_rate: float, cap: float | None = None, floor: float | None = None, ) -> float: """Return the charged rate after capping the implied change.""" if current_rate <= 0: raise ValueError("current_rate must be positive") change = indicated_rate / current_rate - 1.0 return current_rate * (1.0 + cap_change(change, cap, floor))
[docs] def band(change: float, deadband: float = 0.0, step: float | None = None) -> float: """Snap a change to zero within ``deadband``; optionally to ``step`` grid.""" out = 0.0 if abs(change) <= deadband else float(change) if step is not None and step > 0: out = round(out / step) * step return out
[docs] def round_rate(rate: float, ndigits: int = 2) -> float: """Round a rate to a filed precision (default cents).""" return float(np.round(rate, ndigits))
[docs] def corridor( current_rate: float, indicated_rate: float, max_up: float, max_down: float, ) -> float: """Limit a single renewal move to ``[-max_down, +max_up]`` proportionally.""" return apply_cap(current_rate, indicated_rate, cap=max_up, floor=-abs(max_down))