Financial Modelling · Section 5.2
Catastrophe excess of loss
The contract that composes filter, contract period, occurrence excess, and aggregate excess, with reinstatements encoded in the aggregate limit.
The composition
Section titled “The composition”A catastrophe excess of loss contract (CatXoL) composes four building blocks. The filter and contract period select the covered events, the occurrence excess clips each one, and the aggregate excess caps the annual total. For a CatXoL with reinstatements:
The occurrence layer attaches at (the attachment point) and pays up to (the limit); Contract 1 is $30M excess of $10M, so and (in dollars). The reinstatements, introduced conceptually in Foundations, set the aggregate limit.
Read the formula as every composition is read. It defines the contract at 100% (Caldera Re’s 14.5% participation enters at portfolio roll-up), and it is exactly the pipeline we dissected on Trial 9, now named term by term. For Contract 1:
| Building block | Contract 1 |
|---|---|
| Loss filter | peril = HU, geography = FL |
| Contract period | 2024-01-01 to 2024-12-31 |
| Occurrence excess | $30M xs $10M per event |
| Aggregate excess | cap at $60M (1 reinstatement) |
The composition represents the reinstatement as an aggregate limit, which encodes the recoveries; the reinstatement premium is computed from the composition’s output, outside it.
Reinstatements as the aggregate limit
Section titled “Reinstatements as the aggregate limit”The aggregate limit controls how many times the full occurrence limit can be paid. Without reinstatements (), the aggregate limit equals the occurrence limit: one full event. Each reinstatement restores the per-occurrence limit once, free or in exchange for an additional premium.2
With one reinstatement Contract 1’s aggregate limit is $60M, and Trial 9 is a trial in which it binds.
Contract 1 on Trial 9
Section titled “Contract 1 on Trial 9”On Trial 9, the four Florida hurricanes’ per-occurrence gross accumulate against the $60M aggregate limit: $19.7M, $30M, $0 (a $4.0M storm short of the attachment), and $30M after the $30M xs $10M layer. The cumulative would reach $79.7M, but with one reinstatement (two full limits of cover) the limit caps it at $60M:
Contract 1 (30M xs 10M, 1 reinstatement) on Trial 9: each Florida hurricane's gross after the occurrence layer, accumulating against the $60M aggregate limit. The cumulative line is clipped at that limit, and the trial's gross is $60.0M. The sliders set the attachment, the occurrence limit and the number of reinstatements, and the EP curve chart on this page follows them.
import numpy as np
import pandas as pd
from caldera_re.aggregate_excess import aggregate_excess
from caldera_re.loss_filter import loss_filter
from caldera_re.occurrence_excess import OCCURRENCE_KEY, occurrence_excess
from caldera_re.aep import aep
from caldera_re.data import cedents
from caldera_re.contract_period import contract_period
from caldera_re.report import table
from caldera_re.tvar import tvar
from caldera_re.var import var
def catxol(
loss: pd.DataFrame,
attachment: float,
limit: float,
reinstatements: int = 0,
period_start: str = '2024-01-01',
period_end: str = '2024-12-31',
**criteria,
) -> pd.DataFrame:
"""Apply CatXoL: filter + contract period + occurrence layer + aggregate
cap. The result is the 100% layer; participation is applied at roll-up.
Composition:
agg_excess(0, agg_limit) . occ_excess(A, l)
. period(t0, t1) . filter(criteria)
Filters to the covered rows and contract period, applies the occurrence
excess per event, and caps total gross at the aggregate limit
(1 + reinstatements) * limit. The cap erodes per occurrence (YELT in,
YELT out): each occurrence's 'gross_loss' is its contribution to the
capped aggregate, split across the occurrence's rows in proportion to
their losses, so per-trial sums give the trial-level result exactly.
Parameters
----------
loss : pd.DataFrame
YELT with 'trial_id' and 'loss' columns.
attachment : float
Per-occurrence attachment point.
limit : float
Per-occurrence limit.
reinstatements : int
Number of reinstatements (default 0 = single coverage).
period_start, period_end : str
Contract period, inclusive. Defaults to full-year 2024.
**criteria
Filter column = value pairs, e.g. peril='HU', geography_id='FL'.
Returns the covered input rows in claims order, every input column
kept, with 'gross_loss' carrying each row's share of the occurrence-layer
result after the aggregate cap's erosion.
"""
covered = loss_filter(loss, **criteria)
covered = contract_period(covered, period_start, period_end)
occ = occurrence_excess(covered, attachment, limit)
agg_limit = limit * (1 + reinstatements)
result = aggregate_excess(occ, attachment=0, limit=agg_limit,
loss_column='gross_loss')
return result
if __name__ == '__main__':
# Contract 1: SunCoast FL HU CatXoL, $30M xs $10M, 1 reinstatement.
A, L, REINST = 10.0, 30.0, 1
crit = dict(peril='HU', geography_id='FL')
# Covered subject (filter + contract period) for the per-occurrence view.
covered = contract_period(loss_filter(cedents.suncoast, **crit),
'2024-01-01', '2024-12-31')
occ_result = occurrence_excess(covered, attachment=A, limit=L)
print(f"Contract 1: Florida Hurricane CatXoL ({L:.0f}M xs {A:.0f}M, "
f"{REINST} reinst.)")
print(f"\nPer-occurrence layer application:")
N = 20
lines = []
for s in range(1, N + 1):
rows = occ_result[occ_result['trial_id'] == s]
n_occ = rows.groupby(OCCURRENCE_KEY).ngroups
subj = rows['loss'].sum() if n_occ > 0 else 0
gross = rows['gross_loss'].sum() if n_occ > 0 else 0
if subj > 0:
lines.append((s, n_occ, subj, gross))
table(['Trial', 'Occs', 'Subject', 'Occ Gross'], lines,
fmt=['{}', '{}', '{:.1f}M', '{:.1f}M'])
# Step 2: full CatXoL composition on the raw YELT
# (filter -> period -> occ_excess -> agg_excess), at the 100% layer.
result = catxol(cedents.suncoast, attachment=A, limit=L,
reinstatements=REINST, **crit)
by_trial = (result.groupby('trial_id')
.agg(aggregate_subject=('loss', 'sum'),
gross_loss=('gross_loss', 'sum'))
.reset_index())
occ_total = occ_result.groupby('trial_id')['gross_loss'].sum()
agg_limit = L * (1 + REINST)
print(f"\nAfter aggregate cap (limit = {agg_limit:.0f}M, "
f"{REINST} reinstatement):")
gross_arr = np.zeros(N)
lines = []
for s in range(1, N + 1):
row = by_trial[by_trial['trial_id'] == s]
if len(row) > 0:
subj = float(occ_total.get(s, 0.0))
gross = row['gross_loss'].iloc[0]
gross_arr[s - 1] = gross
if subj > 0:
lines.append((s, subj, gross))
table(['Trial', 'Occ Total', 'Final Gross'], lines,
fmt=['{}', '{:.1f}M', '{:.1f}M'])
print(f"\nGross EL: {np.mean(gross_arr):.2f}M")
gross_curve = aep(pd.DataFrame({
'trial_id': range(1, N + 1), 'loss': gross_arr,
}), n_trials=N)
print(f"Gross VaR(90%): {var(gross_curve, 0.90):.1f}M")
print(f"Gross TVaR(90%): {tvar(gross_curve, 0.90):.1f}M")
print(f"\nNote: With multiple FL HU occurrences per trial,")
print(f"the aggregate cap binds when per-occurrence payouts")
print(f"sum to more than the aggregate limit ({agg_limit:.0f}M).")
print(f"The cap erodes per occurrence, so the output stays a YELT.") The reinstatement makes the contract stateful within a trial. The October storm’s gross depends on what July and September consumed: the same $30M occurrence pays $30M early in the year and $10.3M once $49.7M of the aggregate limit is gone, so occurrences must be evaluated in claims order and the aggregate cap is a running quantity, not a per-trial constant. The cap erodes occurrence by occurrence, so the output stays a year event loss table (YELT): occurrence exceedance probability (OEP) metrics and occurrence-term retrocession still work downstream. Across trials there is no state at all: the partition by trial holds, and what the state costs at production scale is one sort into claims order plus a running sum per trial. The aggregate excess page gives the vectorised form.
Across the demo tier
Section titled “Across the demo tier”Across all 20 trials of the demo tier, the standalone gross distribution is reported at 100%, before participation. The $60M aggregate limit binds in two trials, Trial 9 and Trial 19:
Contract 1 (30M xs 10M, 1 reinstatement) over the 20 trials: the Florida-hurricane subject against the gross loss. 13 of 20 trials pay; the expected gross loss is $17.39M. The sliders are shared with the Trial 9 chart on this page.
This composition turns Trial 9’s $272.1M subject into a $60M standalone gross (Caldera Re books its 14.5% share, $8.7M, at roll-up) and across all 20 trials produces a standalone expected loss of $17.4M.
Drop the per-occurrence layer and you have an aggregate excess of loss (AggXoL) contract; replace the occurrence excess with a trigger and you have a franchise structure, which pays the whole loss once the threshold is exceeded rather than the excess over it.4 The programs page works through how a CatXoL chains with other contracts.
Footnotes
Section titled “Footnotes”-
David R. Clark, “Basics of Reinsurance Pricing”, Casualty Actuarial Society study note, rev. 2014, §5A: a catastrophe cover is written per occurrence, and “because the limit is often a substantial dollar amount, the contract provides a limited number of reinstatements”, without which “after the full layer is exhausted, there is no more protection”. United Property and Casualty Insurance Company, “Property Catastrophe Excess of Loss Reinsurance Agreement”, SEC EDGAR exhibit 10.4, 2012, places three occurrence layers over retentions of $15M, $25M, and $63.3M, each reinstated once. ↩
-
Clark, “Basics of Reinsurance Pricing”, §5A: “the treaty effectively has an aggregate limit equal to one plus the number of reinstatements, times the occurrence limit.” United Property and Casualty Insurance Company, Property Catastrophe Excess of Loss Reinsurance Agreement, whose first layer states both numbers, “the Reinsurer’s liability hereunder shall never exceed $10,000,000 for any one Loss Occurrence and $20,000,000 for all Loss Occurrences during the term of this Agreement”, and whose schedules reinstate one layer free and another “in consideration of the payment by the Company of an additional premium”. ↩
-
Guy Carpenter, Glossary of Reinsurance Terms, Franchise Covers: “no loss is payable until the loss exceeds a certain amount but, when that amount is exceeded, the whole loss is paid.” ↩