The planning formulas
This page explains how the Planning engine turns the planner's stored figures into a five-year technical statement. It also explains how that statement is compared with what was actually booked. Every formula is quoted the way the code writes it, and the source path is named beside it.
The page starts with one worked example, followed step by step. The general rule, the data model and a reference table come after it.
The one rule to remember. The plan year is the planner's stored fact. Every later year is
computed as assumption × driver. Computed years are streamed to the page and never saved
(Planning/Engine/Source/Projection.cs).
The worked example: Budget 2027
The module ships one worked plan as data nodes under Planning/Samples. The
tests pin the same numbers through Planning/SampleData/Source/PlanningSampleData.cs.
Budget 2027 has plan year 2027 and horizon 5. It is a Budget with status Approved. It covers one slice: line of business Casualty, underwriting year 2027, in EUR millions.
Stored plan-year facts (PlanFact, projection: 0). Inflows are positive,
outflows negative.
| Plan line | 2027 |
|---|---|
| Premium written | 127.5 |
| Premium earned | 127.5 |
| Attritional losses | −78.2 |
| Large losses | −8.5 |
| Commission | −12.8 |
| Acquisition expenses | −5.1 |
| Administrative expenses | −1.9 |
| Other expenses | −0.6 |
Assumptions. Values[i] applies to projection i+1, so the first value is 2028.
| Node | Archetype | Line | Values |
|---|---|---|---|
Budget2027WrittenGrowth |
RateAssumption | Premium written | 1.04, 1.04, 1.03, 1.03 |
Budget2027LargeLossPath |
AmountAssumption | Large losses | −8.7, −8.9, −9.2, −9.4 |
Budget2027AdministrativeExpensePath |
AmountAssumption | Administrative expenses | −2.0, −2.0, −2.1, −2.1 |
The budget states no ratio assumption. Every ratio line therefore uses the default, which is the plan year's own implied ratio.
Here is the live projection, computed from those nodes:
Subscribers see this result live; below is the same worked example as a static table.
Budget 2027, Casualty, EUR millions. Every figure follows from the stored facts and assumptions above by the formulas in Steps 1–4. 2028 and 2031 are the values worked out by hand below.
| Line | 2027 | 2028 | 2029 | 2030 | 2031 |
|---|---|---|---|---|---|
| Premium written | 127.5 | 132.6 | 137.904 | 142.041 | 146.302 |
| Premium earned | 127.5 | 132.6 | 137.904 | 142.041 | 146.302 |
| Attritional losses | −78.2 | −81.328 | −84.581 | −87.119 | −89.732 |
| Large losses | −8.5 | −8.7 | −8.9 | −9.2 | −9.4 |
| Commission | −12.8 | −13.312 | −13.844 | −14.260 | −14.688 |
| Acquisition expenses | −5.1 | −5.304 | −5.516 | −5.682 | −5.852 |
| Administrative expenses | −1.9 | −2.0 | −2.0 | −2.1 | −2.1 |
| Other expenses | −0.6 | −0.6 | −0.6 | −0.6 | −0.6 |
| Technical result | 20.4 | 21.356 | 22.462 | 23.081 | 23.931 |
| Combined ratio | 84.0 % | 83.9 % | 83.7 % | 83.8 % | 83.6 % |
The live area also lists the shipped Forecast 2027-1, which is not repeated here.
Step 1 — the plan year reads the stored fact
Each scope family has an applicability rule that dispatches projection 0 to its Planned…
implementation:
bool IsPlanYear => Identity.Projection <= 0;
static ScopeConfiguration<PremiumWritten> Configure(ScopeConfiguration<PremiumWritten> config) => config
.WithApplicability<PlannedPremiumWritten>(x => x.IsPlanYear);
double PremiumWritten.Value => GetStorage().PlanValue(Identity, PlanLines.PremiumWritten);
So 2027 written premium is 127.5. A growth assumption exists, but it has no effect on the plan
year (EngineTests.PlanYear_ReadsTheStoredFact).
Step 2 — written premium grows (the Rate archetype)
/// value[n] = growth[n] × value[n−1].
double Value =>
GetStorage().GrowthFactor(Identity, PlanLines.PremiumWritten)
* GetScope<PremiumWritten>(Identity with { Projection = Identity.Projection - 1 }).Value;
GrowthFactor reads RateAssumption.Values[Projection − 1]. If there is no assumption, or the
array is too short, it returns 1, meaning flat (PlanStorage.cs).
| Year | Formula | Written |
|---|---|---|
| 2028 | 1.04 × 127.5 | 132.6 |
| 2029 | 1.04 × 132.6 | 137.904 |
| 2030 | 1.03 × 137.904 | 142.041 |
| 2031 | 1.03 × 142.041 | 146.302 |
Growth_RecursOverTheFullHorizon pins 2031 as 127.5 × 1.04 × 1.04 × 1.03 × 1.03.
Step 3 — earned premium and the ratio lines (the Ratio archetype)
/// value[n] = earnedRatio[n] × written[n].
double Value =>
GetStorage().RatioTo(Identity, PlanLines.PremiumEarned, PlanLines.PremiumWritten)
* GetScope<PremiumWritten>(Identity).Value;
RatioTo first looks up a RatioAssumption, keyed by version, line of business, underwriting
year, line and reference line, and reads it at Values[Projection − 1]. If there is none, it
falls back to the plan year's implied ratio, carried flat:
return PlanningUtils.DivideOrDefault(PlanValue(planYear, line), PlanValue(planYear, referenceLine));
The budget states no ratios, so every ratio line keeps its 2027 relationship:
| Line | Driver | Default ratio | 2028 |
|---|---|---|---|
| Premium earned | written | 127.5 / 127.5 = 1 | 1 × 132.6 = 132.6 |
| Attritional losses | earned | −78.2 / 127.5 = −0.61333 | −0.61333 × 132.6 = −81.328 |
| Commission | written | −12.8 / 127.5 = −0.10039 | −0.10039 × 132.6 = −13.312 |
| Acquisition expenses | written | −5.1 / 127.5 = −0.04 | −0.04 × 132.6 = −5.304 |
Ratio_FollowsItsDriver pins attritional 2028 as −78.2 × 1.04. The loss grows 4 % along with the
book.
Step 4 — the absolute lines (the Amount archetype)
/// value[n] = amount[n], or value[n−1] where no amount is stated.
double Value =>
GetStorage().Amount(Identity, PlanLines.LargeLoss)
?? GetScope<LargeLoss>(Identity with { Projection = Identity.Projection - 1 }).Value;
| Line | 2028 | 2029 | 2030 | 2031 | Why |
|---|---|---|---|---|---|
| Large losses | −8.7 | −8.9 | −9.2 | −9.4 | read from the assumption |
| Administrative expenses | −2.0 | −2.0 | −2.1 | −2.1 | read from the assumption |
| Other expenses | −0.6 | −0.6 | −0.6 | −0.6 | no assumption, so the previous year carries forward |
A missing assumption means "unchanged", never zero (Absolute_DefaultsToCarryForward).
Step 5 — technical result and combined ratio (derived, never stored)
/// Earned premium + attritional losses + large losses + commission + expenses, all signed.
double Value = PremiumEarned + AttritionalLoss + LargeLoss + Commission
+ AcquisitionExpense + AdministrativeExpense + OtherExpense;
/// −(losses + commission + expenses) / earned premium; 0 over an empty base.
double Value = PlanningUtils.DivideOrDefault(-(AttritionalLoss + LargeLoss + Commission
+ AcquisitionExpense + AdministrativeExpense + OtherExpense), PremiumEarned);
(The formulas are abbreviated here. The code calls GetScope<…>(Identity).Value for each term.)
| Year | Technical result | Combined ratio |
|---|---|---|
| 2027 | 127.5 − 78.2 − 8.5 − 12.8 − 5.1 − 1.9 − 0.6 = 20.4 | 107.1 / 127.5 = 84.0 % |
| 2028 | 132.6 − 81.328 − 8.7 − 13.312 − 5.304 − 2.0 − 0.6 = 21.356 | 111.244 / 132.6 = 83.9 % |
| 2031 | 23.93 | 83.6 % |
TechnicalResult has no Planned… implementation, on purpose. Even in the plan year it is
computed from the stored components, so a plan can never disagree with its own parts
(PlanYear_TechnicalResultIsTheSignedSum, PlanYear_CombinedRatioReadsPositive).
Step 6 — the forecast revision
Forecast 2027-1 is BasedOn the budget. It changes four things:
- plan-year attritional losses of −83.3 instead of −78.2;
- plan-year administrative expenses of −2.0 instead of −1.9, with no administrative-expense path, so −2.0 carries flat through 2031;
- growth of 1.03 a year;
- an explicit attritional RatioAssumption on earned premium of −0.655, −0.655, −0.65, −0.65.
| Budget | Forecast | |
|---|---|---|
| Plan-year loss ratio (attritional + large) / earned | (78.2 + 8.5) / 127.5 = 0.68 | (83.3 + 8.5) / 127.5 = 0.72 |
| Plan-year technical result | 20.4 | 15.2 |
| 2028 attritional loss | −81.328 | −0.655 × (127.5 × 1.03) = −86.018 |
The revision costs 5.2 of technical result in 2027: 5.1 from the higher attritional losses
and 0.1 from the higher administrative expenses (Chain_TheForecastRevisionCostsResult).
The explicit ratio is applied to the grown driver (Ratio_HonoursAnExplicitAssumption).
Plan vs actual — the variance join
Variance is where Planning reads the
Reinsurance cash-flow cube. It reads the cube live through a
nodeType:Reinsurance/Cashflow query over the hub's own partition and the Reinsurance
partition (Planning/Variance/Source/VarianceAreas.cs). The dependency runs one way only:
Planning reads Reinsurance, and Reinsurance never reads Planning.
The mapping from cash-flow type to plan line is data. It lives on the PlanLine rows:
| PlanLine | actualsCashflowType |
|---|---|
| Premium written | Reinsurance/CashflowType/Premium |
| Loss (group) | Reinsurance/CashflowType/PaidLoss |
| Commission | Reinsurance/CashflowType/Commission |
PlanVsActual (Planning/Variance/Source/PlanVsActual.cs) builds two cubes and subtracts them:
// plan side: the engine's value for each slice, each projection year, each mapped line
FinancialYear = planYear + projection, Value = ProjectionChain.LineValue(registry, identity, line)
// actual side: each cash flow whose type a line claims
PlanLine = mapping[cf.CashflowType], FinancialYear = cf.DevelopmentDate.Year, Value = cf.Value
// the variance
actual.Minus(plan).AggregateOver()
Worked on the test book (Planning/Variance/Test/VarianceTests.cs, synthetic actuals against
Budget 2027):
| Line, FY 2027 | Actual | Plan | Variance = actual − plan |
|---|---|---|---|
| Premium written | 126.0 | 127.5 | −1.5 (adverse: less premium came in) |
| Loss (−35.0 and −45.0 netted) | −80.0 | −78.2 − 8.5 = −86.7 | +6.7 (favourable: less was paid) |
| Commission | −12.8 | −12.8 | 0.0 |
Rows that exist on only one side are kept. An unplanned marine book shows its full actual, +5.0 against a plan of 0. FY 2028 premium, with nothing booked yet, shows −132.6.
The general rule
- Identity. Each scope is keyed by
PlanIdentity(PlanVersion, LineOfBusiness, UnderwritingYear, Projection). Results are memoized, so the five-year recursion evaluates each year only once. - Pattern lives on the dimension. Each PlanLine row declares its
ProjectionKind:Growth,RatioToReference,AbsoluteorDerived. The scope family implements that pattern against its fixed driver. - Defaults are stated once, in
PlanStorage:- a missing rate means flat (1);
- a missing ratio means the plan year's own ratio;
- a missing amount means the previous year carries forward;
- past the end of an assumption array, the same default applies.
- Kind does not affect the arithmetic. Budget, Forecast, Stress and AsIf all run the same chain.
Kind_IsOrthogonalToTheMechanicspins identical results for a budget relabelled as Stress. - Facts add up. Facts on the same cell are summed over everything the identity does not carry,
such as currencies. Group lines are sums of their children:
Loss = Attritional + Large, andExpense = Acquisition + Administrative + Other.Premiumis a heading only, and reads 0.
The data model
Prerequisite: the class diagrams on this page are embedded from the Data Modelling plugin's explorer (/DataModelling/Explorer), which Planning does not require. On an installation without it they render nothing; each type's own page under /Planning still lists its properties.
One plan cell, the dimensions it references, and the Reinsurance dimensions it borrows:
Check yourself
Q1. Budget 2027 has no attritional loss-ratio assumption. Why does its 2028 attritional loss still change from −78.2?
A.
AttritionalLossis a ratio line on earned premium. With no assumption,RatioTouses the plan year's own ratio, −78.2 / 127.5, and applies it to 2028's earned premium of 132.6. That gives −81.328. The ratio stays the same; the driver grows.
Q2. Suppose you delete Budget2027LargeLossPath. What are the large losses for 2028 to 2031?
A. −8.5 in every year.
Amountreturns null, so the scope carries the previous year forward, starting from the plan-year fact (Absolute_DefaultsToCarryForward).
Q3. A premium actual of 126.0 sits against a plan of 127.5. Why does the variance read −1.5 and not +1.5?
A. Variance is
actual.Minus(plan). Inflows are positive, so receiving less premium than planned gives a negative variance (adverse). Losses are negative, so paying less than planned gives a positive variance (favourable).
What this module does not (yet) do
These are stated so nobody has to rediscover them. The list is also in Planning:
- No planning at an aggregated line-of-business level. The engine works on stored slices only.
- No scenario read-through overrides along
BasedOn. A forecast stores its own complete facts. - No tie-in to SST capital or to IFRS 17.
- The variance converts no currencies. The demo book uses one currency.
- Not established here: the precedence when more than one
WithApplicabilityrule matches. Each family here has exactly one rule, so the question does not come up.
For how the cash flows the variance reads are modelled, see How the cash-flow model works. For how Planning fits with the other modules, see the suite overview.
Reference — the scopes (Planning/Engine/Source/Projection.cs)
| Scope | Pattern | Plan year (Projection <= 0) |
Later years (as written) |
|---|---|---|---|
PremiumWritten |
Growth | PlannedPremiumWritten: PlanValue |
GrowthFactor × PremiumWritten[n−1] |
PremiumEarned |
Ratio → written | PlannedPremiumEarned |
RatioTo(Earned, Written) × PremiumWritten[n] |
AttritionalLoss |
Ratio → earned | PlannedAttritionalLoss |
RatioTo(Attritional, Earned) × PremiumEarned[n] |
LargeLoss |
Absolute | PlannedLargeLoss |
Amount ?? LargeLoss[n−1] |
Commission |
Ratio → written | PlannedCommission |
RatioTo(Commission, Written) × PremiumWritten[n] |
AcquisitionExpense |
Ratio → written | PlannedAcquisitionExpense |
RatioTo(Acquisition, Written) × PremiumWritten[n] |
AdministrativeExpense |
Absolute | PlannedAdministrativeExpense |
Amount ?? AdministrativeExpense[n−1] |
OtherExpense |
Absolute | PlannedOtherExpense |
Amount ?? OtherExpense[n−1] |
TechnicalResult |
Derived | same formula | Earned + Attritional + Large + Commission + Acquisition + Administrative + Other |
CombinedRatio |
Derived | same formula | DivideOrDefault(−(losses + commission + expenses), Earned) |
Storage helper (Planning/Engine/Source/PlanStorage.cs) |
Returns | Default |
|---|---|---|
PlanValue(identity, line) |
stored fact at the identity's projection, summed | 0 |
GrowthFactor(identity, line) |
RateAssumption.Values[Projection − 1] |
1 |
RatioTo(identity, line, reference) |
RatioAssumption.Values[Projection − 1] |
plan-year line / reference |
Amount(identity, line) |
AmountAssumption.Values[Projection − 1] |
null, so the previous year carries forward |
FinancialYear(identity) |
PlanYear + Projection |
— |