The IFRS 17 formulas

This page explains how the IFRS 17 engine turns a nominal cash-flow projection into a balance, and then explains how that balance moved. Every formula is quoted as the code writes it. The source path is named beside each formula, so you can check it.

We start with one worked example and follow it step by step. The general rules, the data model and a reference table come after.

For the methodology behind these formulas — ported from the documentation of the open-source IFRS 17 Calculation Engine, with what this implementation does and does not do yet — see The IFRS 17 methodology.

Where the inputs come from. The engine does not read the Reinsurance cash-flow cube. It reads its own input type, RawVariable: one row per group of contracts, AoC step, novelty, estimate type and amount type, holding a monthly array of nominal amounts. Figures it has already computed are stored as IfrsVariable. The queries are listed in Ifrs17/Engine/Source/Ifrs17DataSource.cs. They read the engine hub's own partition, then Ifrs17 and ReinsuranceDemo. An installed copy at alice/Ifrs17/Engine reads alice, Ifrs17 and ReinsuranceDemo (EngineTests.Queries_AreAnchoredAndNameTheirPartitions), and never any other partition. The demo's MTPL incurred-claims book (ReinsuranceDemo/Ifrs17/...) is built from the demo's claims register, but that happens when the data files are generated (ReinsuranceDemo/SampleData/Source/Ifrs17Spine.cs, OutstandingOnOurBook()), not at run time. For the cash-flow cube the other modules share, see How the cash-flow model works. For how the modules fit together, see the suite overview.

The worked example: a Swiss endowment, one year

The movement report uses one fixture book, and the end-to-end tests check that same book (Ifrs17/Report/Test/EndToEndFixture.cs). Its Swiss group, Demo/Groups/EndowmentCH, is set up as follows:

Input Value
Claims (NIC, best estimate BE) −120 a year for ten years, on the monthly grid: 120 entries of −10
Steps supplied BOP/I (opening, in force) and CL/C (combined control step), both carrying the same array
Valuation approach BBA, liability for remaining coverage (LRC)
Locked-in curve (2025-12) 2.0 % flat
Closing curve (2026-12) 1.0 % flat
Reporting period 2026-12, one projection period: Shift = 0, TimeStep = 12
Currency CHF throughout, so nothing is translated

Here is the live report computed from that fixture. The Swiss table is the one we rebuild by hand below.

Subscribers see this result live; below is the same worked example as a static table.

Swiss group EndowmentCH, best-estimate claims, CHF, reporting period 2026-12. The live report also shows the fixture's other groups; only the Swiss one is reproduced here. Steps 3–5 below derive each value by hand.

AoC step Value
Opening balance (BOP / I) 1,087.76
Cash flow (CF / I) −120.00
Interest accretion (IA / I) +20.66
Yield-curve update (YCU / I) 0.00
Combined control (CL / C) 0.00
Closing balance (EOP / C) 988.41

The movement report runs on the test fixture, not on the installed book. Its node says so: "It is not yet wired to the installed book" (Ifrs17/MovementReport.json).

Step 1 — the measurement basis

Ifrs17Reporting.Measure takes the economic basis from the group's valuation approach (Ifrs17/Engine/Source/Identities.cs):

(ValuationApproaches.BBA, _) => EconomicBases.L,
(ValuationApproaches.VFA, _) => EconomicBases.C,
(ValuationApproaches.PAA, LiabilityTypes.LIC) => EconomicBases.C,
_ => EconomicBases.N,

The group is BBA, so every step is valued on the locked-in basis L. On that basis the curve is the one observed when the group was recognised (data.Year, data.Month = 2025-12), so it is 2.0 % (ImportStorage.GetYearlyYieldCurve).

Step 2 — annual rate to monthly factors

MonthlyRate (Ifrs17/Engine/Source/PresentValue.cs):

double[] Interest => YearlyYieldCurve.Select(rate => Math.Pow(1d + rate, 1d / Ifrs17Constants.MonthInAYear)).ToArray();
double[] Discount => Interest.Select(x => Math.Pow(x, -1)).ToArray();

With rate 0.02:

The code takes the twelfth root of the annual rate. It does not divide the rate by twelve, so compounding is preserved.

Step 3 — discount and cumulate into a reserve

DiscountedCashflow.Values (PresentValue.cs) calls ComputeDiscountAndCumulate (Ifrs17/Source/Arithmetics.cs) and flips the sign:

double[] Values => Arithmetics.Multiply(-1d, NominalValues.ComputeDiscountAndCumulate(MonthlyDiscounting, PeriodType));

// PeriodType.EndOfPeriod (claims, here):
for (var i = nominalValues.Length - 1; i >= 0; i--)
    result[i] = (nominalValues[i] + result.GetValidElement(i + 1)) * monthlyDiscounting.GetValidElement(i / 12);

The recursion runs backwards from the last month. Each entry result[i] is the value at the start of month i of every payment from month i onwards. The sign flip turns a cash outflow (−) into a liability (+). The resulting reserve at each month:

Month i Reserve R[i]
0 1,087.76
1 1,079.55
11 996.77
12 988.41

Step 4 — read each step at the point its configuration names

PresentValue.GetValueFromValues (PresentValue.cs) reads the value array at the point the step's configured ValuationPeriod names:

ValuationPeriod.BeginningOfPeriod => values.ElementAtOrDefault(shift),
ValuationPeriod.MidOfPeriod      => values.ElementAtOrDefault(shift + Convert.ToInt32(Math.Round(timeStep / 2d, MidpointRounding.AwayFromZero)) - 1),
ValuationPeriod.Delta            => values.Skip(shift).Take(timeStep).Sum(),
ValuationPeriod.EndOfPeriod      => values.ElementAtOrDefault(shift + timeStep),

Each step then works out as follows:

Step Rule that applies Formula Value
BOP / I opening PresentValueFromDiscountedCashflow R[shift] = R[0] 1,087.76
CF / I cash flow CashflowAocStep nominal values, read as Delta: sum of months 0–11 = 12 × −10 −120.00
IA / I interest accretion PresentValueWithInterestAccretion Σ over months 0–11 of R[i] × (1.00165158 − 1) +20.66
YCU / I yield-curve update TelescopicDifference R_YCU[12] − R_BOP[12]; both on the locked-in 2 % curve 0.00
CL / C combined control TelescopicDifference R_CL[12] − R_YCU[12]; same claims, same curve 0.00
EOP / C closing PresentValueFromDiscountedCashflow R[shift + timeStep] = R[12], read from CL/C 988.41

The interest-accretion line is InterestAccretion.Values for an end-of-period amount type:

result[i] = -1d * discounted[i] * (interestFactors.GetValidElement(i / Ifrs17Constants.MonthInAYear) - 1d);

Here discounted is the reserve with its sign flipped back. The result is the interest earned on the balance in each month.

Step 5 — the reconciliation

opening   1,087.76
cash flow  −120.00
interest    +20.66
YCU, CL       0.00
         ---------
explained   988.41  =  valued closing balance 988.41

The two sides agree exactly; the gap is 3 × 10⁻¹³. They agree because of how the reserve recursion is built, not because anything was adjusted. Each month the reserve moves by R[i+1] = R[i] × f − c, where f is the monthly interest factor and c is that month's payment. Summed over the twelve months:

Σ R[i] × (f − 1)  =  R[12] − R[0] + 12 × 10

So the interest-accretion line is exactly the closing balance, less the opening balance, plus the cash that was paid.

The live report shows 1,087.8 − 120.0 + 20.7 = 988.5, then a Rounding −0.1 line, then a closing balance of 988.4. That rounding line comes from summing figures already rounded to one decimal. The unrounded figures above agree exactly.

What if the curve were current, not locked in? On the current basis C, the yield-curve-update step reads the closing 1.0 % curve, while its parent BOP/I reads the opening 2.0 % curve (YcPeriod EndOfPeriod versus BeginningOfPeriod). The step would then be R_1%[12] − R_2%[12] = 1,032.63 − 988.41 = +44.21: rates fell, so the liability rose. The fixture is BBA and therefore locked-in, which is why the report shows 0.0 on that line. The +44.21 is computed with the same formulas but is not a figure the module displays. EngineTests.YieldCurveUpdate_MeasuresTheCurveMove tests the same mechanism in the other direction: rates rise from 2 % to 4 %, and the step must come out negative.

Step 6 — the German book: translation and the FX step

The German group in the same fixture pays −120 a year for ten years in USD. Its entity measures in EUR and the group reports in CHF. The USD rate moves from 0.90 (spot, 2025-12) to 1.00 (spot, 2026-12), with an average of 0.95. The EUR rate is flat at 1.00. Fx.Rate (Ifrs17/Report/Source/Fx.cs):

double Rate => FunctionalRate * GroupRate;
// FunctionalRate: BeginningOfPeriod → previous-period Spot, Average → Average, otherwise → current Spot
// GroupRate: functional → group at the Average rate (only when CurrencyType.Group)

Each step is translated at its own rate: the opening balance at 0.90, cash flow and interest accretion at 0.95, the closing balance at 1.00. Translated that way, the steps no longer add up to the closing balance. FxData.FxMovement names the gap instead of hiding it:

closing.Minus(movements).AggregateOver(Novelty, VariableType) → VariableType = FX, Novelty = C

The live report gives: opening 912.5, CF −114.0, IA +31.9, FX +97.1, closing 927.5. EndToEndTests.GermanBook_ReconcilesOnlyBecauseOfTheFxStep asserts that the FX step is non-zero and that the report reconciles. The consolidated group sums each entity after translation (ReportingNodeRollup). The group total is 2,000.3 → 1,915.9.

The CSM and the loss component — worked on the pinned numbers

The movement report above shows best-estimate present values only. Ifrs17Reporting.Measure evaluates PresentValue and nothing else. The margin scopes are tested on their own, in Ifrs17/Engine/Test/TechnicalMarginTests.cs, with these pinned numbers.

Sign convention. The technical margin is positive when the group is onerous:

string Classify(double aggregatedTechnicalMargin) =>
    aggregatedTechnicalMargin > Ifrs17Constants.Precision ? EstimateTypes.L : EstimateTypes.C;

Here Precision = 1E-5. A negative margin means profit not yet earned, which is the CSM.

The case: a group has accumulated a margin of −30, so it carries 30 of unearned profit. A step then adds +50: the cash flows deteriorate by 50. AllocateTechnicalMargin.Value (Ifrs17/Engine/Source/TechnicalMargin.cs):

double Value => (HasSwitch, EstimateType == ComputedEstimateType) switch
{
    (true, true)  => StepMargin + AggregatedMargin,
    (true, false) => -1d * AggregatedMargin,
    (false, true) => StepMargin,
    _ => 0d,
};
  1. After the step the margin is −30 + 50 = +20 > 1E-5. The group is now classified L, while before the step it was C, so HasSwitch is true.
  2. Asked for L (the loss component): (true, true) gives 50 + (−30) = +20.
  3. Asked for C: (true, false) gives −1 × (−30) = +30.
  4. ContractualServiceMargin.Value = -1d * GetScope<AllocateTechnicalMargin>(Identity, EstimateTypes.C).Value = −30. The step releases the whole CSM of 30.
  5. LossComponent.Value = GetScope<AllocateTechnicalMargin>(Identity, EstimateTypes.L).Value = +20. A loss of 20 is recognised.

The two allocations add up to exactly the step: 20 + 30 = 50. Switch_ConservesTheStep checks this for four (accumulated margin, step) pairs. The group never ends up holding 30 of profit and 20 of loss at the same time.

What the margin is measured from. TechnicalMargin.Value:

double Value =>
    GetScope<TechnicalMarginAmountType>((Identity, EstimateType)).Values
        .Sum(amountType => GetScope<PresentValue>(new ValuationIdentity(Identity, amountType, EstimateTypes.BE, null), EconomicBasis).Value)
    + GetScope<PresentValue>(new ValuationIdentity(Identity, string.Empty, EstimateTypes.RA, null), EconomicBasis).Value;

So the margin is the best estimate summed over the service amount types, plus the risk adjustment. TechnicalMarginAmountType leaves out coverage units, non-attributable expenses and CDR. For loss recovery (LR) it also leaves out attributable expenses. Under PAA it also leaves out premiums and deferrable expenses. The basis is L (locked-in), or C under VFA (TechnicalMarginForCurrentBasis).

The risk adjustment is an input, not a model. The engine values the RA estimate type through the same PresentValue chain as any other cash flow. No scope here computes a risk adjustment (no cost of capital, no confidence level). It has to arrive as RawVariable rows with EstimateType = RA.

The general rule

A step's value is its own valuation minus its parent's. That one line, TelescopicDifference.Values = CurrentValues − PreviousValues, is the analysis of change.

ParentAocStep chains each step to the one before it, in the order the configuration gives. Adding up the whole chain therefore collapses to the last valuation minus the first, so the report reconciles by construction. Steps are data: each AocConfiguration row gives a step's order, data type, yield-curve period, FX period and valuation period. To add a step, you add a row.

Four kinds of step are not themselves telescopic differences:

Only the last three are Calculated in the configuration (EOP/C, CF/I, IA/I in EndToEndFixture.Configuration); the opening balance BOP/I is Optional. ParentAocStep drops Calculated steps from the parent candidates, but keeps the opening balance. So the yield-curve update telescopes against the opening balance, not against interest accretion (EngineTests.StepGraph_TelescopesAgainstTheLastValuation).

The data model

Prerequisite: the class diagrams on this page are embedded from the Data Modelling plugin's explorer (/DataModelling/Explorer), which Ifrs17 does not require. On an installation without it they render nothing; each type's own page under /Ifrs17 still lists its properties.

A group of contracts, the types it references, and the inputs that point at it:

Check yourself

Q1. In the Swiss example, the closing curve is 1.0 % and the opening curve is 2.0 %. Why does the yield-curve-update line show 0.0 rather than about +44?

A. The group is BBA, so GetDefaultEconomicBasisDriver measures it on the locked-in basis L. GetYearlyYieldCurve then returns the curve from the recognition date (2.0 %) for every step, including the yield-curve update. A step and its parent valued on the same curve differ by nothing. The +44.21 shows up only on the current basis C, which a VFA group or a PAA incurred-claims group would use.

Q2. A profitable group carries an accumulated technical margin of −100, and a step adds −20. What do ContractualServiceMargin and LossComponent report for that step?

A. −100 − 20 = −120, still classified C, so there is no switch. Allocate(C) = StepMargin = −20, so the CSM is −1 × −20 = +20: the CSM grows by 20. Allocate(L) = 0 (TechnicalMarginTests.Profitable_CarriesMarginAndNoLoss).

Q3. Why does the cash-flow step use Delta, while the opening balance reads a single month?

A. A cash flow is a flow: money that moved over the period. It is the sum of the months the period spans. A balance is a stock: the reserve at one point in time. The two steps share the reading helper (GetValueFromValues), not the array it reads: CashflowAocStep sums the nominal array (NominalCashflow.Values, 12 × −10 = −120), while PresentValueFromDiscountedCashflow reads one month of the discounted reserve (DiscountedCashflow.Values, R[0] = 1,087.76).

What this module does not (yet) do

These are stated plainly so nobody has to rediscover them:

Reference — the scopes

Measurement (Ifrs17/Engine/Source/PresentValue.cs)

Scope Identity Formula (as written)
MonthlyRate ImportIdentity Interest = (1 + rate)^(1/12) per curve year; Discount = Interest^-1; basis N ⇒ curve {0}
NominalCashflow ValuationIdentity Σ over ReferenceAocStep of GetValues(step, amountType, estimateType, accidentYear)
AllClaimsCashflow ″ the same, summed over every claims amount type (applies to credit-risk amount types)
CreditDefaultRiskNominalCashflow ″ PvCreditDefaultDecumulated − NominalClaimsCashflow; backward result[i] = e^(−rate)·result[i+1] + claims[i] − claims[i+1]
DiscountedCashflow ″ −1 × ComputeDiscountAndCumulate(nominal, discount, periodType)
DiscountedCreditRiskCashflow ″ −1 × ComputeDiscountAndCumulateWithCreditDefaultRisk(nominal, discount, nonPerformanceRiskRate), for reinsurance held only
TelescopicDifference ″ DiscountedCashflow(this) − Σ DiscountedCashflow(parent steps)
InterestAccretion ″ end of period: −discounted[i] × (f − 1); beginning of period: −(discounted[i] − nominal[i]) × (f − 1)
PresentValue ″ Value = GetValueFromValues(Values); Values = TelescopicDifference unless a rule below applies
PresentValueFromDiscountedCashflow ″ BOP (novelty ≠ C) and EOP: DiscountedCashflow.Values
CashflowAocStep ″ CF: NominalCashflow.Values, read as Delta
PresentValueWithInterestAccretion ″ IA: InterestAccretion.Values
EmptyValuesAocStep ″ EA/C, AM/C, RCU/I, and CRU on a non-CDR amount type: 0
ComputePresentValueWithIfrsVariable ″ not a cash-flow import: read back the stored IfrsVariable

Step structure (Ifrs17/Engine/Source/AocStructure.cs)

Scope What it returns
PreviousAocSteps the steps with a lower Order, of the same novelty (all novelties for C)
ParentAocStep the last non-Calculated previous step, excluding CRU unless the amount type is CDR (for a CDR valuation the credit-risk update stays an eligible parent); none for BOP or calculated steps; the last step of each novelty for the first combined step
ReferenceAocStep / …ForProjections which step's data a calculated step reads: EA ← CF/C, AM ← CL/C, EOP ← CL/C, otherwise the last supplied step; in projection years, in-force steps read CL/C
GetIdentities / GetCashflowIdentities the steps to compute: every configured step, or (for cash-flow imports) the supplied steps plus the derived ones for novelties that have data

Technical margin (Ifrs17/Engine/Source/TechnicalMargin.cs)

Scope Formula (as written)
TechnicalMarginAmountType the group's amount types, minus CU, non-attributable expenses and CDR (minus attributable expenses for LR; minus premiums and deferrables under PAA)
TechnicalMargin Σ PV(BE, amountType) + PV(RA) on basis L; AggregatedValue = Σ TechnicalMargin(previous steps)
TechnicalMarginForCurrentBasis VFA: basis C
TechnicalMarginForOpeningBalance BOP/I: −stored(C) + stored(L)
TechnicalMarginIsZero CF: 0
TechnicalMarginForInterestAccretion IA: AggregatedValue × (Π monthly factors over the period − 1)
TechnicalMarginForClosingBalance EOP: AggregatedValue
AllocateTechnicalMargin the four-case switch above; Classify(m) = m > 1E-5 ? L : C; opening balances never switch; closing balance = Σ allocations of the previous steps
ContractualServiceMargin −TechnicalMargin(C) for reinsurance held, else −AllocateTechnicalMargin(C)
LossComponent AllocateTechnicalMargin(L)

Reporting (Ifrs17/Report/Source/)

Scope Formula (as written)
Fx Rate = FunctionalRate × GroupRate
FxData Translated + FxMovement; FxMovement = closing − (all other steps), aggregated over novelty and step, kept only when its absolute value is at least Precision
FxDataWithoutAoc estimate types with no AoC chain (A, F, BEPA, APA, PL): Translated only
ReportingNodeRollup FxData.Values aggregated over reporting node, group and portfolio
AocReport Unexplained = Closing − Opening − Movements; Reconciles = abs(Unexplained) < Precision
Reconnecting…
The connection to the server was interrupted. Trying to restore it…
Trying again…
The connection could not be restored. Reloading the page…
The server was updated. Reloading the page to pick up the latest version.