Interest Rate Assumption Governance
Interest rate assumption governance is the control discipline that decides which discount and reinvestment rates an actuarial valuation is allowed to use, where each rate came from, who approved it, and how a proposed curve is proven against its market source before it ever reaches a projection engine. Under the principle-based framework of NAIC VM-20, the net asset earned rate and the prescribed valuation rates are not free parameters an actuary types into a model — they are governed quantities with a documented lineage, an effective date, and a reconciliation back to observable Treasury and swap markets. This guide, part of the Assumption Validation & Rule Engine Design discipline, sets out how to source risk-free curves, construct and version an interest assumption, gate it against source data, and release it into production with an approver and an audit record that survives examination.
The Problem
Discount rates move reserves more than almost any other assumption, and they move them non-linearly. A ten-basis-point shift in the net asset earned rate on a long-duration block re-prices decades of projected cash flows, so an ungoverned rate is not a small error — it is a material misstatement waiting for an examiner to find. Yet interest assumptions are uniquely exposed to sloppy governance because they change every valuation cycle. Unlike a mortality table that is set once and reviewed annually, the discount and reinvestment rates are re-sourced from the market at each quarter-end, which means the opportunity to introduce a stale curve, a mis-keyed tenor, or an unapproved override recurs on every filing calendar.
VM-20 makes the stakes explicit. The deterministic reserve of Section 4 discounts modeled net cash flows at the path of net asset earned rates, where that rate is the projected earned rate on the assets backing the block net of default costs and investment expense. The reinvestment assumption — the rate at which projected positive cash flows are assumed to be reinvested and negative cash flows funded — grades from the modeled portfolio rate toward prescribed long-term Treasury and investment-grade spreads over a defined number of years. Section 7B fixes those prescribed reinvestment rates so that no carrier can flatter its reserve by assuming an optimistic forward curve. When a valuation reports a deterministic reserve, an examiner is entitled to ask three questions: what earned-rate path produced it, what market data underpins that path, and who signed off. Governance exists to make all three answerable without a scramble.
The failure this page prevents is the silent one. A proposed curve that looks reasonable on a chart can still carry a settlement-date mismatch, a par-versus-spot confusion, or an interpolation artifact at an unquoted tenor. None of those show up in the headline reserve; all of them show up when someone tries to reproduce the number. Interest rate assumption governance turns the curve from an input someone remembers setting into a versioned, validated, approved artifact the model is contractually bound to consume.
Architecture
The governance pipeline is a one-way street with a gate in the middle. Market data enters on the left as raw Treasury and swap quotes; it is transformed into a candidate curve; that candidate is validated against the source it claims to represent; only on passing does it become an approved, versioned assumption; and only an approved assumption is admitted to the projection engine. Nothing skips the gate, and nothing enters the engine that the gate did not stamp.
The market source stage collects the raw quotes: constant-maturity Treasury yields, on-the-run bond prices, or swap rates, each stamped with the settlement date they represent. Curve construction bootstraps those quotes into a spot (zero-coupon) curve and interpolates the tenors the projection grid needs but the market does not quote directly. The validation gate is the heart of the design — it re-derives observable quantities from the candidate curve and compares them against the source within tolerance, refusing any curve that reprices its own inputs incorrectly. The reinvestment and net-asset-earned-rate logic then layers the prescribed VM-20 grading on top of the validated risk-free base. What emerges is an approved assumption object: an immutable, versioned record carrying the curve, its effective date, the approver, and a checksum. The projection engine is wired to accept only such objects, which is what closes the loop between market reality and the filed reserve. The candidate check itself is deep enough to warrant its own treatment in Validating Interest Rate Assumptions Against Treasury Curves.
Prerequisites
Before standing up the governance pipeline, assemble the following:
- Python 3.11+ with
numpyandpandasfor curve arithmetic,scipyfor interpolation, andpydanticfor the immutable assumption schema. - A reliable market feed for risk-free rates: the U.S. Treasury daily par yield curve, on-the-run Treasury quotes, or a swap-rate source, each retrievable with an explicit as-of settlement date rather than “latest”.
- A curve construction convention agreed in advance: par-to-spot bootstrapping method, day-count basis, compounding frequency, and the interpolation scheme (log-linear on discount factors is a common, defensible default). These are governance decisions, not implementation details.
- A version store — a database table or content-addressed object store — where approved assumptions live immutably with an effective date, an approver identity, and a hash.
- Conceptual grounding in the parent Assumption Validation & Rule Engine Design discipline, and familiarity with how the approved curve feeds Economic Scenario Mapping & Yield Curve Alignment when the deterministic base is extended into stochastic paths.
- A regulatory frame: VM-20 Section 4 (deterministic reserve discounting) and Section 7B (prescribed reinvestment rates), plus ASOP No. 22 and the general modeling controls of ASOP No. 56.
Core Implementation
The governance layer models an interest assumption as an immutable, self-describing object. It carries the spot curve, the metadata that makes it traceable, and a checksum that binds the two together. Construction is a single gated function: build the candidate, validate it against source, and only then mint the approved record. The net asset earned rate and the prescribed reinvestment grading are derived from the validated risk-free base, so the whole assumption traces back to one reconciled curve.
from __future__ import annotations
import hashlib
import json
from dataclasses import dataclass, field
from datetime import date
import numpy as np
@dataclass(frozen=True)
class InterestAssumption:
"""An approved, versioned interest assumption bound to its source."""
version: str
effective_date: date
approver: str
tenors_years: tuple[float, ...] # e.g. (0.5, 1, 2, 5, 10, 30)
spot_rates: tuple[float, ...] # annual-effective zero rates
reinvestment_ltr: float # prescribed long-term reinvestment rate
grading_years: int # years to grade portfolio -> prescribed
source_settlement_date: date
checksum: str = field(default="")
def discount_factor(self, t: float) -> float:
"""Log-linear interpolation on discount factors, then discount at t."""
years = np.asarray(self.tenors_years)
dfs = (1.0 + np.asarray(self.spot_rates)) ** (-years)
log_df = np.interp(t, years, np.log(dfs))
return float(np.exp(log_df))
def net_asset_earned_rate(self, portfolio_rate: float, year: int) -> float:
"""Grade the modeled portfolio earned rate toward the prescribed rate."""
w = min(year, self.grading_years) / self.grading_years
return (1.0 - w) * portfolio_rate + w * self.reinvestment_ltr
def _seal(payload: dict) -> str:
canonical = json.dumps(payload, sort_keys=True, default=str)
return hashlib.sha256(canonical.encode()).hexdigest()
def approve_assumption(
version: str,
effective_date: date,
approver: str,
tenors_years: tuple[float, ...],
spot_rates: tuple[float, ...],
reinvestment_ltr: float,
grading_years: int,
source_settlement_date: date,
source_par_yields: dict[float, float],
tolerance_bps: float = 2.0,
) -> InterestAssumption:
"""Gate: validate the candidate against source, then mint an approved record."""
if effective_date < source_settlement_date:
raise ValueError("effective date precedes the source settlement date")
candidate = InterestAssumption(
version=version,
effective_date=effective_date,
approver=approver,
tenors_years=tenors_years,
spot_rates=spot_rates,
reinvestment_ltr=reinvestment_ltr,
grading_years=grading_years,
source_settlement_date=source_settlement_date,
)
# Re-price each quoted par bond from the candidate spot curve and compare.
breaches: list[float] = []
for tenor, par_yield in source_par_yields.items():
implied = _implied_par_yield(candidate, tenor)
if abs(implied - par_yield) * 1e4 > tolerance_bps:
breaches.append(tenor)
if breaches:
raise ValueError(f"candidate reprices source badly at tenors {breaches}")
payload = {
"version": version,
"effective_date": effective_date,
"approver": approver,
"tenors_years": tenors_years,
"spot_rates": spot_rates,
"source_settlement_date": source_settlement_date,
}
return InterestAssumption(**{**candidate.__dict__, "checksum": _seal(payload)})
def _implied_par_yield(assumption: InterestAssumption, tenor: float) -> float:
"""Par yield a coupon bond of this tenor would carry under the spot curve."""
coupon_times = np.arange(1.0, tenor + 1e-9, 1.0)
annuity = sum(assumption.discount_factor(t) for t in coupon_times)
final_df = assumption.discount_factor(tenor)
return (1.0 - final_df) / annuity
The object is frozen, so once approved it cannot be mutated — a re-source produces a new version, never an in-place edit. The checksum seals the curve together with its effective date, approver, and settlement date, so any later tampering is detectable. The net_asset_earned_rate method implements the VM-20 grading in miniature: a modeled portfolio rate in early years blending linearly toward the prescribed long-term reinvestment rate over grading_years. Formally, for a projection year the earned rate is
where is the modeled portfolio rate, the prescribed long-term reinvestment rate, and the grading period. The reinvestment/asset-earned-rate spread — the gap between what the current portfolio earns and the prescribed forward rate — is exactly at , and governance ensures both endpoints are sourced and approved rather than assumed.
Configuration and Tuning
The two governance parameters that most affect the reserve are the validation tolerance and the grading period, and each is a policy choice with a documented rationale rather than a number to optimize.
GOVERNANCE_POLICY = {
# Repricing tolerance: how far the candidate may miss its own source.
# 2 bps absorbs quote rounding and day-count nuance without hiding a
# genuine bootstrapping error. Tighten for short tenors, loosen at 30y.
"tolerance_bps": 2.0,
"tolerance_bps_by_bucket": {"short": 1.0, "belly": 2.0, "long": 3.0},
# Prescribed grading: VM-20 Section 7B grades reinvestment toward the
# prescribed long-term rate over a fixed horizon. This is regulatory,
# not discretionary -- record the manual version it derives from.
"grading_years": 20,
"prescribed_ltr_source": "VM-20 Section 7B, 2024 Valuation Manual",
# Staleness ceiling: a curve older than this cannot back a valuation,
# forcing a re-source rather than silent reuse of last quarter's rates.
"max_source_age_days": 5,
}
Three tuning notes carry real consequence. First, the tolerance should be bucketed by tenor: a one-basis-point miss at the two-year node is a genuine bootstrapping bug, whereas the same absolute miss at thirty years is within quote noise, so a flat tolerance is either too loose at the short end or too tight at the long end. Second, the grading period and prescribed long-term rate are not yours to tune — they are read from the Valuation Manual, and the configuration’s job is to pin which version of the manual they came from, so a mid-year update is a visible, approved change. Third, the staleness ceiling is what converts “we reused last quarter’s curve by accident” from an undetectable error into a blocked run, and it should be set tight enough that a re-source is forced every cycle. Drift in the realized versus expected earned rate across cycles is then watched with the statistical machinery in Dynamic Threshold Tuning for Assumption Drift.
Step-by-Step
- Fix the settlement date first. Decide the as-of date the valuation discounts to and pull every market quote for that exact date. The settlement date is the anchor the whole assumption hangs from; a curve without one cannot be governed.
- Source the risk-free base. Retrieve Treasury par yields or swap rates for the agreed tenor set, recording the provider and retrieval timestamp alongside the quotes.
- Bootstrap par to spot. Convert the quoted par or coupon rates into zero-coupon spot rates using the agreed convention, so discounting uses true zero rates rather than par-yield approximations.
- Interpolate the projection grid. Fill the tenors the model needs but the market does not quote, using the agreed interpolation scheme on discount factors, not on rates.
- Validate against source. Reprice each quoted instrument from the candidate curve and confirm every node sits within tolerance; a breach at any tenor blocks approval.
- Layer the prescribed grading. Derive the net asset earned rate path by grading the modeled portfolio rate toward the prescribed long-term reinvestment rate over the mandated horizon.
- Seal, date, and approve. Mint the immutable assumption with its version, effective date, approver identity, and checksum, and write it to the version store.
- Wire the engine to approved-only. Configure the projection engine to load assumptions solely by version from the store, so no unapproved or ad-hoc curve can enter a filed valuation.
Validation and Testing
Governance is only real if it is tested. Three properties must hold: an approved curve reprices its source, a stale or out-of-order curve is rejected, and an approved assumption is immutable and reproducible from its checksum.
import pytest
from datetime import date
def test_approved_curve_reprices_source():
"""A well-bootstrapped curve reprices its own par quotes within tolerance."""
par = {1.0: 0.0480, 2.0: 0.0455, 5.0: 0.0430, 10.0: 0.0425, 30.0: 0.0450}
assumption = approve_assumption(
version="2026Q2.1", effective_date=date(2026, 6, 30),
approver="chief.actuary", tenors_years=(1, 2, 5, 10, 30),
spot_rates=(0.0480, 0.0454, 0.0428, 0.0421, 0.0447),
reinvestment_ltr=0.0350, grading_years=20,
source_settlement_date=date(2026, 6, 30), source_par_yields=par,
)
assert assumption.checksum # sealed
assert assumption.discount_factor(10.0) < assumption.discount_factor(1.0)
def test_effective_date_before_source_is_rejected():
"""A curve dated before its own market data cannot be approved."""
with pytest.raises(ValueError):
approve_assumption(
version="bad.1", effective_date=date(2026, 6, 29),
approver="chief.actuary", tenors_years=(1, 5),
spot_rates=(0.048, 0.043), reinvestment_ltr=0.035,
grading_years=20, source_settlement_date=date(2026, 6, 30),
source_par_yields={1.0: 0.048, 5.0: 0.043},
)
def test_grading_reaches_prescribed_rate():
"""After the grading horizon the earned rate equals the prescribed rate."""
a = InterestAssumption(
version="v", effective_date=date(2026, 6, 30), approver="a",
tenors_years=(1, 30), spot_rates=(0.048, 0.045),
reinvestment_ltr=0.035, grading_years=20,
source_settlement_date=date(2026, 6, 30),
)
assert a.net_asset_earned_rate(0.052, year=25) == pytest.approx(0.035)
Beyond unit tests, reconcile the approved curve against an independent recomputation of the deterministic reserve: discount a fixed set of test cash flows under the sealed curve and confirm the present value matches the projection engine’s output to the cent. Any divergence means the engine is discounting at a curve other than the one that was approved — the exact condition governance exists to detect.
Failure Modes and Gotchas
- Stale curve reuse. The most common failure is silently backing a valuation with last cycle’s rates because the re-source step was skipped. The staleness ceiling and a mandatory settlement-date check are the defense; never let the engine default to the most recent curve on disk.
- Par-versus-spot confusion. Discounting cash flows at par yields instead of bootstrapped spot rates systematically mis-states the reserve, most visibly on steep curves. Bootstrap first, always, and validate the spot curve by repricing the par quotes it came from.
- Interpolation on rates instead of discount factors. Linearly interpolating annualized rates between quoted tenors introduces small forward-rate kinks that compound over long durations. Interpolate on log discount factors so implied forwards stay smooth.
- Settlement-date drift between legs. Sourcing Treasury quotes as of one date and swap spreads as of another produces a curve that reconciles to neither. Stamp every input with its settlement date and reject a build that mixes them.
- Unapproved overrides. A curve hand-edited after approval but before the run breaks the checksum-to-reserve chain. Wire the engine to load by version and verify the checksum on load, so any post-approval edit is caught rather than trusted.
- Grading-period ambiguity. Reading the prescribed long-term rate or grading horizon from an outdated Valuation Manual silently biases the reinvestment assumption. Pin the manual version in configuration and treat a mid-year update as an approved, versioned change.
Frequently Asked Questions
What is the difference between the net asset earned rate and the prescribed reinvestment rate?
The net asset earned rate is the projected rate the assets backing a block earn net of defaults and expenses, and it is used to discount the deterministic reserve cash flows. The prescribed reinvestment rate is the rate VM-20 Section 7B mandates for reinvesting future positive cash flows, and the earned rate grades toward it over the prescribed horizon so no carrier can assume an optimistic forward curve.
Why must an interest assumption be immutable once approved?
Immutability is what makes the reserve reproducible. If an approved curve could be edited in place, the checksum sealed at approval would no longer match the curve the engine consumed, and an examiner could not confirm which rates produced the filed number. A re-source therefore mints a new version rather than mutating the existing one.
How tight should the validation tolerance be?
Tolerance should be bucketed by tenor rather than flat. A single basis point at the short end usually signals a genuine bootstrapping error, whereas the same absolute miss at thirty years sits within normal quote noise, so a common pattern is roughly one basis point short, two in the belly, and three at the long end.
What source data underpins a governed interest curve?
A governed curve is built from observable risk-free instruments stamped with a settlement date: U.S. Treasury par yields, on-the-run Treasury prices, or swap rates for the agreed tenor set. Each quote is recorded with its provider and retrieval time so the curve can be reconstructed and reconciled to the market it claims to represent.
Related
- Validating Interest Rate Assumptions Against Treasury Curves — the focused check that gates a candidate curve against its source.
- Economic Scenario Mapping & Yield Curve Alignment — extending the approved deterministic base into aligned stochastic paths.
- Dynamic Threshold Tuning for Assumption Drift — monitoring realized-versus-expected earned-rate drift across cycles.
- IFRS 17 Discount Rate Unlocking — the parallel discipline of re-measuring liabilities as discount rates move under IFRS 17.
- Assumption Validation & Rule Engine Design — the governing discipline these interest controls sit within.
Up a level: Assumption Validation & Rule Engine Design
Regulatory references: NAIC Valuation Manual VM-20 (Requirements for Principle-Based Reserves for Life Products), Sections 4 and 7B; Actuarial Standards Board ASOP No. 22 and ASOP No. 56.