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:

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

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. AttritionalLoss is a ratio line on earned premium. With no assumption, RatioTo uses 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. Amount returns 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:

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 —
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.