Applications · Section 4
Portfolio analytics
The portfolio manager measures the risk posture of Caldera Re's whole portfolio, slices it for concentrations, and weighs its expected loss against the premium it earns. The roll-up is the prerequisite; the analytics are the objective. Diversification is measured here rather than asserted.
The business problem
Section titled “The business problem”The risk-profiling and standalone-pricing applications each covered a single deal: the post-model report characterised one cedent’s losses; one program’s marketed premium was compared with its technical premium. Both deliberately left the rest of Caldera Re’s portfolio out of scope. The portfolio manager answers questions about everything Caldera Re has written, not about any one contract:
What is the risk posture of the whole portfolio: how much can it lose, where is that risk concentrated, and is it earning enough premium for its risk?
The portfolio manager reports the answers, advises underwriters about gaps and opportunities in the portfolio, and runs pro forma analyses of speculative future business to see how the portfolio’s metrics would change.
That is portfolio analytics: it produces the metrics, views, and quantified relationships from which the business makes informed strategic and tactical decisions. Its prerequisite is the roll-up, the mechanical task that reduces the portfolio’s losses to one distribution. The pipeline starts with that roll-up; the analytics built on it are the objective. One claim from standalone pricing is still untested. With C1’s reinstatement premium counted, SunCoast’s program returns 9.0% on its program capital, below Caldera Re’s 10% cost of capital; the claim is that business like it returns more on capital once it is combined with uncorrelated business, because it adds far less to the portfolio’s capital than it requires alone. Testing that claim means rolling up the six contracts’ losses into one distribution, measuring it, slicing the portfolio, and settling whether the portfolio, not any single deal, earns its cost of capital on the same basis.
Every figure the portfolio manager reads is a metric of one rolled-up distribution, so the first two steps of the pipeline produce it.
The pipeline
Section titled “The pipeline”Portfolio analytics runs as a pipeline of five steps, and every one reuses methods from the earlier chapters. Only the scope is new. The pipeline is deliberately generic: nothing in it is specific to Caldera Re, and it applies to a slice of the portfolio exactly as to the whole.
| Step | What happens | Built in |
|---|---|---|
| 1. Select the constituents | Define the portfolio: choose contracts (by business unit, line of business, binding status) at the reinsurer’s participations | Portfolios |
| 2. Roll up | Sum the participated losses per occurrence and per trial into the portfolio’s loss distribution | Portfolios |
| 3. Measure the risk posture | EL, VaR, TVaR, capital on the portfolio distribution (the diversification implicit in the portfolio view) | Metrics |
| 4. Slice | Repeat the pipeline on narrower selections to find concentrations | this page |
| 5. Weigh risk vs opportunity | Expected loss and capital cost against booked premium, per portfolio | this page |
Every later number is calculated for whichever contracts, at whichever shares, are in the selection.
Step 1: select the constituents
Section titled “Step 1: select the constituents”A portfolio is a selection, and to start with we select everything: Caldera Re’s whole enterprise portfolio (the six base contracts, each written at a participation share placed alongside a panel of other reinsurers). The same pipeline would apply unchanged to a narrower selection (one business unit’s contracts, one line of business, only bound deals). The risk-profiling and standalone-pricing applications covered the three SunCoast layers in detail; here they are three of six, alongside Pacific Mutual, Baltica, and Atlas Cat Fund:
| Contract | Cedent | Peril · region | Participation | Booked premium |
|---|---|---|---|---|
| C1 | SunCoast | Hurricane · US (FL) | 14.5% | $2.03M |
| C2 | SunCoast | Earthquake · US (CA/AZ) | 20% | $1.00M |
| C3 | SunCoast | All-US backstop (net of C1, C2) | 10% | $1.30M |
| C4 | Pacific Mutual | Earthquake · US (CA) | 25% | $1.50M |
| C5 | Baltica | Windstorm · EU | 30% | $5.10M |
| C6 | Atlas Cat Fund | Earthquake · Japan | 15% | $1.95M |
| Portfolio | $12.88M |
The premium is Caldera Re’s own share: the 100% layer premium scaled by the participation, and the losses are scaled the same way. Caldera Re books $12.88M of premium for its participation in these six contracts, and on C1, C4 and C6 it also receives reinstatement premium in the trials where they pay. The risk taken on in return for that premium is the subject of the remaining steps, starting with the roll-up, which produces the loss distribution every later step is computed from.
Step 2: roll it up
Section titled “Step 2: roll it up”The roll-up is one operation: for each trial, take every contract’s gross loss at Caldera Re’s participation (the amount ceded to Caldera Re) and add them together. Do it across all 20 trials of the demo tier and the roll-up produces the portfolio’s loss distribution.
Caldera Re's portfolio rolled up trial by trial. Each bar stacks the six contracts' ceded loss at their participation shares; the full height is the portfolio's ceded loss in that trial. Hover a segment or a legend entry to follow one contract across all trials. The peaks fall in different trials — SunCoast's Florida hurricane (C1) reaches its cap in trials 9 and 19, Baltica's European windstorm (C5) in trial 5, Atlas's Japanese quake (C6) in trial 14 — so the contracts' worst losses never coincide in one trial. That staggering is the diversification the metrics below put a number on.
The peaks are staggered across trials. The portfolio’s worst trial (trial 9, at $24.9M) is dominated by Florida hurricane: C1 pays its full $8.7M capped share and Baltica’s windstorm contributes a further $7.7M, while the SunCoast All-US backstop does not contribute at all.
Trial 9, the portfolio's worst trial, split into its 24 occurrences in date order. Each bar stacks the contracts that pay on that occurrence, at Caldera Re's participation, in the roll-up chart's colours. Hover a segment or a legend entry to follow one contract. The largest occurrence is $4.50M, below the dashed line at the portfolio's 1-in-10 OEP loss; the trial's $24.94M total is the sum of many occurrences, none of them large.
Trial 9’s $24.94M is the sum of 24 occurrences between February and December. The largest, Atlas’s Japanese earthquake on 9 September, is $4.50M at Caldera Re’s share; C1’s $8.7M comes from three hurricanes, and Baltica’s windstorm share from eleven storms. No occurrence in Trial 9 exceeds $5M, and no occurrence in any trial does, which is why the portfolio’s occurrence exceedance probability (OEP) curve stays at or below $5.00M while its trial totals reach $24.94M.
Trial 4’s loss, by contrast, is mostly C6’s (a $7.0M Japanese-quake share) with only a small loss from C1. No single trial contains every contract’s worst loss. Atlantic hurricane, European windstorm, and Japanese earthquake are treated as uncorrelated perils,2 so the probability that each peril’s rare large events fall in the same trial is very small, and the simulation reflects it.3 This separation across trials is quantified as a capital number in Step 3.
Step 3: the risk posture
Section titled “Step 3: the risk posture”With the portfolio’s losses reduced to one distribution, its exceedance probability (EP) curve and metrics are computed by exactly the method the Toolkit defines: a portfolio distribution is another loss distribution. The aggregate exceedance probability (AEP) curve uses trial totals, which the capital metrics read; the occurrence exceedance probability (OEP) curve uses each trial’s largest occurrence:
The portfolio's EP curves from the roll-up; hover for each curve's loss at any return period the twenty trials resolve. AEP is the trial-aggregate view the capital metrics below read from: the 1-in-10-year loss is $22.08M, and the dotted line marks the expected loss. The AEP curve is above it over most of its width. OEP is the largest single occurrence per trial. Its 1-in-10 loss is $5.00M.
| Portfolio metric (AEP) | Value | What it says |
|---|---|---|
| Expected loss (EL) | $14.59M | The portfolio’s loss in an average trial |
| Probability of a loss | 100% | At least one contract pays a claim in every trial |
| 60% | A worse-than-average trial, 12 of the 20 | |
| VaR(90%) | $22.08M | The 1-in-10-year loss, exceeded in one trial in ten |
| TVaR(90%) | $23.51M | The average of the worst 10% of trials |
| Capital | $8.92M | The unexpected loss, against which capital is held |
The capital row is the risk posture as a balance-sheet figure. Premium covers the expected loss (EL); capital is held against the unexpected loss, Tail Value at Risk (TVaR) at 90% less EL, : the average excess over expected loss across the worst 10% of trials. The same definition holds for a portfolio because expected loss adds. A portfolio of well-priced contracts earns enough premium to cover the portfolio’s EL, and capital is held against the portfolio’s own tail beyond it.
This is the risk posture of the portfolio: Caldera Re expects $14.6M of claims a year on it, and a worse-than-average trial is not rare, occurring in 12 of 20. A 1-in-10 year costs $22M, and the reinsurer must hold $8.9M of capital against the tail beyond the expected loss. Every one of these is computed from the portfolio EP curve above, by the method risk profiling and standalone pricing applied to a single cedent, now on the whole portfolio. The expected loss is also above the $12.88M of booked upfront premium: on that premium alone, the portfolio would lose money in an average trial. Step 5 adds the reinstatement premium, which reverses that.
"""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 roll-up and the metrics take any selection of contracts, which is all a slice of the portfolio requires.
Step 4: slice the portfolio
Section titled “Step 4: slice the portfolio”A single tail number is a summary; the portfolio manager’s next question is where the risk is concentrated. A slice is a narrower selection. Include or exclude whole contracts by business unit, line of business, or binding status, at the same participations, and roll up again. And because the portfolio year event loss table (YELT) preserves occurrence detail, a slice can also be cut within contracts by peril or region. Each subportfolio produced this way has its own metrics and its own risk-versus-opportunity assessment, which is how reinsurers actually manage: business units and lines of business are steered individually, each against its own objectives as well as the enterprise’s.
The simplest slice is expected loss along the contracts’ tags. EL is additive, so it can be sliced along any tag (region, peril, cedent), and the slices always sum to the portfolio’s $14.59M:
Where the portfolio's expected loss lives. Expected loss is additive, so every slice sums back to the portfolio's $14.59M — no diversification credit hides inside EL. By region the portfolio is roughly half US ($6.99M), a third European windstorm ($4.80M), a fifth Japanese quake ($2.81M); by peril, earthquake is the largest single exposure at 43%. The tail concentrates differently from EL, which is why a portfolio manager slices both.
By region, the portfolio’s EL is roughly half US, a third European windstorm, a fifth Japanese quake; by peril, earthquake is the single largest line at 43%, spread across three cedents and two continents. A portfolio that looks diversified by cedent can still be concentrated by peril: Caldera Re’s earthquake exposure spans SunCoast, Pacific Mutual, and Atlas, so a change to earthquake pricing or retrocession affects three placements at once. Slicing the same EL two ways shows that concentration.
Each slice is a subportfolio, so it is rolled up and measured on its own. The code below cuts the portfolio by region and by peril and reads EL, TVaR, and capital per subportfolio. Slice ELs sum to the enterprise EL along any axis, but slice TVaRs do not: by region they sum to $30.92M compared with the enterprise’s $23.51M, because measuring slices separately includes diversification within each slice but excludes diversification between slices.
"""Slicing Caldera Re's portfolio: subportfolios as narrower selections.
A slice is not a new operation. It is the same pipeline -- select
constituents, roll up, measure -- run on a narrower selection: include or
exclude whole contracts by a tag (here region and peril; in production also
business unit, line of business, binding status) at the same participations.
Each subportfolio gets its own metrics, because business units, lines of
business, and capital sources are managed individually.
Two lessons fall out of the numbers:
1. Expected loss is additive -- the slice ELs sum to the enterprise EL
exactly, along any axis.
2. Tail risk is not -- each slice keeps the diversification *within*
itself but loses the diversification *across* slices, so the slice
TVaRs sum to more than the enterprise TVaR (but less than the
standalone sum).
"""
from caldera_re.portfolio_rollup import ALPHA, CONTRACTS, metrics, roll_up
from caldera_re.report import heading, table
def slice_portfolio(r: dict, axis: str) -> dict:
"""Roll up one subportfolio per value of a contract tag.
Returns {tag value: {'members', 'el', 'tvar', 'k'}}, ordered by EL.
"""
buckets = {}
for c in CONTRACTS:
buckets.setdefault(CONTRACTS[c][axis], []).append(c)
slices = {}
for key, members in buckets.items():
sub = sum(r['participated'][m] for m in members) # the slice's roll-up
m = metrics(sub)
slices[key] = {'members': members, 'el': m['el'], 'tvar': m['tvar'],
'k': m['tvar'] - m['el']}
return dict(sorted(slices.items(), key=lambda kv: -kv[1]['el']))
if __name__ == '__main__':
r = roll_up()
enterprise = metrics(r['portfolio'])
for axis in ('region', 'peril'):
heading(f"Subportfolios by {axis} "
f"(each its own roll-up + metrics, $M)")
el_sum = tvar_sum = 0.0
rows = []
for key, s in slice_portfolio(r, axis).items():
el_sum += s['el']; tvar_sum += s['tvar']
rows.append((key, '+'.join(s['members']), s['el'], s['tvar'],
s['k']))
rows.append(('Sum', None, el_sum, tvar_sum, None))
rows.append(('Whole', None, enterprise['el'], enterprise['tvar'],
None))
table(['Slice', 'Contracts', 'EL', f"TVaR({ALPHA:.0%})", 'K'], rows,
fmt=['{}', '{}', '{:.2f}', '{:.2f}', '{:.2f}'])
print(f"Slice ELs sum to the enterprise EL exactly; slice TVaRs "
f"({tvar_sum:.2f}) exceed the enterprise TVaR "
f"({enterprise['tvar']:.2f})\nbecause the diversification "
f"*across* slices is lost when they are measured apart.\n") EL and capital, for the whole portfolio or any slice of it, are the risk side of the comparison with premium.
Step 5: risk versus opportunity
Section titled “Step 5: risk versus opportunity”The portfolio manager can weigh the portfolio’s risk against what it earns. Three of the six contracts have paid reinstatements, so what the portfolio earns depends on whether their reinstatement premium is counted, and so does the capital: the reinstatement premium is paid in the loss trials, and capital is measured on the outcome net of it.
| Without reinstatement premium | With reinstatement premium | |
|---|---|---|
| Booked upfront premium | $12.88M | $12.88M |
| Expected reinstatement premium | $3.24M | |
| Loss ratio (EL / premium income) | 113.3% | 90.5% |
| Premium income − EL − expenses | −$2.44M | $0.80M |
| Required capital | $8.92M | $7.25M |
| Risk-adjusted return on capital (RAROC) | −27.4% | 11.0% |
Without reinstatement premium the booked upfront premium does not cover the expected loss: the premiums of C1, C4 and C6 were set on the understanding that their cedents pay again for restored limit, and the view without reinstatement premium omits the payment. Portfolio decisions use the view with reinstatement premium.
One caveat from the standalone pricing framework applies here too: the booked premium is treated as fully earned revenue (no brokerage, ceding commission, or premium taxes are netted out), so these ratios share that framework’s illustrative scope. Their values also depend on the demo portfolio’s arithmetic-convenience premiums, which are far above real placements’ rates.
Whether the portfolio clears the profitability hurdle depends on its risk-adjusted return on capital (RAROC):
With a cost of capital of 10%, a portfolio returning 11.0% on its required capital clears the profitability hurdle, by 1.0 point. The margin is due to the capital basis, and the counterfactual shows it. Capitalised one by one, the same six contracts require $18.95M between them, net of their reinstatement premium. Premium income, expected loss, and expenses are unchanged, so the numerator is the same $0.80M:
The same premium income earns 11.0% on the portfolio’s capital and 4.2% on the sum of standalone capital. The contracts’ worst trials overlap little (US hurricane beside Japanese earthquake and European windstorm), so held together they require less capital than apart. The portfolio clears a profitability hurdle that the same contracts, capitalised separately, do not.
The standalone return on SunCoast’s program, 9.0%, is a different calculation and is not the counterfactual here: it is computed on three contracts at 100%, $32M of premium on $47.18M of program capital, where the portfolio holds six contracts at Caldera Re’s participations. It is below the profitability hurdle for the same reason. A deal judged alone has its own capital held against its whole tail and cannot use capital already held for other business.
Both returns are computed from a 20-trial tail (the 11.0% depends on the same two-trial TVaR flagged in the Watch Out in Step 3). The difference between them comes from the capital basis; the returns themselves are estimates, and a real portfolio manager would compute them from a production-scale roll-up before basing decisions on them.
"""Portfolio analytics on Caldera Re's rolled-up book of business.
The roll-up (`portfolio_rollup.py`) reduces the portfolio to loss
distributions; this module asks the business questions of them: what capital
does the portfolio tie up, is the premium adequate for the risk, and where is
the risk concentrated. Pricing reuses the standalone-pricing conventions --
capital K = TVaR_alpha - EL of the outcome net of reinstatement premium,
RAROC = (premium + expected reinstatement premium - EL - expenses) / K -- so the
portfolio verdict is directly comparable to the per-program one.
The headline is the capital basis. With reinstatement premium counted, the
same margin earns 11.0% on the portfolio's diversified capital but only 4.2%
on the sum of the contracts' standalone capital -- the portfolio clears the
10% hurdle only because it is diversified.
"""
import numpy as np
from caldera_re.portfolio_rollup import ALPHA, CONTRACTS, roll_up
from caldera_re.reinstatement_premium import reinstatement_factor
from caldera_re.reinstatement_pricing import capital as outcome_capital
from caldera_re.report import fields, heading, table
R_C = 0.10 # cost of capital rate
EPSILON = 0.05 # expense load as a fraction of EL
# 100% layer premiums from the Caldera Re Bible (0C); Caldera Re books its
# participation share of each.
PREMIUM_100 = {'C1': 14.0, 'C2': 5.0, 'C3': 13.0,
'C4': 6.0, 'C5': 17.0, 'C6': 13.0}
# C7, proposed (not in the six-contract portfolio): premium at 100%.
PROPOSED_C7 = 6.0
# Reinstatement clauses (Caldera Re Bible): occurrence limit, number of
# reinstatements, and the premium rate of each, all pro rata to amount. These
# repeat the terms passed to catxol() in portfolio_rollup.contract_gross();
# change them together.
REINSTATEMENTS = {'C1': (30.0, 1, 1.0), 'C4': (10.0, 1, 1.0),
'C6': (30.0, 2, (1.0, 0.5))}
def reinstatement_income(gross: dict) -> dict:
"""Each contract's reinstatement premium per trial at 100%, on its premium
(zero for a contract without a paid reinstatement)."""
out = {}
for c, g in gross.items():
if c in REINSTATEMENTS:
limit, n, rate = REINSTATEMENTS[c]
out[c] = PREMIUM_100[c] * reinstatement_factor(g, limit, n, rate)
else:
out[c] = np.zeros_like(g)
return out
def view(r: dict, with_reinstatement: bool) -> dict:
"""The portfolio's economics at its booked premium, with or without the
reinstatement premium its contracts earn. Capital is measured on the
outcome: participated gross, less reinstatement premium when counted."""
rip = reinstatement_income(r['gross'])
if not with_reinstatement:
rip = {c: 0 * v for c, v in rip.items()}
part = {c: CONTRACTS[c]['participation'] for c in CONTRACTS}
outcome = {c: part[c] * (r['gross'][c] - rip[c]) for c in CONTRACTS}
total = sum(outcome.values())
booked = premium_book()['total']
income = booked + sum(part[c] * rip[c].mean() for c in CONTRACTS)
el = r['port']['el']
margin = income - (1 + EPSILON) * el
k = outcome_capital(total, ALPHA)
k_sum = sum(outcome_capital(x, ALPHA) for x in outcome.values())
return {'premium': booked, 'reinstatement_premium': income - booked,
'income': income, 'loss_ratio': el / income, 'margin': margin,
'k': k, 'k_sum': k_sum, 'raroc': margin / k,
'raroc_standalone': margin / k_sum, 'outcome': total}
def premium_book() -> dict:
"""Caldera Re's booked premium for the participated portfolio."""
booked = {c: PREMIUM_100[c] * CONTRACTS[c]['participation']
for c in CONTRACTS}
return {'by_contract': booked, 'total': sum(booked.values())}
def capital(r: dict) -> dict:
"""Capital K = TVaR - EL, per standalone contract and for the portfolio."""
standalone = {c: s['tvar'] - s['el'] for c, s in r['standalone'].items()}
return {
'standalone': standalone,
'sum_standalone': sum(standalone.values()),
'portfolio': r['port']['tvar'] - r['port']['el'],
}
def concentration(r: dict, axis: str) -> dict:
"""Participated EL by slice -- additive, so slices sum to portfolio EL."""
buckets = {}
for c in CONTRACTS:
key = CONTRACTS[c][axis]
buckets[key] = buckets.get(key, 0.0) + r['standalone'][c]['el']
return dict(sorted(buckets.items(), key=lambda kv: -kv[1]))
if __name__ == '__main__':
r = roll_up()
prem = premium_book()
p = r['port']
heading("Booked premium, reinstatement premium and loss ratio, participated ($M)")
rip = reinstatement_income(r['gross'])
rows = []
for c in CONTRACTS:
s = r['standalone'][c]
booked = prem['by_contract'][c]
booked_rip = CONTRACTS[c]['participation'] * rip[c].mean()
rows.append((c, CONTRACTS[c]['participation'], s['el'], booked,
booked_rip, s['el'] / (booked + booked_rip)))
table(['C', 'part', 'pEL', 'prem', 'reinst. prem', 'LR'], rows,
fmt=['{}', '{:.1%}', '{:.2f}', '{:.2f}', '{:.2f}', '{:.1%}'])
without, with_ = view(r, False), view(r, True)
print()
heading(f"The portfolio at its booked premium (K = TVaR({ALPHA:.0%}) - EL of the outcome)")
table(['', 'Without', 'With reinstatement premium'], [
('Booked premium', without['premium'], with_['premium']),
('Expected reinstatement premium', without['reinstatement_premium'],
with_['reinstatement_premium']),
('Expected loss', p['el'], p['el']),
('Margin after expenses', without['margin'], with_['margin']),
('Portfolio K', without['k'], with_['k']),
('Sum of standalone K', without['k_sum'], with_['k_sum']),
], fmt=['{}', '{:.2f}M', '{:.2f}M'])
table(['RAROC', 'Without', 'With reinstatement premium'], [
('on portfolio K', without['raroc'], with_['raroc']),
('on sum of standalone K', without['raroc_standalone'],
with_['raroc_standalone']),
], fmt=['{}', '{:.1%}', '{:.1%}'])
print(f"\nWith reinstatement premium the portfolio clears the {R_C:.0%} cost of")
print("capital on its diversified capital and not on the sum of its contracts'")
print("standalone capital. Without it, the booked premium does not cover the")
print("expected loss.")
print()
heading("Concentration: participated EL by slice (adds to portfolio EL)")
for axis in ('region', 'peril', 'cedent'):
buckets = concentration(r, axis)
print(" " + axis + ": " + ", ".join(
f"{key} {v:.2f} ({v/p['el']:.0%})" for key, v in buckets.items())) The $7.25M of capital in that 11.0% is measured on the diversified portfolio, net of reinstatement premium. The diversification is derived below on the loss alone, without reinstatement premium, and then compared on both capital bases.
The diversification, carried implicitly
Section titled “The diversification, carried implicitly”The contrast at the end of Step 5 (contracts that fall short of the profitability hurdle capitalised one by one, a portfolio that clears it) has one cause. The standalone pricing application reported diversification within SunCoast’s program ($32.72M of capital saved because the three layers’ losses do not peak in the same trials) and claimed that the same effect operates between programs, across the whole portfolio. The portfolio numbers in Step 3 already contain it. The Portfolios page demonstrates the mechanism on these six contracts, each measured standalone on its own participated losses:4 the standalone TVaRs sum to $36.54M against the portfolio’s $23.51M, a $13.02M,5 35.6% diversification benefit, because the contracts’ worst losses are in different trials. That page does not compute the capital consequence. In capital, the same comparison is:
The EL term is the same $14.59M in both lines, because expected loss adds exactly, so the capital difference equals the tail difference: . Put as a multiplier, : held as one diversified portfolio, the same contracts require of the capital they would standalone, a capital efficiency of 2.46× (glossary). The $13.02M difference is capital Caldera Re does not have to hold; the $21.94M is a counterfactual, six separately capitalised positions, not a sum Caldera Re ever held.
Both capital figures above are measured on the loss alone, without reinstatement premium. The return in Step 5 uses capital measured on the outcome net of reinstatement premium, which is lower for the portfolio and for the sum of standalone positions:
| Capital measured on | Portfolio | Sum of standalone | Capital efficiency |
|---|---|---|---|
| Loss, without reinstatement premium | $8.92M | $21.94M | 2.46× |
| Outcome net of reinstatement premium (Step 5) | $7.25M | $18.95M | 2.61× |
Run deal by deal, this comparison has little use: a reinsurer already holding a portfolio prices against that portfolio, not against an empty one, so the standalone figure is an upper bound rather than the number to act on.6 Adding up standalone TVaRs is exactly the mistake the roll-up exists to avoid, and once a contract is written, its standalone performance is close to irrelevant. The portfolio manager tracks the portfolio figures: premium income, expected loss, capital at risk, return on capital, and each contract’s contribution to them. The diversification is not a step in the pipeline; it is implicit in the portfolio view. It is the reason the portfolio and standalone results differ, and the reason marginal pricing (planned) prices new deals against the portfolio.
The capital figure is only as reliable as the sum used to compute it, and that sum requires every contract’s losses aligned to the same trials.
At production scale
Section titled “At production scale”At production scale discusses how rows and trials, currency, and reproducibility change for the roll-up, together with the partition-by-trial shape it shares with the other pipelines. The roll-up adds one requirement of its own.
Alignment. Every contract’s losses must be aligned to the same trials before they are
summed: the roll-up is a sum within a trial, so a mismatched trial index silently corrupts
the tail. At occurrence resolution the same requirement applies to the full occurrence key,
(trial_id, timestamp, event_id): the alignment is finer, and the key determines how the data
is laid out on disk and whether a roll-up queries it or scans it. Where every contract’s rows are
co-partitioned by trial, one pass over a trial’s partition that sums them is faster than a query per
contract for its trial loss. Shared trial identity
across every cedent’s YELT (the demo data’s fixed trial ids) makes
the sum meaningful. In production this is a join key you validate at every boundary, the way
risk profiling validates a loss set at ingest.
Validation catches a trial id outside 1..N; it cannot catch a dropped trial, an index permuted
within 1..N, or loss sets from different simulations. A trial_id identifies a trial within one simulation and its trial numbering, so two vendors’ YELTs
with 10,000 trials each pass a trial-count check, match on every trial_id, and still pair
unrelated simulated futures: trial 17 in one has no relation to trial 17 in the other, and the
sum treats the two loss sets as independent whatever the actual dependence between the risks.
The precondition is a property of how the loss sets were produced (one simulation, one set of
trials), not of their row counts or ids. A reinsurer checks it against each loss set’s provenance:
the model, version and run that produced it, which it
tracks for every loss set.
Combining loss sets from different vendors, model versions or climate views needs a dependence
treatment that this site does not cover yet.
| Roll-up resolution | Join key | What a mismatch corrupts |
|---|---|---|
| Trial (produces a YLT) | trial_id | The AEP tail: a shifted index adds losses from different trials as if they were one |
| Occurrence (produces a YELT) | (trial_id, timestamp, event_id) | The OEP tail and every occurrence term downstream: one event booked twice, or against the wrong trial |
| Either, on loss sets from different simulations | Both keys match as integers but name unrelated trials (and, across catalogs, unrelated events) | Every tail metric and the capital on it: the sum pairs unrelated futures, which, for independently generated simulations, imposes independence between the loss sets; for simulations that share a catalog or random streams it imposes an arbitrary coupling. No check on the ids detects it. Expected loss still adds correctly |
The same trial identity is the partition key in the computational shape all three pipelines share.
What carries forward
Section titled “What carries forward”Portfolio analytics is one pipeline: select the constituents, roll up (participation, then sum), measure, slice, and weigh risk against opportunity. The roll-up reduces the portfolio’s losses to a single distribution; the analytics compute from it the numbers on which Caldera Re bases its portfolio decisions. For Caldera Re those numbers are a $14.59M expected loss, an $8.92M capital requirement on the loss, a capital efficiency of 2.46× (the same contracts would need $21.94M measured standalone), and, with the reinstatement premium received on its contracts, an 11.0% return on required capital, above the 10% profitability hurdle by 1.0 point. The demo-scale calculation produces those figures; the same pipeline applies at production trial counts.
A retrocession contract can use the portfolio YELT as its subject loss, so Caldera Re can buy protection on its own portfolio, or free the capital held against it to write more business. Either way, a retro layer attaches to the loss the roll-up produces. Marginal pricing (planned) also uses the rolled-up portfolio YELT: the value of one new contract is what it adds to the portfolio’s capital, its Marginal TVaR less its expected loss, computed as the roll-up run twice, with and without the contract, and differenced. Standalone pricing asked whether a deal clears alone; marginal pricing asks whether it clears here, against this portfolio. At 10,000 to 1,000,000+ trials, portfolio analytics uses a partitioned computation over named, versioned loss sets.
Footnotes
Section titled “Footnotes”-
Lloyd’s, “Exposure management”, which makes the function responsible for “the Corporation’s understanding and control of catastrophe risk, at the individual Syndicate and Market levels”. The Lloyd’s Market Association (LMA), “Exposure Management”, whose team “supports managing agents in enhancing their approach to catastrophe risk modelling, exposure data quality and regulatory preparedness”. Both describe the function at market level, at the Corporation and at the association; that a reinsurer’s own equivalent carries the same name, and which job title holds it, is this site’s assessment. ↩
-
Commission Delegated Regulation (EU) 2015/35, Official Journal of the European Union L 12, 17 January 2015. Article 120(2) aggregates the windstorm, earthquake, flood, hail, and subsidence capital requirements as the square root of the sum of their squares, with no correlation term, where Article 121(1) writes the same form for windstorm regions with an explicit correlation matrix. Aggregating without that term treats the five perils as uncorrelated. The Annex V windstorm matrix sets 0,00 between many separated regions and reaches 1,00 only for the three Caribbean overseas regions. The standard formula fixes a calibration rather than reporting a measured correlation, and it covers EU perils and regions, so it supports treating separated catastrophe regions and distinct perils as independent by convention rather than by measurement. ↩
-
A European windstorm is not entirely independent of the Atlantic weather systems that drive hurricanes, but the correlation is small enough to ignore for simplicity. ↩
-
Everything in this comparison is at Caldera Re’s participations; the standalone pricing application priced C1–C3 at the 100% layer, so its figures are larger than the ones here. ↩
-
The tail difference and both capital lines below are computed from the unrounded values and displayed to two decimals; with the displayed inputs the tail difference comes to 13.03 and the second capital line to 21.95. The rounding rule is stated in the Caldera Re appendix. ↩
-
Rodney E. Kreps, “Investment-Equivalent Reinsurance Pricing”, Proceedings of the Casualty Actuarial Society, 1998. Because “most reinsurers already have a book of business and hence contracts mutually support each other”, a stand-alone risk load “can be regarded as a reasonable maximum”: pricing a deal alone “is equivalent to assuming that the whole book is fully correlated” and “will result in larger risk loads than are actually needed”. ↩