# Portfolios

> The roll-up that produces a portfolio's YLT (for AEP) or portfolio YELT (for OEP), and why expected loss adds across contracts but tail risk does not.

Source: https://reinsurance.dev/financial-modelling/portfolios/

The [Programs](https://reinsurance.dev/financial-modelling/programs/) page described how a single cedent's
placement is assembled: a coordinated set of layers on a shared exposure set. But a
reinsurer does not write only one program. It writes a **[portfolio](https://reinsurance.dev/glossary/#portfolio)** of programs and
contracts: SunCoast's three-layer program sits beside Pacific Mutual's Californian
earthquake program, Baltica's European windstorm quota share, and Atlas Cat Fund's
Japanese earthquake placement.

The portfolio is where a reinsurer's business becomes visible, and it is not a passive
record. Like an investment fund's portfolio, it is **actively managed**. What to write,
what to renew, what to shed is a continuous business decision, made with insurance
contracts where a fund manager deals in capital-market instruments. Portfolio-level
metrics are the key performance indicators (KPIs) the company manages by, tactically and strategically. The written portfolio is the culmination of everything the reinsurer sells, and everything else it
does (investment strategy, [retrocession](https://reinsurance.dev/glossary/#retrocession), [sidecars](https://reinsurance.dev/glossary/#sidecar), [cat bonds](https://reinsurance.dev/glossary/#cat-bond)) depends on that portfolio existing and behaving predictably. Pricing and capital decisions are not based on the portfolio alone, but every one of them uses its numbers. Before any of that management can act on numbers, though, the
portfolio has to be reduced to a single loss distribution. Here the portfolio is
treated as a modelling object; the **roll-up** is the mechanical task that produces that
distribution, and the one property that makes the result interesting is that expected loss
adds across contracts, but tail risk does not.

## A portfolio is a bundle of bundles

Nothing structurally new appears at the portfolio level. The nesting goes one level deeper:

```mermaid
graph TD
    P["Portfolio"]:::Port --> PR1["SunCoast<br/>program"]:::Prog
    P --> PR2["Pacific Mutual<br/>program"]:::Prog
    P --> PR3["Baltica<br/>program"]:::Prog
    P --> PR4["Atlas Cat Fund<br/>program"]:::Prog
    PR1 --> C1["C1<br/>FL hurricane"]:::Contract
    PR1 --> C2["C2<br/>CA/AZ quake"]:::Contract
    PR1 --> C3["C3<br/>All-US backstop"]:::Contract
    PR2 --> C4["C4<br/>CA quake"]:::Contract
    PR3 --> C5["C5<br/>EU windstorm quota share"]:::Contract
    PR4 --> C6["C6<br/>JP quake"]:::Contract

    classDef Port fill:#F7CBCB,stroke:#B46669,stroke-width:1.5px,color:#2B2620
    classDef Prog fill:#BDE2CB,stroke:#459269,stroke-width:1.5px,color:#2B2620
    classDef Contract fill:#BFDBF8,stroke:#4F84BA,stroke-width:1.5px,color:#2B2620
```

A **[term](https://reinsurance.dev/financial-modelling/composition/)** transforms a loss. A **contract** is a
composition of terms. A **[program](https://reinsurance.dev/financial-modelling/programs/)** is a bundle of
contracts on one exposure set, and even a placement of a single layer, like Pacific
Mutual's or Atlas's, arrives as a program of one contract. A **portfolio** bundles
written programs and contracts for analysis. Each level is a container for the one
below, and each level's loss is a sum of the losses one level down.

To the business a portfolio is a managed object:
a set of positions whose composition is deliberately steered and whose metrics are
company KPIs. To the model it is a selection: a construct that picks
programs and contracts and bundles them for aggregate analysis. Reinsurers routinely run
several portfolios at once, cut by business unit, by line of business, by contract
period, or by binding status. The last of
those separates the **live bound** portfolio from the **live working** portfolio of deals
still in negotiation, which is watched to catch concentrations building up before the deals
bind.[^portfolio-slices] In this site's assessment there is only one portfolio and everything
else is a slice of it. That is also how the tools behave: a slice is a filter, not a
new object.

:::note[Remember]
"Portfolio" here means the reinsurer's collection of in-force programs and contracts:
like a financial portfolio, a set of actively managed positions that may generate profit
or loss. Do not confuse it with a cedent's program, which is one placement the
reinsurer might write a share of. A program is something you buy or write; a portfolio
is what you manage across everything you have written.
:::

Whichever slice is selected, its loss distribution comes from
[the roll-up](https://reinsurance.dev/financial-modelling/portfolios/#the-roll-up-participation-then-sum): each contract scaled to its written share,
then summed.

## The roll-up: participation, then sum

Each contract, evaluated on its cedent's year event loss table ([YELT](https://reinsurance.dev/glossary/#yelt-year-event-loss-table)), produces a per-trial **[gross loss](https://reinsurance.dev/glossary/#gross-loss)**:
the loss at the 100% layer. But the reinsurer rarely writes the whole layer; it writes a
**[participation](https://reinsurance.dev/glossary/#participation)** share $p_i$, placed alongside a panel of other
reinsurers ([why layers are split](https://reinsurance.dev/foundations/products-and-contracts/#participation) is
covered in Foundations). The roll-up scales each contract's gross loss by its participation, then sums:

$$
L_j^{\text{portfolio}} = \sum_{i=1}^{n} p_i \, C_i(L_j)
$$

For each trial $j$, take every contract's gross loss $C_i(L_j)$ (the 100% layer result by
definition, since [compositions carry no participation](https://reinsurance.dev/financial-modelling/composition/#composing-contracts-from-the-blocks)),
scale it by that contract's participation $p_i$ (the roll-up's own
[scaling](https://reinsurance.dev/financial-modelling/terms/scaling/) step), and add
the scaled losses together. **[Portfolio roll-up](https://reinsurance.dev/glossary/#roll-up)** is the task of producing that sum across
all $N$ trials, reducing the constituents' loss tables to a single loss distribution for
the whole selection.

The sum over $i$ at a fixed $j$ assumes that trial $j$ is the same simulated future in every
contract's YELT. For Caldera Re it is: every cedent's YELT is sampled from
[one simulation](https://reinsurance.dev/toolkit/trials/#what-is-a-trial), over one event catalog and one set of
trials, so Trial 9 in SunCoast's YELT and Trial 9 in Baltica's are the same simulated future.
A `trial_id` identifies a trial within one simulation and its trial numbering; an `event_id`
identifies an event within one event catalog. Two simulations (two vendors' models, and usually
two versions of one model) number their trials independently, so equal trial ids from them do
not name the same trial: summing them adds losses from unrelated futures, and nothing in the
rows shows it. The roll-up is a sum over loss sets on one simulation basis;
[portfolio analytics](https://reinsurance.dev/applications/portfolio-analytics/#at-production-scale) covers what that
requires of a production portfolio.

```mermaid
flowchart LR
    G["Contract gross<br/>C_i(L_j)<br/>(100% layer)"]:::Losses --> S["× participation<br/>p_i"]:::Process
    S --> A["Σ over contracts"]:::Process
    A --> YLT["Portfolio loss table<br/>(YLT or YELT)"]:::Losses
    YLT --> EP["Portfolio<br/>EP curves"]:::Metric
    EP --> M["EL · VaR · TVaR"]:::Metric

    classDef Losses fill:#F5CEBC,stroke:#B16C4B,stroke-width:1.5px,color:#2B2620
    classDef Process fill:#BFDBF8,stroke:#4F84BA,stroke-width:1.5px,color:#2B2620
    classDef Metric fill:#BDE2CB,stroke:#459269,stroke-width:1.5px,color:#2B2620
```

Why participation before the sum? Because the portfolio is Caldera Re's own position,
and Caldera Re only carries its share of each contract. Summing the 100% layers first
would roll up losses that belong to the other panel members. Scale each contract to the
written share, then combine. Reverse the order and the portfolio distribution is wrong by
whatever the rest of the panel wrote.

Roll-up is a named task (often conflated with portfolio analytics
itself) for two reasons beyond the arithmetic. Operationally, the constituents rarely sit in one system.[^rollup-ops] Business units manage their own portfolios, sometimes on different
models and in different data formats, and assembling an enterprise-wide portfolio is a
reconciliation exercise before it is a sum. Computationally, the reduction is big: tens of thousands of contract
YELTs across [10,000 to 1,000,000+ trials](https://reinsurance.dev/toolkit/trials/#demo-scale), often broken out by line of business and other exposure
characteristics, reduced to one loss table. That process is I/O-bound and not instant,
which is why the roll-up is often scheduled, monitored, and treated as a pipeline stage
of its own,[^rollup-scale] although the vendor pitch has moved to continuous, on-demand
roll-up.[^live-view]
**Portfolio analytics** is the business requirement; the roll-up is its prerequisite.

:::caution[Watch Out]
The roll-up is a sum of losses **within the same trial**, not a sum of metrics. It is
tempting to combine contracts by adding each one's [expected loss (EL)](https://reinsurance.dev/glossary/#el-expected-loss), [Value at Risk (VaR)](https://reinsurance.dev/glossary/#var-value-at-risk), or [Tail Value at Risk (TVaR)](https://reinsurance.dev/glossary/#tvar-tail-value-at-risk): the numbers are right
there. EL survives that shortcut; the tail metrics do not (the reason is [two sections below](https://reinsurance.dev/financial-modelling/portfolios/#expected-loss-adds-tail-risk-diversifies)).
Easy to overlook once the per-contract metrics are
already computed: roll up at the loss level, then compute metrics on the combined column.
:::

The sum itself can be taken per trial or per occurrence, and
[the two resolutions](https://reinsurance.dev/financial-modelling/portfolios/#two-resolutions-trial-and-occurrence) produce different tables.

## Two resolutions: trial and occurrence

What the roll-up produces depends on the resolution of the sum, and the distinction
matters enough to name the outputs.

**Trial level → a year loss table (YLT).** Sum each contract's participated losses per trial and the
result has one row per trial: `trial_id, loss`. That structure is a **[YLT](https://reinsurance.dev/glossary/#ylt-year-loss-table)**, not a YELT, because the roll-up chose to drop the occurrence detail (`timestamp`, `event_id`).
A YLT is all the [aggregate exceedance probability (AEP) view](https://reinsurance.dev/toolkit/distributions/) needs, and it is the fast path when
aggregate metrics are the only consumers.

**Occurrence level → a YELT.** Keep the occurrence key, sum the participated losses per
occurrence (an occurrence key shared by several cedents' YELTs from one simulation and trial numbering names one
simulated occurrence, so its losses add), and the result is a genuine
**portfolio YELT**: the portfolio's loss for every simulated catastrophe. This is the roll-up
that supports [occurrence exceedance probability (OEP) metrics](https://reinsurance.dev/toolkit/distributions/) (the distribution of the single
worst event) and any downstream contract with occurrence terms. The constituents arrive
ready for it: every contract's output is itself a YELT, aggregate components included.
The [aggregate excess](https://reinsurance.dev/financial-modelling/terms/aggregate-excess/) attributes its gross
to occurrences by [erosion](https://reinsurance.dev/glossary/#erosion), the per-occurrence audit trail the
[aggregate excess of loss (AggXoL) page](https://reinsurance.dev/financial-modelling/contracts/aggxol/) walks through.

Occurrence resolution is the safer default, and this site's recommendation: OEP metrics need occurrence losses,[^resolution] and a
portfolio protected by [retrocession](https://reinsurance.dev/glossary/#retrocession) with occurrence terms
cannot be evaluated without them. The trial-level YLT remains useful twice over: as the cheap
roll-up when only AEP metrics are needed, and as the simpler structure to reason about. Reducing a YELT to a YLT is lossy, irreversible,
and always a choice: no [financial term](https://reinsurance.dev/glossary/#term-financial-term) outputs a YLT (every
term, the aggregate excess included, produces occurrence rows), so the reduction happens only
where a consumer asks for it, and the trial-level roll-up is the usual place. When in doubt,
roll up at occurrence resolution and collapse later.

| | Trial-level roll-up → YLT | Occurrence-level roll-up → portfolio YELT |
| --- | --- | --- |
| Schema | `trial_id, loss` | `trial_id, timestamp, event_id, loss` (plus the enrichment columns) |
| Sum taken per | trial | occurrence: a `(trial_id, timestamp, event_id)` key shared by cedents' YELTs from one simulation and trial numbering; its losses add |
| Supports | AEP curve, EL, VaR, TVaR | Everything the YLT supports, plus OEP metrics and any downstream contract with occurrence terms |
| Rows | $N$ | one per portfolio occurrence (1,113 on the demo tier) |
| Reducible to the other | No: the occurrence detail cannot be recovered | Yes: a groupby-sum per trial gives the YLT exactly |
| Use it when | Only aggregate metrics are needed, and speed matters | Retrocession, OEP, or slicing within contracts is in view (the production default) |

[Caldera Re's portfolio](https://reinsurance.dev/financial-modelling/portfolios/#caldera-res-portfolio-rolled-up) is rolled up at both resolutions, and the
occurrence-level result collapses to the trial-level one exactly.

## Caldera Re's portfolio, rolled up

Caldera Re's six base contracts, at the shares it writes:

*[Table: Portfolio table. See the web page: https://reinsurance.dev/financial-modelling/portfolios/]*

Running the roll-up across the locked [20-trial demo tier](https://reinsurance.dev/toolkit/trials/#demo-scale) (at both resolutions) gives the
portfolio's loss distribution, expressed as a pair of [exceedance probability (EP) curves](https://reinsurance.dev/glossary/#ep-curve-exceedance-probability-curve). The tail figures below rest on two trials (VaR(90%) is the second-worst of twenty and TVaR(90%) averages the worst two), so they demonstrate the roll-up's mechanics and do not estimate a capital amount; a production roll-up runs over 10,000 to 1,000,000+ trials, and the [1,000-trial tier](https://reinsurance.dev/toolkit/trials/#demo-scale) shows the same quotes resting on many more trials.

*Figure (interactive on the web page): The portfolio's EP curves from the roll-up; hover for each curve's loss at any return period the twenty trials resolve. AEP uses the trial aggregate: the 1-in-10-year loss is \$22.08M, the second-worst of twenty trials. OEP uses the largest single occurrence per trial from the occurrence-level roll-up: its 1-in-10 loss is \$5.00M. The flat OEP top is the SunCoast backstop's participated share concentrating on the occurrence that pushes its aggregate layer through.*

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

```python
"""Caldera Re's portfolio roll-up: from six contracts to one portfolio loss distribution.

The roll-up is the mechanical prerequisite of portfolio analytics: reduce the
constituents' ceded losses, taken at each contract's participation share, to a
single portfolio loss distribution. The only genuinely new step versus the
single-program roll-up (`program_ceded.py`) is that here the constituents span
*different cedents*, so their tails land in different trials and the aggregate
diversifies harder.

Two resolutions:

    Trial level (AEP)      -- scale each contract's per-trial gross by p_i and
                              sum per trial -> a portfolio YLT (year loss
                              table: one loss per trial, occurrence detail gone)
    Occurrence level (OEP) -- take each contract's occurrence-resolution
                              output (aggregate layers arrive already
                              erosion-attributed), scale by p_i, and sum per
                              occurrence -> a portfolio YELT that supports
                              OEP metrics and occurrence-term retrocession

Metrics (EL, VaR, TVaR) are then read straight off the portfolio's EP curves
with the same Toolkit helpers used everywhere else. Expected loss is additive
across contracts; tail risk is not -- the gap between the portfolio's TVaR and
the sum of the constituents' TVaRs is the diversification benefit.

Reproduces the numbers in the Financial Modelling *Portfolios* page and the
Applications *Portfolio analytics* story from the locked 20-trial YELT.
"""

import numpy as np
import pandas as pd

from caldera_re.aep import aep
from caldera_re.aggregate_excess import aggregate_excess
from caldera_re.aggxol import aggxol
from caldera_re.catxol import catxol
from caldera_re.data import cedents
from caldera_re.expected_loss import expected_loss
from caldera_re.loss_filter import loss_filter
from caldera_re.program_ceded import suncoast_program
from caldera_re.quota_share import quota_share
from caldera_re.contract_period import contract_period
from caldera_re.report import fields, heading, table
from caldera_re.tvar import tvar
from caldera_re.var import var

N = 20          # locked trial count of the Caldera Re demo data
ALPHA = 0.90    # confidence level for the tail metric

# Each contract carries Caldera Re's participation share. Premiums, capital,
# and pricing live in portfolio_analytics.py -- the roll-up itself is purely
# a loss computation.
CONTRACTS = {
    'C1': {'cedent': 'SunCoast', 'peril': 'HU', 'region': 'US',
           'participation': 0.145},
    'C2': {'cedent': 'SunCoast', 'peril': 'EQ', 'region': 'US',
           'participation': 0.20},
    'C3': {'cedent': 'SunCoast', 'peril': 'Multi', 'region': 'US',
           'participation': 0.10},
    'C4': {'cedent': 'PacificMutual', 'peril': 'EQ', 'region': 'US',
           'participation': 0.25},
    'C5': {'cedent': 'Baltica', 'peril': 'WS', 'region': 'EU',
           'participation': 0.30},
    'C6': {'cedent': 'AtlasCatFund', 'peril': 'EQ', 'region': 'JP',
           'participation': 0.15},
}

def _per_trial(result: pd.DataFrame, column: str = 'gross_loss') -> np.ndarray:
    """Collapse a contract result into a length-N per-trial gross array."""
    arr = np.zeros(N)
    for tid, value in result.groupby('trial_id')[column].sum().items():
        arr[int(tid) - 1] = value
    return arr

def contract_gross() -> dict:
    """Per-trial ceded loss at the 100% layer for all six base contracts.

    C1-C3 come from SunCoast's inuring program (C3's subject is net of C1 and
    C2 recoveries); C4-C6 are
    independent single contracts on their own cedents.
    """
    prog = suncoast_program()
    c4 = _per_trial(catxol(cedents.pacific, attachment=3.0, limit=10.0,
                           reinstatements=1, peril='EQ', geography_id='CA'))
    c5 = _per_trial(quota_share(cedents.baltica, cession_percentage=0.25,
                                peril='WS'))
    c6 = _per_trial(catxol(cedents.atlas, attachment=20.0, limit=30.0,
                           reinstatements=2, peril='EQ', geography_id='JP'))
    return {'C1': prog['C1'], 'C2': prog['C2'], 'C3': prog['C3'],
            'C4': c4, 'C5': c5, 'C6': c6}

def _curve(arr: np.ndarray) -> pd.DataFrame:
    """Wrap a per-trial array as an AEP curve for the metric helpers."""
    return aep(pd.DataFrame({'trial_id': range(1, N + 1), 'loss': arr}), n_trials=N)

def metrics(arr: np.ndarray, alpha: float = ALPHA) -> dict:
    """EL, VaR, and TVaR for a per-trial loss array."""
    curve = _curve(arr)
    return {'el': expected_loss(curve), 'var': var(curve, alpha),
            'tvar': tvar(curve, alpha)}

def roll_up() -> dict:
    """Roll the six contracts into Caldera Re's portfolio.

    Returns per-contract participated arrays, the portfolio array, and the
    diversification decomposition (sum-of-standalone vs portfolio).
    """
    gross = contract_gross()
    participated = {c: gross[c] * CONTRACTS[c]['participation'] for c in gross}
    portfolio = sum(participated.values())

    standalone = {c: metrics(participated[c]) for c in participated}
    port = metrics(portfolio)

    sum_el = sum(s['el'] for s in standalone.values())
    sum_tvar = sum(s['tvar'] for s in standalone.values())

    return {
        'gross': gross,
        'participated': participated,
        'portfolio': portfolio,
        'standalone': standalone,
        'port': port,
        'sum_el': sum_el, 'sum_tvar': sum_tvar,
        'diversification_tvar': sum_tvar - port['tvar'],
    }

# --- Occurrence-level roll-up (OEP resolution) ------------------------------
#
# The trial-level roll-up produces a YLT -- one loss per trial -- which is all
# the AEP view needs. OEP metrics and occurrence-term retrocession need the
# portfolio's losses per *occurrence*. The contract outputs already carry that
# resolution: the aggregate excess term attributes its gross to occurrences by
# erosion (claims order), so the catalog helpers return YELTs and the roll-up
# only has to reduce each one to the occurrence key.

_KEYS = ['trial_id', 'timestamp', 'event_id']

def _by_occurrence(result: pd.DataFrame,
                   loss_column: str = 'gross_loss') -> pd.DataFrame:
    """Reduce a contract's YELT output to the occurrence key."""
    return result.groupby(_KEYS)[loss_column].sum().reset_index()

def _occurrences(yelt: pd.DataFrame, **criteria) -> pd.DataFrame:
    """Covered per-occurrence subject losses (filter + period + group)."""
    covered = contract_period(loss_filter(yelt, **criteria),
                            '2024-01-01', '2024-12-31')
    return covered.groupby(_KEYS)['loss'].sum().reset_index()

def occurrence_gross() -> dict:
    """Per-occurrence gross at the 100% layer for all six contracts.

    Each entry is a DataFrame [trial_id, timestamp, event_id, gross_loss]
    whose per-trial sums reproduce contract_gross() exactly.
    """
    sc = cedents.suncoast

    # C1/C4/C6 (CatXoL) and C2 (AggXoL): the helpers' YELT outputs, reduced
    # to the occurrence key. The aggregate components (CatXoL caps, AggXoL
    # layers) are already erosion-attributed by the aggregate excess term.
    c1 = _by_occurrence(catxol(sc, attachment=10.0, limit=30.0,
                               reinstatements=1, peril='HU',
                               geography_id='FL'))
    c2 = _by_occurrence(aggxol(sc, attachment=15.0, limit=10.0, peril='EQ'))
    c4 = _by_occurrence(catxol(cedents.pacific, attachment=3.0, limit=10.0,
                               reinstatements=1, peril='EQ',
                               geography_id='CA'))
    c6 = _by_occurrence(catxol(cedents.atlas, attachment=20.0, limit=30.0,
                               reinstatements=2, peril='EQ',
                               geography_id='JP'))

    # C5 (quota share): proportional, so attribution is trivial.
    c5 = _occurrences(cedents.baltica, peril='WS')
    c5['gross_loss'] = c5.pop('loss') * 0.25

    # C3 (AggXoL net of C1 and C2): the subject per occurrence is SunCoast's
    # total occurrence loss net of what C1 and C2 recovered on that occurrence,
    # then the 50 xs 300 aggregate layer.
    subject = _occurrences(sc)
    net = (subject
           .merge(c1.rename(columns={'gross_loss': 'c1'}), on=_KEYS, how='left')
           .merge(c2.rename(columns={'gross_loss': 'c2'}), on=_KEYS, how='left')
           .fillna({'c1': 0.0, 'c2': 0.0}))
    net['loss'] = net['loss'] - net['c1'] - net['c2']
    c3 = _by_occurrence(aggregate_excess(net[_KEYS + ['loss']], 300.0, 50.0))

    return {'C1': c1, 'C2': c2, 'C3': c3, 'C4': c4, 'C5': c5, 'C6': c6}

def occurrence_roll_up() -> dict:
    """Roll the portfolio up at occurrence resolution -> the portfolio YELT.

    Scales each contract's per-occurrence gross by its participation and sums
    per occurrence (shared event_ids add across cedents). Returns the portfolio
    YELT, its per-trial OEP maxima, and verifies that collapsing it to trial
    level reproduces the trial roll-up exactly.
    """
    occ = occurrence_gross()
    parts = []
    for c, df in occ.items():
        scaled = df.copy()
        scaled['gross_loss'] *= CONTRACTS[c]['participation']
        parts.append(scaled)
    yelt = (pd.concat(parts)
            .groupby(_KEYS)['gross_loss'].sum().reset_index()
            .rename(columns={'gross_loss': 'loss'}))

    aep_by_trial = (yelt.groupby('trial_id')['loss'].sum()
                    .reindex(range(1, N + 1), fill_value=0.0).to_numpy())
    oep_by_trial = (yelt.groupby('trial_id')['loss'].max()
                    .reindex(range(1, N + 1), fill_value=0.0).to_numpy())

    # Consistency: the portfolio YELT collapsed per trial must equal the
    # trial-level roll-up (and each contract's attribution its trial gross).
    trial = roll_up()
    assert np.allclose(aep_by_trial, trial['portfolio']), \
        'occurrence roll-up does not collapse to the trial roll-up'
    for c, df in occ.items():
        per_trial = (df.groupby('trial_id')['gross_loss'].sum()
                     .reindex(range(1, N + 1), fill_value=0.0).to_numpy())
        assert np.allclose(per_trial, trial['gross'][c]), \
            f'{c} occurrence attribution does not sum to its trial gross'

    return {'yelt': yelt, 'aep_by_trial': aep_by_trial,
            'oep_by_trial': oep_by_trial}

def trial_occurrences(j: int) -> pd.DataFrame:
    """Trial j's occurrences in date order: one row per occurrence key, one
    column per contract at Caldera Re's participation, and their total."""
    occ = occurrence_gross()
    columns = []
    for c, df in occ.items():
        rows = df[df['trial_id'] == j].set_index(_KEYS)['gross_loss']
        columns.append((rows * CONTRACTS[c]['participation']).rename(c))
    wide = pd.concat(columns, axis=1).fillna(0.0)
    wide['total'] = wide[list(occ)].sum(axis=1)
    return wide[wide['total'] > 0].sort_index().reset_index()

if __name__ == '__main__':
    r = roll_up()

    heading("Per-contract: 100% layer and participated ($M)")
    rows = []
    for c in CONTRACTS:
        s100 = metrics(r['gross'][c])
        s = r['standalone'][c]
        rows.append((c, CONTRACTS[c]['participation'], s100['el'],
                     s100['tvar'], s['el'], s['tvar']))
    table(['C', 'part', 'EL100', 'TVaR100', 'pEL', 'pTVaR'], rows,
          fmt=['{}', '{:.1%}', '{:.2f}', '{:.2f}', '{:.2f}', '{:.2f}'])

    p = r['port']
    print()
    fields([('Portfolio EL', p['el']),
            ('Portfolio VaR90', p['var']),
            ('Portfolio TVaR90', p['tvar'])], fmt='{:.2f}M', sep=' = ')

    print(f"\nSum standalone EL   = {r['sum_el']:.2f}M   "
          f"portfolio EL = {p['el']:.2f}M  (adds exactly)")
    print(f"Sum standalone TVaR = {r['sum_tvar']:.2f}M   "
          f"portfolio TVaR = {p['tvar']:.2f}M")
    print(f"Diversification benefit (TVaR) = {r['diversification_tvar']:.2f}M "
          f"({r['diversification_tvar']/r['sum_tvar']:.1%} of the sum)")

    # The tail: which contracts drive the worst trials?
    print()
    heading("Tail structure: worst trials of the participated portfolio")
    order = np.argsort(r['portfolio'])[::-1]
    table(['Trial', 'Total', *CONTRACTS],
          [(idx + 1, r['portfolio'][idx],
            *(r['participated'][c][idx] for c in CONTRACTS))
           for idx in order[:4]],
          fmt=['{}'] + ['{:.2f}'] * (1 + len(CONTRACTS)))

    # Occurrence-level roll-up: the portfolio YELT and its OEP view.
    o = occurrence_roll_up()
    om = metrics(o['oep_by_trial'])
    print()
    heading("Occurrence roll-up: portfolio YELT (OEP resolution)")
    print(f"Portfolio YELT rows (occurrences): {len(o['yelt'])}")
    fields([('OEP EL', om['el'], 'mean worst-occurrence loss'),
            ('OEP VaR90', om['var']),
            ('OEP TVaR90', om['tvar'])], fmt='{:.2f}M', sep=' = ')
    worst = o['yelt'].sort_values('loss', ascending=False).head(3)
    print("Largest portfolio occurrences:")
    for _, row in worst.iterrows():
        print(f"  trial {int(row['trial_id']):2d}  event {int(row['event_id'])}"
              f"  {row['timestamp']}  {row['loss']:.2f}M")
    print("(collapsing the portfolio YELT per trial reproduces the "
          "trial-level roll-up exactly -- asserted above)")

    # One trial, occurrence by occurrence: the worst trial's total is the
    # sum of many occurrences, none of them large.
    t9 = trial_occurrences(9)
    print()
    heading("Trial 9, occurrence by occurrence, at participation ($M)")
    table(['Date', 'Event', *CONTRACTS, 'Total'],
          t9[['timestamp', 'event_id', *CONTRACTS, 'total']],
          fmt=['{}', '{}'] + ['{:.2f}'] * (len(CONTRACTS) + 1))
    fields([('Occurrences', f"{len(t9)}"),
            ('Largest occurrence', t9['total'].max()),
            ('Trial 9 total', t9['total'].sum())], fmt='{:.2f}M')
```

The metrics are computed from those curves with the [same helpers](https://reinsurance.dev/toolkit/metrics/) used
everywhere else on the site (nothing about a portfolio distribution is special once it
exists):

| Portfolio metric (AEP basis) | Value |
| --- | ---: |
| Expected loss (EL) | \$14.59M |
| VaR(90%) | \$22.08M |
| TVaR(90%) | \$23.51M |

On the OEP basis the 1-in-10 single-occurrence loss is \$5.00M. The occurrence-level
roll-up that produced it collapses back to the trial-level numbers exactly (the code
above asserts it), which is the practical consequence of "lossy in one direction only."

The portfolio's TVaR(90%) of \$23.51M is below the \$36.54M that the six contracts' standalone
TVaRs sum to, and [diversification](https://reinsurance.dev/financial-modelling/portfolios/#expected-loss-adds-tail-risk-diversifies) accounts for the gap.

## Expected loss adds; tail risk diversifies

Diversification makes the rolled-up distribution more than bookkeeping. Take
the six contracts' standalone metrics (each computed on its own participated loss)
and compare the sum against the portfolio's:

*[Interactive figure: Portfolio diversification chart. See the web page: https://reinsurance.dev/financial-modelling/portfolios/]*

Expected loss adds **exactly**. The six contracts' participated ELs sum to \$14.59M, the
portfolio EL to the cent, because expectation is linear: trial by trial the portfolio loss
is the sum of the contract losses, so the means add. Structure is invisible to EL: it
does not matter which cedent, peril, or region a dollar of expected loss comes from.

The tail does not add. The contracts' standalone TVaR(90%)s sum to \$36.54M, but the
portfolio's TVaR(90%) is \$23.51M, **\$13.02M lower**,[^rounding] a 35.6% reduction. The reason is
structural: each contract's TVaR averages its own worst trials, but the portfolio's TVaR
averages the trials where the total is worst, and those are different trials. SunCoast's
Florida hurricane peaks in one trial, Baltica's European windstorm in another, Atlas's
Japanese quake in a third. They do not all fail together, so the portfolio's worst trial is
milder than the sum of each contract's worst trial. That gap is the
**[diversification benefit](https://reinsurance.dev/glossary/#diversification-benefit)**, and it is exactly
[TVaR's sub-additivity](https://reinsurance.dev/toolkit/metrics/var-vs-tvar/#sub-additivity), $\text{TVaR}(A + B) \le
\text{TVaR}(A) + \text{TVaR}(B)$, realised across the whole portfolio.

:::tip[Key Insight]
The diversification benefit is a property of the combined loss column, and of nothing
else. No standalone view of the contracts contains it, because it is in how their bad trials interleave, information that only exists once the losses share a trial index. That
is why the roll-up is the prerequisite of every portfolio-level question: the quantity the
business cares most about does not exist until the sum is taken. What that
\$13.02M of tail reduction is worth (in [capital](https://reinsurance.dev/applications/standalone-pricing/#step-3-from-metrics-to-premium), and to whom) is a pricing question, taken up in
the [portfolio analytics story](https://reinsurance.dev/applications/portfolio-analytics/).
:::

A reinsurer can reduce the tail further with
[retrocession](https://reinsurance.dev/financial-modelling/portfolios/#the-portfolio-yelt-is-also-a-subject-loss), which takes the portfolio YELT as its
subject loss.

## The portfolio YELT is also a subject loss

The portfolio YELT has the same schema as any other loss table, so it can serve as another contract's **subject
loss**. A reinsurer buys [retrocession](https://reinsurance.dev/glossary/#retrocession) (reinsurance on
reinsurance) for two reasons: to protect its portfolio, and to free up capacity so that more business
can be written against the same capital base.[^retro-why]
Either way, the retro contract's subject loss YELT may be precisely the portfolio YELT
this roll-up produces. The retro layer transforms that subject loss into the retro's
gross loss (the recoveries the reinsurer collects), and netting those recoveries out of
the portfolio YELT gives the portfolio's **[net of retro](https://reinsurance.dev/glossary/#net-loss)** loss. The
[perspectives](https://reinsurance.dev/financial-modelling/perspectives/) chain (subject loss,
gross loss, net loss) reappears one level up, with the whole portfolio playing the role
a cedent's exposure set played before. Resolution matters here too: a retro program with
occurrence terms needs the portfolio loss at YELT resolution, not as a YLT. That is another
reason the occurrence-level roll-up is the one production systems keep.

```mermaid
graph LR
    P["Portfolio YELT<br/>the roll-up's output"]:::Losses -- "is the subject loss of" --> R["Retro contract<br/>a composition of the same terms"]:::Contract
    R -- "gross loss" --> G["Retro recoveries"]:::Metric
    P -- "minus recoveries" --> N["Portfolio net of retro"]:::Metric
    G --> N

    classDef Losses fill:#F5CEBC,stroke:#B16C4B,stroke-width:1.5px,color:#2B2620
    classDef Contract fill:#BFDBF8,stroke:#4F84BA,stroke-width:1.5px,color:#2B2620
    classDef Metric fill:#BDE2CB,stroke:#459269,stroke-width:1.5px,color:#2B2620
```

The retro program itself (its layers, its price, what it is worth to Caldera Re) is out of scope for this page. A retro program can protect one slice of the portfolio, and [a flattened portfolio](https://reinsurance.dev/financial-modelling/portfolios/#in-practice-portfolios-are-flattened) makes any slice's roll-up a filter and a sum.

## In practice: portfolios are flattened

Conceptually a portfolio is the tidy tree above: programs containing contracts containing
terms. In production it is usually flattened into a single list of participated
layers, each tagged with the metadata that lets you slice it back apart: cedent, peril,
region, program, business unit. The tree is how humans reason about a placement; the flat
list is how an engine evaluates it. The two views answer the same questions differently:

| | The tree | The flat list |
| --- | --- | --- |
| Unit | Portfolio → program → contract → term | One participated layer per row, tagged with cedent, peril, region, program, business unit |
| How a portfolio is defined | By membership: which programs it holds | By a filter over the tags: `business_unit = US and status = bound` |
| How a slice is computed | Walk the subtree, then roll up | Filter the rows, then roll up |
| What it is for | Reasoning about a placement; the structure a person negotiates | Evaluating at speed; the structure an engine iterates |
| Cost of a new slice | A new subtree to maintain | A new predicate, no new object |

Both views describe the same object, and a well-built
model moves between them freely: evaluate the flat list for speed, group by any tag to
recover the structure. The grouping keys are also what define portfolios themselves: "the
live bound US portfolio" is a filter over the flat list, not a separate object. They are what
let a portfolio manager ask "how much of my tail is Californian earthquake?" without
re-evaluating a single contract. The slice still needs its own roll-up over the selected
constituents, but that is a filter and a sum over losses already computed, not a
re-evaluation of the contracts.

:::note[Consider]
The roll-up is where the **portfolio-first** design principle matters. If a system
treats the portfolio as an afterthought (contracts evaluated in isolation, results added afterwards), it cannot see the diversification benefit at all, because that benefit only
exists in the combined loss column. A model built to roll up from day one gets portfolio
distributions, slicing, and (with one more contract) marginal impact for free. A model
that keeps only per-contract results has discarded the combined loss column, so every
portfolio-level question forces it to re-evaluate the constituents from scratch, work the
portfolio-first design never repeats. Design for the sum, not only the parts.
:::

The [portfolio analytics story](https://reinsurance.dev/applications/portfolio-analytics/#step-4-slice-the-portfolio)
slices this portfolio by region, peril, and cedent, each slice a selection rolled up again.

## What carries forward

A portfolio is an actively managed selection of written programs and contracts. The roll-up (**scale each contract to its participation, sum within each trial or
occurrence**) reduces it to one loss distribution: a YLT at trial resolution for AEP, a
portfolio YELT at occurrence resolution for OEP and retrocession. Even those
distributions are intermediaries: the business questions they serve are the portfolio
KPIs (how much can the portfolio lose, and where is its risk concentrated). Expected loss rolls up by addition. Tail risk rolls up below addition, and the gap between the
sum of the standalone tails and the combined tail is the diversification benefit, a
quantity that exists only in the combined column.

That closes the Financial Modelling chapter: from one term, to a contract, to a program,
to the whole portfolio. And it settles the chapter's claim. **A handful of operations on
trial losses can express the loss side of the core property-cat structures (quota share,
catastrophe excess of loss, aggregate excess of loss) and of programs and portfolios built
from them; other structures add terms to the catalog.**
Five terms built three contract types; one relationship between contracts built a program;
one sum built the portfolio, and the same five terms apply to its loss table again as a retro
contract's subject. What the chapter builds, the [Applications](https://reinsurance.dev/applications/)
chapter uses. The [portfolio analytics story](https://reinsurance.dev/applications/portfolio-analytics/) takes
this exact portfolio and works it as a portfolio manager would: assessing its risk posture, slicing
it for concentrations, and weighing its expected loss against the premium it earns. And
pricing one new contract against the rolled-up portfolio, the marginal view, is **marginal pricing with portfolio view** *(planned)*, the application the Applications chapter builds toward.

[^rounding]: The difference is taken on the unrounded tails and then displayed to two decimals; the two displayed figures differ by \$13.03M. See the rounding convention in the [Caldera Re appendix](https://reinsurance.dev/caldera-re/#portfolio).

[^portfolio-slices]: Verisk, ["Touchstone Re"](https://www.verisk.com/4a2ccc/siteassets/media/downloads/catastrophe-and-risk-solutions/touchstone-re.pdf), product brochure, 2025. Quantifies risk "on a global, regional, peril, and line of business level" and filters a portfolio to "treaties contained in your In-Force book of business or in the status of submitted, renewals, quoted, expired", which is the cut by line of business and by binding status; the cut by contract period is this site's. International Actuarial Association, [*Risk Book: Catastrophe Risk*](https://actuaries.org/app/uploads/2025/10/IAARiskBook_CatastropheRisk_2025_10.pdf), Insurance Regulation Committee, October 2025, sets risk-transfer decisions "at the level of a single location or account, or at a business unit or a corporate aggregate portfolio level". The *live bound* and *live working* names are this site's.

[^rollup-ops]: Moody's, ["Four strategies to enhance reinsurance exposure management"](https://www.moodys.com/web/en/us/insights/insurance/four-strategies-to-enhance-reinsurance-exposure-management.html), February 2025. Reinsurers "routinely manage vast amounts of cedant exposure data spread across numerous databases", so a business-wide roll-up "typically involves restoring and attaching cedant databases individually, then running accumulations on a portfolio, and combining each for an aggregate view of risk", with multiple tools and systems making it "significantly less efficient and more time-intensive". The piece describes exposure accumulation and attributes the scatter to cedant databases; the business-unit attribution is this site's.

[^rollup-scale]: Blesson Varghese and Andrew Rau-Chaplin, ["Data Challenges in High-Performance Risk Analytics"](https://arxiv.org/abs/1311.5685), Risk Analytics Lab, Dalhousie University, 2013. Splits reinsurance analytics into a three-stage pipeline whose middle stage is portfolio risk management, puts a reinsurer at "tens of thousands of contracts", finds that at that stage data "needs to be scanned over rather than randomly access[ed]", and reports that the volume means "a weekly simulation can be performed with limited possibility for a real-time simulation". The same paper times a single contract's million-trial simulation at 25 seconds, which is the per-contract figure behind "not instant".

[^live-view]: Moody's, "Four strategies to enhance reinsurance exposure management", on the vendor pitch: a cloud-native platform gives reinsurers "24/7 access to real-time analytics" and lets exposure-based rollups "be carried out more frequently". It is a product claim, and it evidences what is marketed rather than what reinsurers do.

[^resolution]: International Actuarial Association, *Risk Book: Catastrophe Risk*, October 2025. Catastrophe models "typically generate two types of EP curves", the occurrence distribution being "the probabilities of losses from the maximum occurrence in a year", and where the underlying loss data "is developed in a year, event loss format" the aggregate view is "a simple aggregation exercise" of it, while estimating the aggregate distribution by other routes is "more challenging to estimate and less robust than the OEP distribution".

[^retro-why]: Guy Carpenter, [Glossary of Reinsurance Terms](https://www.guycarp.com/company/news-and-events/glossary.html), *Retrocession*: it "is placed to afford additional capacity to the original reinsurer or to contain or reduce the original reinsurer's risk of loss". RAA, [*Fundamentals of Reinsurance and Glossary of Reinsurance Terms*](https://www.reinsurance.org/Common/Uploaded%20files/Fundamentals/FundamentalsofReinsuranceandGlossaryofReinsuranceTerms.pdf), records that reinsurers buy retrocession "in the same forms and for the same reasons as ceding insurers", among them increased capacity, which it explains through the surplus a company commits as it writes more business.
