Code
import pandas as pd
from aggregate import build, qd, __version__
from style import wprint, GT, refuse, figures
figures()
print(__version__)1.60.1
Swing-rated aggregate reinsurance, with a ceded premium set by the ceded loss between a minimum and a maximum
Stephen J. Mildenhall
2026-11-01
A swing-rated cover sets its ceded premium after the treaty year from the reinsurer’s own losses. With ceded loss \(A\), a basic premium \(b\), a loss conversion multiplier \(m\), and a minimum \(\ell\) and maximum \(u\), the ceded premium is \[ p(A) = \min\bigl(\max(b + mA,\ \ell),\ u\bigr). \] Swing plans are used on working layers and aggregate covers whose losses are predictable enough that the cedant is content to pay them back. Between the minimum and the maximum, each unit of ceded loss raises the premium by \(m\), so the cover gives real protection only in the years when the premium reaches its maximum. The minimum defaults to the basic and the maximum to no cap.
The clause swing basic <b> lcm <m> [min <lo>] [max <hi>] takes the place of the deposit, rol, or rate premium clause on an aggregate layer. A pnl or xpnl books the result (see Using pnl and xpnl). A program has at most one variable feature: swing, a sliding scale, a profit commission, or a loss corridor. Swing on an occurrence layer is refused; there, reinstatements give the random ceded premium.
1.60.1
The variable feature pages share one base book: premium 10,000 at a 70% loss ratio, a policy limit of 2,000, lognormal severity with mean 150 and CV 2.5, gamma mixed claim counts with mixing CV 0.25, and expenses of 25% of premium. The cover here is a stop loss of 4,000 xs 8,000, from an 80% to a 120% loss ratio, with swing premium \(200 + 1.1A\), at least 500 and at most 2,500.
book = '''agg Book
10000 premium at 0.7 lr
2000 xs 0
sev lognorm 150 cv 2.5 {occurrence}
mixed gamma 0.25 {aggregate}'''
program = '''xpnl {name}
inherit premium
less
''' + book + '''
less
0.25 premium expenses'''
layer = 'aggregate net of 4000 xs 8000 {} as "Stop loss"'
s = build(program.format(name='Swing', occurrence='', aggregate=layer.format(
'swing basic 200 lcm 1.1 min 500 max 2500')))
qd(s)
gross = s.engine
print(f'premium at the minimum {gross.cdf(8000 + 300 / 1.1):.4f}, '
f'at the maximum {gross.sf(8000 + 2300 / 1.1):.4f}')PnL('Swing': 2 groups, 5 legs -> 'margin')
EX SD CV Skew P01 Median P99
Step Side
Gross Consideration 10000 0 0 10000 10000 10000
Obligation -9500 2704 -0.28463 -0.67687 -17071 -9193 -4633
Margin 500 2704 5.408 -0.67687 -7071 807 5367
Stop loss Consideration -902.87 733.2 -0.81208 -1.4843 -2500 -500 -500
Obligation 607.6 1160.2 1.9094 1.8962 0 0 4000
Margin -295.26 497.71 -1.6856 2.8642 -500 -500 1500
Net 204.73 2352.5 11.49 -0.27905 -5571 307 4867
All Consideration 9097.1 733.2 0.080597 -1.4843 7500 9500 9500
Obligation -8892.4 1841.3 -0.20706 0.20371 -13071 -9193 -4633
Margin 204.73 2352.5 11.49 -0.27905 -5571 307 4867
Impact -295.26 497.71 -1.6856 2.8642 -500 -500 1500
premium at the minimum 0.7113, at the maximum 0.1297
The Stop loss step shows a ceded loss with mean 607.60, which is zero in more than half of all years, and a ceded premium with mean 902.87 and standard deviation (SD) 733.20. The premium is at the minimum when the ceded loss is at most \(300/1.1 = 272.7\), in 71.1% of years, and at the maximum when the ceded loss exceeds \(2{,}300/1.1 = 2{,}090.9\), in 13.0% of years. Without the collar the mean premium would be \(200 + 1.1 \times 607.60 = 868.36\); the minimum adds more than the maximum removes.
A fixed premium of 902.87, the mean swing premium, on the 4,000 xs 8,000 stop loss gives the same mean results as the swing plan, so the comparison isolates the effect of the swing on the spread. The table shows the mean and SD of the ceded premium, the SD of the cover’s result (premium less recovery), and the mean, SD, and 1 in 100 result P01 of the cedant’s net result, with the uncovered book for reference.
premium = -s.summary_df.loc[('Stop loss', 'Consideration'), 'EX']
f = build(program.format(name='Fixed', occurrence='',
aggregate=layer.format(f'deposit {premium:.2f}')))
rows = {}
for k, x in {'fixed': f, 'swing': s}.items():
t = x.summary_df
rows[k] = [-t.loc[('Stop loss', 'Consideration'), 'EX'],
t.loc[('Stop loss', 'Consideration'), 'SD'],
t.loc[('Stop loss', 'Margin'), 'SD'],
*t.loc[('All', 'Margin'), ['EX', 'SD', 'P01']]]
g = s.summary_df.loc[('Gross', 'Margin'), ['EX', 'SD', 'P01']]
rows = {'no cover': [0, 0, 0, *g], **rows}
df = pd.DataFrame.from_dict(rows, orient='index', columns=[
'premium', 'premium SD', 'cover SD', 'net', 'net SD', 'net P01'])
df.index.name = 'ceded premium'
GT(df, float_format=',.2f')| ceded premium | premium | premium SD | cover SD | net | net SD | net P01 |
|---|---|---|---|---|---|---|
| no cover | 0.00 | 0.00 | 0.00 | 500.00 | 2,703.99 | -7,071.00 |
| fixed | 902.87 | 0.00 | 1,160.18 | 204.73 | 1,841.30 | -3,973.87 |
| swing | 902.87 | 733.20 | 497.71 | 204.73 | 2,352.46 | -5,571.00 |
Swing rating returns most of the volatility to the cedant. Inside the collar each unit of ceded loss raises the premium by 1.1, so the cedant pays back more than it recovers, and the cover gives real protection only above a ceded loss of 2,090.9, where the premium stops at its maximum. The SD of the cover’s result falls from 1,160.18 under the fixed premium to 497.71 under the swing, and the cedant’s net SD rises from 1,841.30 to 2,352.46, against 2,703.99 with no cover. The 1 in 100 net result is \(-5{,}571\) under the swing, against \(-3{,}974\) under the fixed premium and \(-7{,}071\) with no cover.
Swing is refused on an occurrence layer, alongside a deposit, rol, or rate clause, alongside cede on its own layer, and on a second layer, since a program has at most one variable feature.
swing = 'swing basic 200 lcm 1.1 min 500 max 2500'
refuse(program.format(name='S1', aggregate='',
occurrence=f'occurrence net of 1000 xs 1000 {swing}'),
program.format(name='S2', occurrence='',
aggregate=f'aggregate net of 4000 xs 8000 rol 0.2 {swing}'),
program.format(name='S3', occurrence='',
aggregate=f'aggregate net of 4000 xs 8000 cede 0.1 {swing}'),
program.format(name='S4', occurrence='',
aggregate=f'aggregate net of 4000 xs 8000 {swing} and '
f'4000 xs 12000 {swing}'))DecL: variable-rating features (swing / slide / pc / corridor) decorate
aggregate reinsurance only. On an occurrence layer, reinstatements give a random
ceded premium.
DecL: 'swing' supplies the ceded premium and replaces the deposit / rol / rate
clause; give one or the other.
DecL: 'swing' supplies the ceded premium and the commission; give it without
'cede'.
DecL: at most one variable-rating feature per program; found 2.
---
title: Using swing rating
subtitle: "Swing-rated aggregate reinsurance, with a ceded premium set by the ceded loss between a minimum and a maximum"
categories: [reinsurance, variable feature, P&L]
date: last-modified
jupyter:
jupytext:
text_representation:
extension: .qmd
format_name: quarto
format_version: '1.0'
kernelspec:
display_name: Python 3 (ipykernel)
language: python
name: python3
---
A **swing-rated** cover sets its ceded premium after the treaty year from the reinsurer's own losses. With ceded loss $A$, a basic premium $b$, a loss conversion multiplier $m$, and a minimum $\ell$ and maximum $u$, the ceded premium is
$$
p(A) = \min\bigl(\max(b + mA,\ \ell),\ u\bigr).
$$
Swing plans are used on working layers and aggregate covers whose losses are predictable enough that the cedant is content to pay them back. Between the minimum and the maximum, each unit of ceded loss raises the premium by $m$, so the cover gives real protection only in the years when the premium reaches its maximum. The minimum defaults to the basic and the maximum to no cap.
The clause `swing basic <b> lcm <m> [min <lo>] [max <hi>]` takes the place of the `deposit`, `rol`, or `rate` premium clause on an aggregate layer. A `pnl` or `xpnl` books the result (see [Using pnl and xpnl](pnls.qmd)). A program has at most one variable feature: swing, a [sliding scale](sliding-scale.qmd), a [profit commission](profit-commission.qmd), or a [loss corridor](loss-corridor.qmd). Swing on an occurrence layer is refused; there, [reinstatements](reinstatements.qmd) give the random ceded premium.
```decl
aggregate net of 4000 xs 8000 swing basic 200 lcm 1.1 min 500 max 2500
4000 xs 8000 swing basic 200 lcm 1.1 max 2500 # minimum 200, the basic
4000 xs 8000 swing basic 0 lcm 1.25 min 500 # no maximum
```
```{python}
import pandas as pd
from aggregate import build, qd, __version__
from style import wprint, GT, refuse, figures
figures()
print(__version__)
```
## A swing-rated stop loss on the base book
The variable feature pages share one base book: premium 10,000 at a 70% loss ratio, a policy limit of 2,000, lognormal severity with mean 150 and CV 2.5, gamma mixed claim counts with mixing CV 0.25, and expenses of 25% of premium. The cover here is a stop loss of 4,000 xs 8,000, from an 80% to a 120% loss ratio, with swing premium $200 + 1.1A$, at least 500 and at most 2,500.
```{python}
book = '''agg Book
10000 premium at 0.7 lr
2000 xs 0
sev lognorm 150 cv 2.5 {occurrence}
mixed gamma 0.25 {aggregate}'''
program = '''xpnl {name}
inherit premium
less
''' + book + '''
less
0.25 premium expenses'''
layer = 'aggregate net of 4000 xs 8000 {} as "Stop loss"'
s = build(program.format(name='Swing', occurrence='', aggregate=layer.format(
'swing basic 200 lcm 1.1 min 500 max 2500')))
qd(s)
gross = s.engine
print(f'premium at the minimum {gross.cdf(8000 + 300 / 1.1):.4f}, '
f'at the maximum {gross.sf(8000 + 2300 / 1.1):.4f}')
```
The `Stop loss` step shows a ceded loss with mean 607.60, which is zero in more than half of all years, and a ceded premium with mean 902.87 and standard deviation (SD) 733.20. The premium is at the minimum when the ceded loss is at most $300/1.1 = 272.7$, in 71.1% of years, and at the maximum when the ceded loss exceeds $2{,}300/1.1 = 2{,}090.9$, in 13.0% of years. Without the collar the mean premium would be $200 + 1.1 \times 607.60 = 868.36$; the minimum adds more than the maximum removes.
## Swing rating against a fixed premium
A fixed premium of 902.87, the mean swing premium, on the 4,000 xs 8,000 stop loss gives the same mean results as the swing plan, so the comparison isolates the effect of the swing on the spread. The table shows the mean and SD of the ceded premium, the SD of the cover's result (premium less recovery), and the mean, SD, and 1 in 100 result `P01` of the cedant's net result, with the uncovered book for reference.
```{python}
premium = -s.summary_df.loc[('Stop loss', 'Consideration'), 'EX']
f = build(program.format(name='Fixed', occurrence='',
aggregate=layer.format(f'deposit {premium:.2f}')))
rows = {}
for k, x in {'fixed': f, 'swing': s}.items():
t = x.summary_df
rows[k] = [-t.loc[('Stop loss', 'Consideration'), 'EX'],
t.loc[('Stop loss', 'Consideration'), 'SD'],
t.loc[('Stop loss', 'Margin'), 'SD'],
*t.loc[('All', 'Margin'), ['EX', 'SD', 'P01']]]
g = s.summary_df.loc[('Gross', 'Margin'), ['EX', 'SD', 'P01']]
rows = {'no cover': [0, 0, 0, *g], **rows}
df = pd.DataFrame.from_dict(rows, orient='index', columns=[
'premium', 'premium SD', 'cover SD', 'net', 'net SD', 'net P01'])
df.index.name = 'ceded premium'
GT(df, float_format=',.2f')
```
Swing rating returns most of the volatility to the cedant. Inside the collar each unit of ceded loss raises the premium by 1.1, so the cedant pays back more than it recovers, and the cover gives real protection only above a ceded loss of 2,090.9, where the premium stops at its maximum. The SD of the cover's result falls from 1,160.18 under the fixed premium to 497.71 under the swing, and the cedant's net SD rises from 1,841.30 to 2,352.46, against 2,703.99 with no cover. The 1 in 100 net result is $-5{,}571$ under the swing, against $-3{,}974$ under the fixed premium and $-7{,}071$ with no cover.
## Refused programs
Swing is refused on an occurrence layer, alongside a `deposit`, `rol`, or `rate` clause, alongside `cede` on its own layer, and on a second layer, since a program has at most one variable feature.
```{python}
swing = 'swing basic 200 lcm 1.1 min 500 max 2500'
refuse(program.format(name='S1', aggregate='',
occurrence=f'occurrence net of 1000 xs 1000 {swing}'),
program.format(name='S2', occurrence='',
aggregate=f'aggregate net of 4000 xs 8000 rol 0.2 {swing}'),
program.format(name='S3', occurrence='',
aggregate=f'aggregate net of 4000 xs 8000 cede 0.1 {swing}'),
program.format(name='S4', occurrence='',
aggregate=f'aggregate net of 4000 xs 8000 {swing} and '
f'4000 xs 12000 {swing}'))
```