How the cash-flow model works

Data model · Cash-flow model · Ceding a loss distribution through contract structures · The reinsurance suite

This page follows one acceptance through the cash-flow cube with real numbers. It then opens up the section engine that handles quota shares, excess-of-loss layers, aggregate layers and inuring. Every figure here is pinned by a test, either Reinsurance/Acceptance/Test/CessionTests.cs or Reinsurance/Acceptance/Test/CededClaimsTests.cs. If the code changed, a test would turn red before this page went out of date.

Two live walkthroughs run the same model in a kernel on the page: The Cash-Flow Cube and Ceding Through an Acceptance.

Part 1 — follow one acceptance

The inputs

The acceptance is Property Cat 40 xs 60 — UWY 2023: AttachmentPoint 60, Cover 40, PremiumRate 0.25, Share 1.0, in USD.

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

Property Cat 40 xs 60 — UWY 2023
Line of business Property
Underwriting year 2023
Currency USD
Attachment point 60
Cover 40
Rate on line 25 %
Share 100 %
Premium (rate on line × cover) 10

The book it protects is the Property / US / underwriting year 2023 segment. Five Cashflow nodes ship for it, all in millions of USD:

Node CashflowType DevelopmentDate Value
Property US 2023 — Premium Premium 2023-03-31 +12.0
Property US 2023 — Commission Commission 2023-03-31 −3.0
Property US 2023 — Paid Loss dev 2023 PaidLoss 2023-12-31 −20.0
Property US 2023 — Paid Loss dev 2024 PaidLoss 2024-12-31 −50.0
Property US 2023 — Paid Loss dev 2025 PaidLoss 2025-12-31 −20.0

The sign convention comes from Reinsurance/Cashflow/Source/Cashflow.cs: an inflow to the (re)insurer is positive (premium), and an outflow is negative (commission, paid loss). DevelopmentDate is the development axis: the same underwriting year 2023 pays out in 2023, 2024 and 2025.

The cession scope reads the paid losses as positive gross amounts per development year (CashflowStorage.GrossPaidByDevYear): 2023 → 20, 2024 → 50, 2025 → 20.

Step 1 — cumulative gross loss

Reinsurance/Acceptance/Source/Cession.cs, scope CededCashflows (its identity is the development year):

double GrossCumulative =>
    GetStorage().GrossPaidByDevYear.Where(kv => kv.Key <= Identity).Sum(kv => kv.Value);

Step 2 — what the layer has paid so far

double CededCumulative =>
    Math.Min(GetStorage().Acceptance.Cover,
             Math.Max(0, GrossCumulative - GetStorage().Acceptance.AttachmentPoint));

The layer would be exhausted at 60 + 40 = 100. This book stops at 90, so 10 of cover remains unused.

Step 3 — the ceded flow of each year

A flow is the increment of the cumulative. The scope reads the previous year from its own earlier result (GetScope<CededCashflows>(Identity - 1)):

double CededFlow =>
    CededCumulative
    - (GetStorage().GrossPaidByDevYear.ContainsKey(Identity - 1)
        ? GetScope<CededCashflows>(Identity - 1).CededCumulative
        : 0);

Step 4 — the net flow the cedent keeps

double NetFlow => -GetStorage().GrossPaidByDevYear.GetValueOrDefault(Identity) + CededFlow;

The result

Dev year Gross paid Gross cum Ceded cum Ceded flow Net flow
2023 20 20 0 0 −20
2024 50 70 10 +10 −40
2025 20 90 30 +20 0

The acceptance premium is PremiumRate × Cover = 0.25 × 40 = 10. The total recovery is 30, so the reinsurer's loss ratio is 30 / 10 = 300 % (CessionTests.AcceptanceResult_LossRatio300Percent).

That premium of 10 is computed from the acceptance's terms. It is not one of the Cashflow nodes. The +12 premium and −3 commission nodes are the segment's own flows.

A benign segment shows the other case. Casualty GB 2023 pays 1, 3 and 4 against a 10 xs 15 acceptance: the cumulative reaches 8 and never gets to 15. Nothing is ceded, and the net flow equals the gross (CessionTests.BenignSegment_NeverCedes).

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

Part 2 — the section engine

The CededCashflows scope handles one excess-of-loss layer applied to cumulative paid losses. Real programmes combine quota shares, per-claim layers, aggregate deductibles and limits, and sections that protect each other. The section engine in Reinsurance/Acceptance/Source/CededCashflows.cs handles these, working on TimeSeries cubes. A TimeSeries carries SectionId, ClaimsId, AmountType, Periodicity and Values[].

Two rules hold throughout the engine:

CededClaims (identity: an acceptance or section id) dispatches on what it finds:

The id names What happens
an Acceptance sum of its SectionIds' ceded cubes
an AggregateSection sum of its SubSectionIds' ceded cubes, then the AAD/AAL layer if set
a ModelledReinsuranceSection with ProportionalSectionCover quota share on every gross-net row, then AAD/AAL if set
a ModelledReinsuranceSection with NonProportionalSectionCover per-claim XL on individual claims only, then AAD/AAL if set

GrossNetClaimsScope supplies the input. A stored series id returns the stored series. A modelled section aggregates its Models. When a model id is another section, that section's claims arrive net of its own cession. This is the inuring rule.

The worked book — Alpina 2025

Six individual claims plus an attritional bulk, incurred, gross 3,240:

Claim C1 C2 C3 C4 C5 C6 ATT (attritional)
Gross 80 240 420 760 1,300 90 350

Quota share — 30 % with a claims limit of 300

Values = cf.Values.Select(v =>
    -Math.Min(sectionCover.Cession * v, sectionCover.ClaimsLimit ?? double.MaxValue)).ToArray()

With 40 % and a limit of 350, the cession is −1,126 and the net is 3,240 − 1,126 = 2,114. Exactly one claim, C5, is capped.

Per-claim XL — 500 xs 250

Values = cf.Values.Select(v =>
    -Math.Min(sectionCover.Limit, Math.Max(0, v - sectionCover.AttachmentPoint))).ToArray()

Only the individual-claim amount types reach this formula. The attritional bulk is filtered out first, because no single loss inside it is known.

The second layer, 500 xs 750, cedes C4 −10 and C5 −500, a total of −510.

The aggregate layer — AAD 200, AAL 800 around the 500 xs 250

CessionOperations.CedeIntoAggregateLayer groups the ceded rows by their top-level amount type and sums the year. The deductible erodes the total first, the limit caps what remains, and the adjustment is spread back over the claims pro rata. The formula as documented in the source:

yearCeded = Σ ceded                       // the year's cessions, summed (a negative number)
afterAad  = min(0, yearCeded + AAD)       // the deductible hands the first AAD back to the cedant
final     = max(afterAad, −AAL)           // the limit caps the year's total recovery
adjusted  = ceded × final / yearCeded     // …pushed back onto each claim, pro rata

With real numbers:

The order matters: the deductible applies before the limit caps. For a recovery of 4 with AAD 1 and AAL 2, the correct cession is 4 → 3 → 2. An earlier ordering gave 1, and the regression test AggregateLayer_RegressionAad1Aal2OnRecovery4_CedesExactly2 keeps that from coming back.

Inuring — who pays first

The programme has two sections. SecA is a 20 % quota share on the book. SecB is a 500 xs 250 whose LossModelMap points at SecA, so SecB sees the book net of SecA's cession.

  1. SecA cedes 20 % of everything: −(16 + 48 + 84 + 152 + 260 + 18 + 70) = −648.
  2. SecB sees the QS-net book: C3 336, C4 608, C5 1,040, ATT 280, …
  3. SecB cedes C3 −min(500, 336 − 250) = −86, C4 −min(500, 608 − 250) = −358, C5 −500, for a total of −944.
  4. The acceptance ProgramQsFirst sums both: −648 − 944 = −1,592, leaving a net of 3,240 − 1,592 = 1,648.

Flip the order and the XL sees the gross book first, ceding −1,170. The QS then takes 20 % of the XL-net 2,070, ceding −414, for a total of −1,584. The difference of 8 comes purely from who pays first.

Wrap SecA and SecB in an AggregateSection with AAD 150 and AAL 600. The total goes −1,592 → −1,442 → −600.

Development periods

The CededCashflows scope uses a development year as its identity, and each Cashflow fact carries a DevelopmentDate. Section-engine series carry a Periodicity (Yearly, Quarterly or Monthly) and treat Values as cumulative. When CessionOperations.Aggregate sums series of different lengths, it extends the shorter one by repeating its last value. The Alpina 2025 book in the tests has a single period per claim.

Part 3 — where the flows go

Check yourself

Question. Keep the 40 xs 60 acceptance, but suppose 2025 paid 40 instead of 20. What are the 2025 ceded cumulative, ceded flow and net flow?

Answer. The gross cumulative is 20 + 50 + 40 = 110. The ceded cumulative is min(40, max(0, 110 − 60)) = min(40, 50) = 40: the layer is exhausted. The ceded flow is 40 − 10 = +30, and the net flow is −40 + 30 = −10. The cedent carries everything above the exhaustion point of 100.

Reference

Formula (as in source) Where Pinned by
GrossCumulative = Σ GrossPaidByDevYear[k ≤ Identity] Acceptance/Source/Cession.cs CessionTests.GrossCumulative_Develops
CededCumulative = Math.Min(Cover, Math.Max(0, GrossCumulative - AttachmentPoint)) same CededCumulative_PiercesAttachmentIn2024
CededFlow = CededCumulative − previous CededCumulative same CededFlows_AreIncrements
NetFlow = −gross paid + CededFlow same NetFlows_RecoveryOffsetsIn2025
QS: -Math.Min(Cession * v, ClaimsLimit ?? double.MaxValue) Acceptance/Source/CededCashflows.cs Qs30_TakesItsSliceOfEveryAmountType
XL: -Math.Min(Limit, Math.Max(0, v - AttachmentPoint)) (individual claims only) same Xl500xs250_AttritionalStaysNet
AAD then AAL, pushed back pro rata Acceptance/Source/CessionOperations.cs AggregateLayer_*
Inuring: a model id that names a section yields that section's net CededCashflows.cs (GrossNetClaimsScope) Program_QsInuresToTheXl, Program_FlippedInuringOrder
Premium at 100 % = Cover × PremiumRate Acceptance/Source/AcceptanceReport.cs AcceptanceResult_LossRatio300Percent

Next: Ceding a loss distribution through contract structures · The Reinsurance data model · The reinsurance suite

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.