3. Tutorial 3: Configuring Insurance
In the previous tutorials, you learned how to set up a manufacturer, generate losses, and run a basic simulation. Now it is time to do what this framework was built for: configuring insurance programs and measuring their impact on long-term business growth.
We will follow NovaTech Plastics, a $10M plastics manufacturer with an 8% operating margin, as they design their insurance protection. NovaTech faces a familiar dilemma: insurance premiums feel expensive relative to expected losses, but a single catastrophic event could cripple the business. Ergodic analysis will show us why the “expensive” premium might actually be a bargain.
3.1. A Quick Insurance Refresher
If you already work with commercial insurance, feel free to skip ahead. For everyone else, here are the four concepts that matter most:
Deductible (Self-Insured Retention): The dollar amount NovaTech pays out of pocket before any insurance kicks in. Higher deductibles reduce premium but increase retained risk.
Attachment Point: The loss amount at which a specific insurance layer begins to respond. For the first layer, this typically equals the deductible.
Limit: The maximum amount a layer will pay. Once a layer is exhausted, any remaining loss falls to the next layer or back on the company.
Premium Rate: The annual cost of a layer, expressed as a percentage of its limit.
Insurance is structured in layers stacked on top of one another, forming a tower. Lower layers are more expensive per dollar of coverage because they pay out more frequently. Higher excess layers are cheaper because they only respond to large, rare events.
3.2. Creating a Simple Insurance Policy
The InsurancePolicy and InsuranceLayer classes in ergodic_insurance.insurance are the building blocks you will use with the Simulation engine. Let us start NovaTech with a straightforward single-layer policy.
from ergodic_insurance import InsurancePolicy, InsuranceLayer
# NovaTech's first insurance layer:
# - $100K deductible (they retain the first $100K of any loss)
# - $5M of coverage above the deductible
# - 2.5% premium rate on the limit
primary_layer = InsuranceLayer(
attachment_point=100_000, # Coverage begins at $100K
limit=5_000_000, # Maximum payout: $5M
rate=0.025 # Annual premium = 2.5% x $5M = $125K
)
policy = InsurancePolicy(
layers=[primary_layer],
deductible=100_000 # Self-insured retention
)
print(f"Annual Premium: ${policy.calculate_premium():,.0f}")
print(f"Total Coverage: ${policy.get_total_coverage():,.0f}")
Expected Output:
Annual Premium: $125,000
Total Coverage: $5,000,000
That \(125K premium represents about 1.25% of NovaTech's \)10M asset base. It might look like a drag on earnings, but wait until we see what happens without it.
3.3. Building a Multi-Layer Tower
Real-world insurance programs rarely consist of a single layer. NovaTech’s risk manager wants broader protection, so she builds a three-layer tower:
# Primary layer: $5M xs $250K (covers $250K to $5.25M)
primary = InsuranceLayer(
attachment_point=250_000,
limit=5_000_000,
rate=0.025 # 2.5% -- highest rate, most frequent claims
)
# First excess layer: $5M xs $5.25M (covers $5.25M to $10.25M)
excess_1 = InsuranceLayer(
attachment_point=5_250_000,
limit=5_000_000,
rate=0.015 # 1.5% -- mid-layer
)
# Second excess layer: $10M xs $10.25M (covers $10.25M to $20.25M)
excess_2 = InsuranceLayer(
attachment_point=10_250_000,
limit=10_000_000,
rate=0.008 # 0.8% -- catastrophe layer, lowest rate
)
tower = InsurancePolicy(
layers=[primary, excess_1, excess_2],
deductible=250_000
)
# Print the tower summary
print("=== NovaTech Insurance Tower ===")
print(f"Deductible: ${tower.deductible:,.0f}")
for i, layer in enumerate(tower.layers, 1):
exhaustion = layer.attachment_point + layer.limit
premium = layer.calculate_premium()
print(
f" Layer {i}: ${layer.limit/1e6:.0f}M xs ${layer.attachment_point/1e6:,.2f}M "
f"| Rate: {layer.rate:.1%} | Premium: ${premium:,.0f}"
)
print(f"Total Coverage: ${tower.get_total_coverage():,.0f}")
print(f"Total Annual Premium: ${tower.calculate_premium():,.0f}")
Expected Output:
=== NovaTech Insurance Tower ===
Deductible: $250,000
Layer 1: $5M xs $0.25M | Rate: 2.5% | Premium: $125,000
Layer 2: $5M xs $5.25M | Rate: 1.5% | Premium: $75,000
Layer 3: $10M xs $10.25M | Rate: 0.8% | Premium: $80,000
Total Coverage: $20,000,000
Total Annual Premium: $280,000
Notice how the premium rate decreases as the layers go higher. The primary layer (closest to expected losses) costs the most per dollar of coverage, while the catastrophe layer is the cheapest. This reflects the decreasing probability that losses will reach those heights.
3.4. Processing Claims Through the Policy
Before running a full simulation, it helps to understand how a single claim flows through the tower. The process_claim() method returns a tuple of (company_payment, insurance_recovery):
# Scenario 1: Small loss -- entirely within the deductible
company_pays, insurer_pays = tower.process_claim(150_000)
print(f"$150K loss -> Company: ${company_pays:,.0f}, Insurance: ${insurer_pays:,.0f}")
# Scenario 2: Medium loss -- penetrates the primary layer
company_pays, insurer_pays = tower.process_claim(3_000_000)
print(f"$3M loss -> Company: ${company_pays:,.0f}, Insurance: ${insurer_pays:,.0f}")
# Scenario 3: Large loss -- hits two layers
company_pays, insurer_pays = tower.process_claim(8_000_000)
print(f"$8M loss -> Company: ${company_pays:,.0f}, Insurance: ${insurer_pays:,.0f}")
# Scenario 4: Catastrophic loss -- exceeds all coverage
company_pays, insurer_pays = tower.process_claim(25_000_000)
print(f"$25M loss -> Company: ${company_pays:,.0f}, Insurance: ${insurer_pays:,.0f}")
Expected Output:
$150K loss -> Company: $150,000, Insurance: $0
$3M loss -> Company: $250,000, Insurance: $2,750,000
$8M loss -> Company: $250,000, Insurance: $7,750,000
$25M loss -> Company: $4,750,000, Insurance: $20,250,000
You can also use calculate_recovery() when you only need the insurance recovery amount:
recovery = tower.calculate_recovery(3_000_000)
print(f"Insurance recovery on $3M claim: ${recovery:,.0f}")
A few things to notice. Small losses stay entirely with NovaTech. Medium losses hit the primary layer and NovaTech only pays the deductible. The catastrophic \(25M loss exceeds the tower, so NovaTech absorbs \)4.75M (\(250K deductible plus \)4.75M above the tower). This is the “uninsured gap” that keeps risk managers up at night.
3.5. Insured vs. Uninsured: The Simulation Comparison
Now for the central question: does insurance actually improve NovaTech’s long-term growth? Let us run parallel simulations with and without coverage.
from ergodic_insurance import (
ManufacturerConfig, WidgetManufacturer, ManufacturingLossGenerator,
Simulation, InsurancePolicy, InsuranceLayer,
)
# -- NovaTech's financial profile --
novatech_config = ManufacturerConfig(
initial_assets=10_000_000,
asset_turnover_ratio=1.0,
base_operating_margin=0.08,
tax_rate=0.25,
retention_ratio=1.0
)
# -- Insurance policy: $5M xs $100K --
layer = InsuranceLayer(
attachment_point=100_000,
limit=5_000_000,
rate=0.025
)
policy = InsurancePolicy(layers=[layer], deductible=100_000)
# -- Loss profile: moderate frequency, high severity variability --
loss_gen = ManufacturingLossGenerator.create_simple(
frequency=0.15,
severity_mean=1_000_000,
severity_std=1_500_000,
seed=42
)
# -- Simulation WITH insurance --
manufacturer_insured = WidgetManufacturer(novatech_config)
sim_insured = Simulation(
manufacturer=manufacturer_insured,
loss_generator=loss_gen,
insurance_policy=policy,
time_horizon=30,
seed=42
)
results_insured = sim_insured.run()
# -- Simulation WITHOUT insurance (same seed for fair comparison) --
loss_gen_no_ins = ManufacturingLossGenerator.create_simple(
frequency=0.15,
severity_mean=1_000_000,
severity_std=1_500_000,
seed=42
)
manufacturer_uninsured = WidgetManufacturer(novatech_config)
sim_uninsured = Simulation(
manufacturer=manufacturer_uninsured,
loss_generator=loss_gen_no_ins,
time_horizon=30,
seed=42
)
results_uninsured = sim_uninsured.run()
# -- Compare outcomes --
insured_growth = results_insured.calculate_time_weighted_roe()
uninsured_growth = results_uninsured.calculate_time_weighted_roe()
print("=== NovaTech: 30-Year Insurance Impact ===")
print(f"{'Metric':<30} {'Insured':>14} {'Uninsured':>14}")
print("-" * 60)
print(f"{'Final Equity':<30} ${results_insured.equity[-1]:>13,.0f} ${results_uninsured.equity[-1]:>13,.0f}")
print(f"{'Time-Weighted ROE':<30} {insured_growth:>13.2%} {uninsured_growth:>13.2%}")
print(f"{'Survived':<30} {'Yes' if results_insured.insolvency_year is None else 'No':>14} {'Yes' if results_uninsured.insolvency_year is None else 'No':>14}")
print(f"{'Annual Premium Paid':<30} ${policy.calculate_premium():>13,.0f} {'$0':>14}")
print(f"{'Growth Improvement':<30} {insured_growth - uninsured_growth:>+13.2%}")
The key metric is Time-Weighted ROE: this is the ergodic (time-average) growth rate that determines what actually happens to NovaTech over its lifetime. Even though NovaTech pays $125K per year in premiums, insurance reduces the devastating impact of large losses on compounding growth.
Why does this work? Ensemble-average thinking says: “expected loss is \(150K, the premium is \)125K, so insurance is a fair deal.” Ergodic thinking reveals something deeper: without insurance, a single $3M loss destroys equity that would have compounded for decades. The growth you lose to volatility drag far exceeds the premium cost.
3.6. Advanced Features: InsuranceProgram
For more sophisticated modeling (reinstatements, aggregate limits, participation rates, and different limit types) use the InsuranceProgram and EnhancedInsuranceLayer classes from ergodic_insurance.insurance_program.
Note:
InsuranceProgramis a standalone analysis tool with its own claim processing methods. TheSimulationengine usesInsurancePolicy(fromergodic_insurance.insurance). You can useInsuranceProgramdirectly for detailed program analysis, or convert between the two usingInsurancePolicy.to_enhanced_program().
3.6.1. Reinstatements
Reinstatements restore layer coverage after a claim erodes the limit. They are common in reinsurance and catastrophe layers:
from ergodic_insurance.insurance_program import (
InsuranceProgram,
EnhancedInsuranceLayer,
ReinstatementType,
)
# Catastrophe layer with 2 reinstatements
cat_layer = EnhancedInsuranceLayer(
attachment_point=5_000_000,
limit=5_000_000,
base_premium_rate=0.02,
reinstatements=2, # Two reinstatements available
reinstatement_premium=1.0, # 100% of base premium per reinstatement
reinstatement_type=ReinstatementType.PRO_RATA # Premium prorated by time remaining
)
print(f"Base Premium: ${cat_layer.calculate_base_premium():,.0f}")
print(f"Reinstatements: {cat_layer.reinstatements}")
print(f"Max Total Coverage: ${cat_layer.limit * (1 + cat_layer.reinstatements):,.0f}")
Expected Output:
Base Premium: $100,000
Reinstatements: 2
Max Total Coverage: $15,000,000
The four reinstatement types are:
Type |
Behavior |
|---|---|
|
No reinstatements (layer exhausts permanently) |
|
Premium prorated based on time remaining in the policy period |
|
Full base premium charged regardless of timing |
|
Coverage restores at no additional cost |
3.6.2. Aggregate Limits
Aggregate limits cap the total payout from a layer across all claims in a policy year, regardless of individual claim sizes:
aggregate_layer = EnhancedInsuranceLayer(
attachment_point=100_000,
limit=5_000_000,
base_premium_rate=0.015,
limit_type="aggregate",
aggregate_limit=10_000_000 # Annual aggregate cap
)
3.6.3. Hybrid Limits
Hybrid layers combine per-occurrence and aggregate limits. Each individual claim is capped, and total annual payouts are also capped:
hybrid_layer = EnhancedInsuranceLayer(
attachment_point=100_000,
limit=5_000_000,
base_premium_rate=0.018,
limit_type="hybrid",
per_occurrence_limit=5_000_000, # No single claim pays more than $5M
aggregate_limit=15_000_000 # Total annual payouts capped at $15M
)
3.6.5. Building a Complete InsuranceProgram
Here is a realistic multi-layer program for NovaTech using all the advanced features:
program = InsuranceProgram(
layers=[
# Primary: per-occurrence, no reinstatements
EnhancedInsuranceLayer(
attachment_point=250_000,
limit=5_000_000,
base_premium_rate=0.025,
limit_type="per-occurrence",
),
# First excess: aggregate with 1 reinstatement
EnhancedInsuranceLayer(
attachment_point=5_250_000,
limit=5_000_000,
base_premium_rate=0.015,
limit_type="aggregate",
aggregate_limit=10_000_000,
reinstatements=1,
reinstatement_premium=1.0,
reinstatement_type=ReinstatementType.FULL,
),
# Catastrophe excess: aggregate with 2 free reinstatements
EnhancedInsuranceLayer(
attachment_point=10_250_000,
limit=10_000_000,
base_premium_rate=0.008,
limit_type="aggregate",
aggregate_limit=10_000_000,
reinstatements=2,
reinstatement_type=ReinstatementType.FREE,
),
],
deductible=250_000,
)
# Print program summary
print(f"Deductible: ${program.deductible:,.0f}")
print(f"Total Coverage: ${program.get_total_coverage():,.0f}")
print(f"Total Annual Premium: ${program.calculate_annual_premium():,.0f}")
3.6.6. Processing Claims Through InsuranceProgram
The InsuranceProgram.process_claim() method returns a detailed dictionary rather than a simple tuple:
# Process a $7M claim through the program
result = program.process_claim(7_000_000)
print(f"Total Claim: ${result['total_claim']:,.0f}")
print(f"Deductible Paid: ${result['deductible_paid']:,.0f}")
print(f"Insurance Recovery: ${result['insurance_recovery']:,.0f}")
print(f"Uncovered Loss: ${result['uncovered_loss']:,.0f}")
print(f"Layers Triggered: {len(result['layers_triggered'])}")
for layer_info in result['layers_triggered']:
print(f" Layer at ${layer_info['attachment']:,.0f}: paid ${layer_info['payment']:,.0f}")
You can also process an entire year of claims and get aggregate statistics:
# Reset for a fresh policy year
program.reset_annual()
# Process multiple claims in a single year
annual_claims = [500_000, 2_000_000, 8_000_000]
annual_result = program.process_annual_claims(annual_claims)
print(f"\n=== Annual Summary ===")
print(f"Total Losses: ${annual_result['total_losses']:,.0f}")
print(f"Total Deductible: ${annual_result['total_deductible']:,.0f}")
print(f"Total Recovery: ${annual_result['total_recovery']:,.0f}")
print(f"Base Premium: ${annual_result['base_premium']:,.0f}")
print(f"Reinstatement Premiums: ${annual_result['total_reinstatement_premiums']:,.0f}")
print(f"Net Benefit (Recovery - Premium): ${annual_result['net_benefit']:,.0f}")
3.8. Exercises
The following exercises build on the NovaTech scenario. Use the same financial profile (novatech_config) and loss parameters from this tutorial.
3.8.1. Exercise 1: Build a Custom Insurance Tower
NovaTech’s board wants a 3-layer insurance tower with the following specifications:
Retention: $500K self-insured retention
Primary layer: \(4.5M xs \)500K at a 3.0% rate
First excess: \(5M xs \)5M at a 1.5% rate
Second excess: \(15M xs \)10M at a 0.6% rate
Tasks:
Create the
InsurancePolicywith these three layers.Calculate the total annual premium and total coverage.
Process claims of \(200K, \)4M, \(9M, and \)22M through the tower. For each claim, print the company payment and insurance recovery.
Identify which claim sizes fall entirely within the deductible and which exceed the tower.
3.8.2. Exercise 2: Survival Rate Comparison
Compare three insurance strategies over a 30-year horizon using 10 different random seeds (seeds 0 through 9):
Strategy A: Uninsured – no insurance at all
Strategy B: Low Retention – \(100K deductible, \)5M limit, 2.5% rate
Strategy C: High Retention – \(1M deductible, \)5M limit (attachment at $1M), 1.5% rate
For each strategy and each seed, run a simulation with frequency=0.2, severity_mean=1_000_000, and severity_std=1_500_000. Track:
Survival count (how many of the 10 seeds survived all 30 years)
Mean final equity among survivors
Mean time-weighted ROE
Present your results in a table comparing the three strategies. Which strategy maximizes survival? Which maximizes long-term growth among survivors?
3.8.3. Exercise 3: Maximum Profitable Loading
Determine the maximum premium loading at which insurance still improves NovaTech’s time-average growth.
Using the base policy structure (\(5M limit, \)100K deductible) and loss parameters (frequency=0.15, severity_mean=1_000_000, severity_std=1_500_000):
Run simulations at premium loadings from 0% to 600% in 50% increments (i.e., rates ranging from
expected_loss / limitup to7 * expected_loss / limit).For each loading, calculate the time-weighted ROE over a 30-year horizon (use seed=42).
Plot or tabulate the results and identify the break-even loading where insurance growth advantage falls to zero.
How does the break-even loading change if you increase
severity_stdto $3,000,000 (higher tail risk)? Explain why.
Hint: Higher tail risk means larger potential losses, which increases volatility drag on uninsured growth. This should shift the break-even loading higher, as NovaTech can benefit despite even more expensive premiums when the downside is more severe.
3.9. Next Steps
Tutorial 4: Optimization Workflow – Use the optimizer to automatically find the best deductible and limit for your business
Tutorial 5: Analyzing Results – Deep dive into ergodic analysis, volatility drag, and DuPont decomposition
Tutorial 6: Advanced Scenarios – Monte Carlo simulations, market cycles, and multi-line programs