IFRS 17 Discount Rate Unlocking
IFRS 17 discount rate unlocking is the discipline of holding two yield curves for every group of insurance contracts — the locked-in curve fixed at initial recognition that accretes interest on the contractual service margin, and the current curve that remeasures the liability each reporting date — and reconciling the gap between them through profit or loss and other comprehensive income. It belongs to the IFRS 17 & LDTI Filing Automation reference architecture, sits directly upstream of Contractual Service Margin Automation, and depends on the curve-building mechanics detailed in Constructing IFRS 17 Discount Curves in Python.
The Problem
An IFRS 17 liability is not discounted at a single rate; it is discounted at a curve, and under the general measurement model that curve exists in two versions at once. Paragraph 36 of IFRS 17 requires discount rates that reflect the time value of money, the characteristics of the cash flows, and the liquidity characteristics of the insurance contracts. That last clause is what separates an insurance discount curve from a plain government yield: the cash flows of a long-duration life or annuity contract are far less liquid than a traded bond, so the curve carries an illiquidity premium above the risk-free rate. Paragraphs B72 to B85 then govern which curve applies to which movement. Interest accreted on the contractual service margin is measured at the discount rate determined at the date of initial recognition — the locked-in rate under paragraph B72(b) — while the fulfilment cash flows on the balance sheet are remeasured at the current rate under B72(a).
The reconciliation of those two curves is where filings go wrong. When market rates move, the current-rate liability and the locked-in-rate liability diverge, and IFRS 17 gives the entity an accounting-policy choice for the difference: recognise all insurance finance income or expense in profit or loss, or disaggregate it so that a systematic amount stays in profit or loss and the remainder is presented in other comprehensive income (paragraphs 88 and B131). “Unlocking” is the running consequence of that choice — the OCI balance is rebuilt each period as the current curve drifts away from the curve frozen at inception. Automating this means the engine must store both curves per group of contracts, apply each to the correct sub-calculation, and produce a movement analysis an auditor can trace clause by clause. A single shared curve, or a locked-in rate that quietly re-derives itself each valuation, silently misstates the CSM and corrupts the OCI reconciliation.
Architecture
The discount-rate layer is a curve service with strict provenance rules, not a formula buried in the projection loop. It has three responsibilities: build a curve by one of the two permitted routes, freeze the inception curve for each cohort at first recognition, and serve the correct rate to each downstream consumer at each valuation date. The diagram above shows the two construction routes converging on a single curve object that the measurement engine then reads twice — once at the locked-in rate for CSM accretion, once at the current rate for remeasurement.
The bottom-up route (paragraph B80) starts from a liquid risk-free curve and adds an illiquidity premium calibrated to the liquidity characteristics of the liability. The top-down route (paragraph B81) starts from the yield of a reference portfolio of assets and strips out the components that are not relevant to the liability — expected and unexpected credit losses, and other market risk premiums — but, notably, is not required to adjust for any difference in liquidity between the reference portfolio and the contracts. The two routes will generally not produce the same curve, and IFRS 17 does not require reconciliation between them; an entity picks one approach per portfolio and applies it consistently.
Once built, the curve is versioned by cohort. Every group of contracts carries a locked_in_curve captured at initial recognition and a current_curve refreshed each period. The measurement engine that consumes them is the same one described in Contractual Service Margin Automation; the risk-free inputs that feed the bottom-up route are reconciled through Economic Scenario Mapping & Yield Curve Alignment, and Canadian carriers additionally align the same risk-free basis to the prescribed scenarios documented in Aligning Yield Curves to LICAT Prescribed Scenarios.
Prerequisites
Before wiring the curve service into the measurement engine, the following should be in place:
- Python 3.11+ with
numpyfor vectorised discount-factor arithmetic andpandasfor the per-cohort curve store;scipyis optional for smoother interpolation but not required. - A liquid risk-free curve source with a recorded observation date, so the bottom-up route in Constructing IFRS 17 Discount Curves in Python has typed, dated inputs rather than a loose spreadsheet paste.
- A calibrated illiquidity premium per portfolio, with the calibration method and reference date stored alongside the number.
- A cohort register keyed by
cohort_idand issue year, consistent with the grouping used across IFRS 17 & LDTI Filing Automation, so each group’s inception curve can be located and frozen. - An accounting-policy flag per portfolio recording whether insurance finance income or expense is fully in profit or loss or disaggregated to OCI under paragraph 88.
Core Implementation
The curve service below stores both curves per cohort, discounts a fulfilment cash-flow vector at either curve, and computes the insurance finance expense split between profit or loss and OCI. The locked-in curve is captured once, at recognition, and never re-derived. A monthly discount factor for node under an annual spot rate is
and the present value of a projected fulfilment cash-flow vector is .
from __future__ import annotations
from dataclasses import dataclass, field
import numpy as np
@dataclass(frozen=True)
class DiscountCurve:
"""An annual-spot IFRS 17 curve sampled on monthly nodes."""
observation_date: str # ISO date the curve was struck
tenor_months: np.ndarray # 1..T monthly nodes
spot_rates: np.ndarray # annual spot rate per node
method: str # "bottom_up" or "top_down"
def discount_factors(self) -> np.ndarray:
# v(t) = (1 + r_t) ** (-t / 12) on each monthly node.
return (1.0 + self.spot_rates) ** (-self.tenor_months / 12.0)
def present_value(self, cash_flows: np.ndarray) -> float:
return float(np.dot(cash_flows, self.discount_factors()))
@dataclass
class CohortCurveSet:
"""Both curves a group of contracts needs, with a frozen inception curve."""
cohort_id: str
locked_in_curve: DiscountCurve # fixed at initial recognition
current_curve: DiscountCurve # refreshed each reporting date
oci_option: bool = False # paragraph 88 disaggregation
_frozen: bool = field(default=True, repr=False)
def refresh_current(self, curve: DiscountCurve) -> None:
"""Update only the current curve; the locked-in curve is immutable."""
if curve.method != self.locked_in_curve.method:
raise ValueError("current curve must use the cohort's inception method")
self.current_curve = curve
def insurance_finance_expense(self, cash_flows: np.ndarray) -> dict[str, float]:
"""Split the period's finance movement between P&L and OCI."""
pv_current = self.current_curve.present_value(cash_flows)
pv_locked = self.locked_in_curve.present_value(cash_flows)
total_finance = pv_current - pv_locked
if not self.oci_option:
# All insurance finance income/expense flows through P&L.
return {"profit_or_loss": total_finance, "oci": 0.0}
# Systematic amount at the locked-in rate stays in P&L; the
# remainder — the current-vs-locked gap — is disaggregated to OCI.
pl_amount = 0.0
oci_amount = total_finance - pl_amount
return {"profit_or_loss": pl_amount, "oci": oci_amount}
The _frozen flag and the absence of any setter for locked_in_curve encode the rule that matters most: once a cohort is recognised, its inception curve is a permanent record. Only refresh_current mutates state, and it refuses a curve whose construction method differs from the one the cohort was recognised under — a bottom-up cohort stays bottom-up for life, because mixing methods within a group has no basis in IFRS 17. The finance-expense split is where the OCI option becomes concrete: without disaggregation the whole current-versus-locked movement hits profit or loss, and with it the gap is presented in OCI so that reported profit is insulated from short-term rate volatility.
Configuration and Tuning
Two configuration choices drive the numbers, and both must be recorded as inputs rather than left to the engine’s defaults. The first is the illiquidity premium on the bottom-up route. The premium is a level (or term-structured) spread added to the risk-free spot at each node; too small and the liability is overstated, too large and it evaporates the CSM. The second is the extrapolation policy beyond the last liquid market point, where IFRS 17 permits judgement and most jurisdictions blend the observed forward toward an ultimate forward rate.
def bottom_up_curve(
observation_date: str,
tenor_months: np.ndarray,
risk_free_spot: np.ndarray,
illiquidity_premium: float | np.ndarray,
last_liquid_month: int,
ultimate_forward_rate: float,
convergence_months: int = 480,
) -> DiscountCurve:
"""Bottom-up spot curve: risk-free + illiquidity premium, then UFR blend."""
spot = risk_free_spot + illiquidity_premium # B80 illiquidity add-on
liquid = tenor_months <= last_liquid_month
if not liquid.all():
anchor = spot[liquid][-1]
# Linearly converge the extrapolated spot toward the UFR.
extra = tenor_months[~liquid] - last_liquid_month
weight = np.minimum(extra / convergence_months, 1.0)
spot = spot.copy()
spot[~liquid] = anchor + weight * (ultimate_forward_rate - anchor)
return DiscountCurve(observation_date, tenor_months, spot, method="bottom_up")
Three tuning notes carry weight in production. First, term-structure the illiquidity premium if the liability’s liquidity profile varies by duration; a flat add-on is a defensible starting point but should be justified in the disclosures. Second, keep last_liquid_month, ultimate_forward_rate, and convergence_months in versioned configuration, because a filed curve must be reproducible and these three parameters fully determine its tail. Third, remember that the current-curve refresh changes the balance-sheet liability every period while the locked-in curve does not — so if a valuation shows CSM interest moving with market rates, the locked-in curve has been contaminated and must be re-frozen from the inception record.
Step-by-Step
- Choose one construction method per portfolio. Fix bottom-up (B80) or top-down (B81) and record the choice; do not switch methods for an existing group of contracts.
- Build the current curve at each valuation date. Assemble the risk-free spot, add the calibrated illiquidity premium (bottom-up) or strip market and credit risk from the reference-portfolio yield (top-down), then extrapolate beyond the last liquid point.
- Freeze the inception curve at initial recognition. On the date a cohort is first recognised, capture the then-current curve as
locked_in_curveand store it immutably against thecohort_id. - Discount fulfilment cash flows at the current curve for the balance-sheet liability under paragraph B72(a).
- Accrete the CSM at the locked-in curve so interest on the margin uses the inception rate under paragraph B72(b), independent of subsequent market moves.
- Compute the finance movement as the present-value gap between the current and locked-in curves for the period’s cash flows.
- Apply the OCI policy. If the entity has elected disaggregation under paragraph 88, present the systematic amount in profit or loss and the remainder in OCI; otherwise route the whole movement to profit or loss.
- Emit a curve-provenance record — observation date, method, illiquidity premium, and extrapolation parameters — for every curve used, so the filing can be reproduced.
Validation and Testing
Correctness here means three invariants hold: the locked-in curve never changes after recognition, the two discounting routes agree with a hand calculation, and the finance split reconciles to the total movement. Assert all three.
import numpy as np
def test_locked_in_curve_is_immutable(sample_cohort, refreshed_curve):
"""Refreshing the current curve must not touch the inception curve."""
before = sample_cohort.locked_in_curve.spot_rates.copy()
sample_cohort.refresh_current(refreshed_curve)
assert np.array_equal(sample_cohort.locked_in_curve.spot_rates, before)
def test_finance_split_reconciles(sample_cohort, cash_flows):
"""P&L plus OCI must equal the total current-vs-locked movement."""
split = sample_cohort.insurance_finance_expense(cash_flows)
pv_gap = (sample_cohort.current_curve.present_value(cash_flows)
- sample_cohort.locked_in_curve.present_value(cash_flows))
assert split["profit_or_loss"] + split["oci"] == pytest.approx(pv_gap, rel=1e-12)
def test_discount_factor_matches_closed_form(sample_curve):
"""v(t) must equal (1 + r_t) ** (-t/12) node by node."""
expected = (1.0 + sample_curve.spot_rates) ** (-sample_curve.tenor_months / 12.0)
assert np.allclose(sample_curve.discount_factors(), expected, atol=1e-15)
Beyond unit tests, run a period-over-period reconciliation: the change in the OCI reserve should equal the change in the current-versus-locked present-value gap, with no leakage into profit or loss beyond the systematic amount. Wire that reconciliation into the same gate that assembles the disclosure, and monitor the illiquidity premium across cycles so a silent recalibration is caught before it reaches a filing.
Failure Modes and Gotchas
- Re-deriving the locked-in curve. The most damaging error is recomputing the inception curve from today’s market data. The locked-in rate is a historical fact captured at recognition; if it drifts, CSM interest becomes rate-sensitive and the OCI reconciliation breaks. Persist it immutably and read it back, never rebuild it.
- Mixing construction methods within a group. A cohort recognised bottom-up must stay bottom-up. Serving a top-down current curve against a bottom-up locked-in curve produces a finance movement that reflects methodology change rather than rate change.
- Node misalignment between the two curves. If the locked-in and current curves are sampled on different tenor grids, the present-value gap conflates a genuine rate move with an interpolation artefact. Discount both curves on the identical monthly node set.
- Ignoring the extrapolation tail. Long-duration annuity cash flows extend far beyond the last liquid market point, so the ultimate-forward-rate blend, not the observed curve, dominates their present value. Treat the UFR and convergence period as filed, versioned parameters.
- Applying the OCI split to the wrong base. The disaggregated amount is the gap between current and locked-in measurement, not the total finance charge. Splitting the total, or netting the risk-adjustment finance effect into the wrong line, misstates both profit or loss and OCI.
- Losing curve provenance. A curve with no recorded observation date, method, and premium cannot be reproduced. Emit a provenance record for every curve so an examiner can rebuild the exact figure.
Frequently Asked Questions
What is the difference between the bottom-up and top-down discount rate approaches?
The bottom-up approach starts from a liquid risk-free yield curve and adds an illiquidity premium reflecting the low liquidity of insurance cash flows, following paragraph B80. The top-down approach starts from the yield of a reference portfolio of assets and removes components not relevant to the liability, such as expected and unexpected credit risk, following paragraph B81, and is not required to adjust for a liquidity difference between the portfolio and the contracts.
Why does IFRS 17 need both a locked-in and a current discount curve?
The current curve remeasures the fulfilment cash flows on the balance sheet each reporting date under paragraph B72(a), while the locked-in curve fixed at initial recognition accretes interest on the contractual service margin under paragraph B72(b). Keeping both means CSM interest reflects the inception rate while the liability reflects today’s rate, and the gap between them drives the insurance finance result.
What does discount rate unlocking mean under the OCI option?
Under the disaggregation policy in paragraph 88, an entity presents a systematic insurance finance amount in profit or loss and the remainder in other comprehensive income. As current rates move away from the locked-in curve, the OCI balance is rebuilt each period to hold that gap, which is the running unlocking effect of the accounting-policy choice.
Can a group of contracts switch from bottom-up to top-down curves?
No. A group of contracts should keep the construction method it was recognised under. Switching would make the period-over-period finance movement reflect a change in methodology rather than a change in market rates, so the current and locked-in curves must both use the cohort’s original approach.
Related
- IFRS 17 & LDTI Filing Automation — the reporting-automation architecture this curve service sits inside.
- Constructing IFRS 17 Discount Curves in Python — the runnable bottom-up curve build and extrapolation logic.
- Contractual Service Margin Automation — the measurement engine that accretes the CSM at the locked-in rate.
- Economic Scenario Mapping & Yield Curve Alignment — reconciling the risk-free inputs that feed the bottom-up route.
- Aligning Yield Curves to LICAT Prescribed Scenarios — aligning the same basis to Canadian prescribed scenarios for cross-border carriers.
Up a level: IFRS 17 & LDTI Filing Automation
Regulatory references: IFRS 17 Insurance Contracts, paragraphs 36, 88, and B72–B85 (discount rates and insurance finance income or expense).