# Catastrophe excess of loss

> The contract that composes filter, contract period, occurrence excess, and aggregate excess, with reinstatements encoded in the aggregate limit.

Source: https://reinsurance.dev/financial-modelling/contracts/catxol/

## The composition

A **[catastrophe excess of loss](https://reinsurance.dev/glossary/#catxol-catastrophe-excess-of-loss)** contract (CatXoL) composes four building blocks. The [filter](https://reinsurance.dev/financial-modelling/terms/filter/) and [contract period](https://reinsurance.dev/financial-modelling/terms/contract-period/) select the covered events, the [occurrence excess](https://reinsurance.dev/financial-modelling/terms/occurrence-excess/) clips each one, and the [aggregate excess](https://reinsurance.dev/financial-modelling/terms/aggregate-excess/) caps the annual total. For a CatXoL with $n$ reinstatements:

$$
\text{CatXoL}(A, \ell, n) = \text{agg}_{\text{excess}}(0,\; (1+n) \cdot \ell) \circ \text{occ}_{\text{excess}}(A, \ell) \circ \text{period}(t_0, t_1) \circ \text{filter}(P)
$$

The occurrence layer attaches at $A$ (the [attachment point](https://reinsurance.dev/glossary/#attachment-point)) and pays up to $\ell$ (the [limit](https://reinsurance.dev/glossary/#limit)); Contract 1 is \$30M excess of \$10M, so $A = 10\text{M}$ and $\ell = 30\text{M}$ (in dollars). The $n$ [reinstatements](https://reinsurance.dev/glossary/#reinstatement), introduced conceptually in [Foundations](https://reinsurance.dev/foundations/products-and-contracts/#reinstatements), set the aggregate limit.

Read the formula as every [composition](https://reinsurance.dev/financial-modelling/composition/#composing-contracts-from-the-blocks) is read. It defines the contract at 100% (Caldera Re's 14.5% [participation](https://reinsurance.dev/glossary/#participation) enters at [portfolio roll-up](https://reinsurance.dev/financial-modelling/portfolios/#the-roll-up-participation-then-sum)), and it is exactly the pipeline we [dissected on Trial 9](https://reinsurance.dev/financial-modelling/dissection/#dissecting-a-catxol), now named term by term. For Contract 1:

| Building block | Contract 1 |
| --- | --- |
| [Loss filter](https://reinsurance.dev/financial-modelling/terms/filter/) | peril = HU, geography = FL |
| [Contract period](https://reinsurance.dev/financial-modelling/terms/contract-period/) | 2024-01-01 to 2024-12-31 |
| [Occurrence excess](https://reinsurance.dev/financial-modelling/terms/occurrence-excess/) | \$30M xs \$10M per event |
| [Aggregate excess](https://reinsurance.dev/financial-modelling/terms/aggregate-excess/) | cap at \$60M (1 reinstatement) |

:::tip[Intuition]
The occurrence layer buys protection against the severity of one storm: whatever a single hurricane costs above \$10M, the layer pays up to \$30M of it. The reinstatement buys protection against a season: a second full limit, so that two exhausting storms in one year are both covered. That is the risk the [post-model report](https://reinsurance.dev/applications/risk-profiling/#step-3-the-subject-loss-profile) identified for SunCoast (bad trials are many-storm trials), and it is why this layer is written per occurrence and carries a reinstatement.[^cat-reinstatements] A cedent's balance sheet is threatened by the size of one event and by the count of events, and the two terms price the two threats separately.
:::

The composition represents the reinstatement as [an aggregate limit](https://reinsurance.dev/financial-modelling/contracts/catxol/#reinstatements-as-the-aggregate-limit), which encodes the recoveries; the [reinstatement premium](https://reinsurance.dev/financial-modelling/beyond-loss/reinstatement-premium/) is computed from the composition's output, outside it.

## Reinstatements as the aggregate limit

The aggregate limit $(1 + n) \cdot \ell$ controls how many times the full occurrence limit can be paid. Without reinstatements ($n = 0$), 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.[^agg-limit]

:::caution[Watch Out]
The aggregate limit $(1 + n) \cdot \ell$ is a claims-side encoding: it represents the recoveries exactly and omits every other term of the reinstatement provision. A real provision has more terms than the count: each reinstatement can be free or paid, and a paid one requires an additional premium computed pro rata to the amount of limit restored, pro rata to the time remaining in the contract period, or both.[^reinst-premium] Those premium terms are commercially material: two contracts with the same layer and the same reinstatement count can cost the cedent very different amounts. The composition includes the claims-side encoding; the premium terms are computed from this contract's output on the [reinstatement premium](https://reinsurance.dev/financial-modelling/beyond-loss/reinstatement-premium/) page.
:::

With one reinstatement Contract 1's aggregate limit is \$60M, and [Trial 9](https://reinsurance.dev/financial-modelling/contracts/catxol/#contract-1-on-trial-9) is a trial in which it binds.

## 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:

*[Interactive figure: Term explorer. See the web page: https://reinsurance.dev/financial-modelling/contracts/catxol/]*

Python (`python/caldera_re/catxol.py`, runnable on the web page):

```python
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)](https://reinsurance.dev/glossary/#yelt-year-event-loss-table): [occurrence exceedance probability (OEP)](https://reinsurance.dev/glossary/#oep-occurrence-exceedance-probability) metrics and occurrence-term retrocession still work downstream. Across trials there is no state at all: the [partition by trial](https://reinsurance.dev/applications/production-scale/#one-computational-shape) 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](https://reinsurance.dev/financial-modelling/terms/aggregate-excess/) gives the vectorised form.

## Across the demo tier

Across all 20 trials of the [demo tier](https://reinsurance.dev/toolkit/trials/#demo-scale), the standalone gross distribution is reported at 100%, before participation. The \$60M aggregate limit binds in two trials, Trial 9 and Trial 19:

*[Interactive figure: Term explorer. See the web page: https://reinsurance.dev/financial-modelling/contracts/catxol/]*

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](https://reinsurance.dev/toolkit/metrics/expected-loss/) of \$17.4M.

Drop the per-occurrence layer and you have an [aggregate excess of loss (AggXoL)](https://reinsurance.dev/financial-modelling/contracts/aggxol/) contract; replace the occurrence excess with a [trigger](https://reinsurance.dev/glossary/#trigger) and you have a franchise structure, which pays the whole loss once the threshold is exceeded rather than the excess over it.[^franchise] The [programs](https://reinsurance.dev/financial-modelling/programs/) page works through how a CatXoL chains with other contracts.

---

[^reinst-premium]: Richard R. Anderson and Weimin Dong, ["Pricing Catastrophe Reinsurance with Reinstatement Provisions Using a Catastrophe Model"](https://www.casact.org/sites/default/files/database/forum_98sforum_98sf303.pdf), CAS Forum, Summer 1998. Prices layers whose reinstatement premiums are pro rata to the amount of limit reinstated and, optionally, to the time remaining. On the free-versus-paid distinction and the variations in practice, see IRMI, ["More Porridge Please: Reinstatements in Reinsurance"](https://www.irmi.com/articles/expert-commentary/more-porridge-please-reinstatements-in-reinsurance), and IRMI, ["Reinstatement Premium"](https://www.irmi.com/term/insurance-definitions/reinstatement-premium).

[^cat-reinstatements]: David R. Clark, ["Basics of Reinsurance Pricing"](https://www.casact.org/sites/default/files/old/studynotes_clark_2014.pdf), 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"](https://www.sec.gov/Archives/edgar/data/1401521/000140152112000027/exh10426jun12.htm), SEC EDGAR exhibit 10.4, 2012, places three occurrence layers over retentions of \$15M, \$25M, and \$63.3M, each reinstated once.

[^agg-limit]: 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".

[^franchise]: Guy Carpenter, [Glossary of Reinsurance Terms](https://www.guycarp.com/company/news-and-events/glossary.html), *Franchise Covers*: "no loss is payable until the loss exceeds a certain amount but, when that amount is exceeded, the whole loss is paid."
