risksim

Portfolio Monte Carlo simulation and risk measures: simulate aggregate outcomes across a portfolio of contracts and summarize the distribution with standard risk measures.

The package builds a portfolio from contract definitions, simulates aggregate outcomes, and reports risk measures (such as value-at-risk and tail value-at-risk) over the simulated distribution — the capital view that sits at the end of the experience → pricing → loss → tail → capital pipeline.

var and tvar follow the ecosystem-wide empirical estimators (inverted-CDF order statistic; Acerbi–Tasche), so a portfolio’s risk measures here match the same quantities computed in lossmodels or extremeloss byte for byte, and every simulation accepts the shared rng argument (None, seed, or Generator) for bit-reproducible runs — see Conventions.

See the API reference below for the full surface; each object’s docstring carries its own usage.

Portfolio simulation

A portfolio is a list of named items, each wrapping anything with a .sample method — a fitted lossmodels.CollectiveRiskModel drops straight in:

import risksim as rs

port = rs.Portfolio([
    rs.PortfolioItem("commercial", crm),
    rs.PortfolioItem("specialty", other_model, weight=0.4),
])
res = port.simulate(100_000, rng=7)
res.gross_losses, res.component_losses, res.component_names

simulate returns a SimulationResult carrying the gross, ceded, and retained loss vectors, the per-component draws, and — when a contract is applied — the per-layer recoveries. The rng argument (None, seed, or Generator) resolves to one generator threaded through the components, so the same seed reproduces every array bit for bit.

Contracts

AggregateLayer(attachment, limit, share) follows the ecosystem coverage semantics: limit is the layer width, so the layer exhausts at attachment + limit (see Conventions). ContractProgram stacks layers, and apply_contract applies either to any loss vector, returning (ceded, retained):

treaty = rs.AggregateLayer(attachment=3_200_000, limit=1_500_000,
                           name="agg_stop_loss")
res = port.simulate(100_000, contract=treaty, rng=7)
res.ceded_losses, res.retained_losses, res.layer_losses

ceded, retained = rs.apply_contract([50.0, 150.0, 400.0],
                                    rs.AggregateLayer(attachment=100.0, limit=200.0))
# ceded    -> [  0.,  50., 200.]
# retained -> [ 50., 100., 200.]

Risk measures

rs.metrics.var(res.gross_losses, 0.99)
rs.metrics.tvar(res.retained_losses, 0.99)

These are the ecosystem-wide empirical estimators — see the intro above and Conventions.

API reference

class AggregateLayer(attachment: float = 0.0, limit: float | None = None, share: float = 1.0, name: str | None = None)[source]

Bases: object

Aggregate annual layer applied to simulated aggregate losses.

For aggregate annual loss S, ceded loss is:

C = share * min((S - attachment)+, limit)

with no cap if limit is None.

attachment_probability(losses: ndarray | list[float]) float[source]

Probability that a loss reaches the layer, i.e. P(loss > attachment).

This depends only on the attachment point, not on share or limit, so it stays correct under degenerate parameters (e.g. share=0).

apply_contract(losses: ndarray | list[float], contract: AggregateLayer | ContractProgram) tuple[ndarray, ndarray][source]

Return (ceded, retained) arrays for a single aggregate layer or a multi-layer contract program.

class ContractProgram(layers: Sequence[AggregateLayer], name: str = 'contract_program')[source]

Bases: object

Collection of aggregate layers applied to the same gross loss.

This first version assumes the layers are intended to work together without overlap. For a standard non-overlapping tower, total ceded loss is the row-wise sum of ceded loss by layer.

class Portfolio(items: Sequence[PortfolioItem], name: str = 'portfolio')[source]

Bases: object

Portfolio of aggregate-loss components.

This first version assumes components are sampled independently.

variance() float[source]

Analytic variance under the independence assumption.

class PortfolioItem(name: 'str', model: 'SupportsSample', weight: 'float' = 1.0)[source]

Bases: object

class SimulationResult(gross_losses: ndarray, ceded_losses: ndarray | None = None, retained_losses: ndarray | None = None, component_losses: ndarray | None = None, component_names: Sequence[str] | None = None, layer_losses: ndarray | None = None, layer_names: Sequence[str] | None = None, contract_name: str | None = None)[source]

Bases: object

Container for portfolio simulation outputs.

If retained_losses is present, the primary losses view is retained/net loss. Otherwise, the primary losses view is gross loss.