Skip to content

Product Recipes

Ready-to-use rollforward patterns for common life insurance products. Each recipe shows the standard within-period calculation order. Adapt column names to match your data.

Whole Life

Premiums, admin charges, and guaranteed interest. No net-amount-at-risk calculation because COI is typically bundled into the premium structure.

af.av = (
    af.projection.rollforward(initial=af.av_init)
    .add(af.premium, "Premium")
    .charge(af.admin_rate, "Admin")
    .grow(af.interest_rate, "Interest")
)

Universal Life

The standard UL rollforward. COI is charged on the net amount at risk, and AV is floored at zero.

af.av = (
    af.projection.rollforward(initial=af.av_init)
    .add(af.premium, "Premium")
    .deduct_nar(af.coi_rate, death_benefit=af.sum_assured, label="COI")
    .charge(af.admin_rate, "Admin")
    .grow(af.interest_rate, "Interest")
    .floor(0)
)

Step ordering matters: premium is added first (increasing AV and reducing NAR before COI is calculated), admin is charged after COI (so the fee applies to the post-COI balance), and interest credits the final balance.

Indexed UL (IUL)

Same as UL but with a floor-and-cap on the crediting rate. The policyholder participates in index returns but is protected on the downside and capped on the upside.

af.av = (
    af.projection.rollforward(initial=af.av_init)
    .add(af.premium, "Premium")
    .deduct_nar(af.coi_rate, death_benefit=af.sum_assured, label="COI")
    .charge(af.admin_rate, "Admin")
    .grow_capped(af.index_return, floor=0.0, cap=0.12, label="Index Credit")
    .floor(0)
)

The floor and cap parameters are the contractual bounds. A 0% floor means the policyholder never loses money from index performance (though charges still apply). A 12% cap means upside is limited even in strong market years.

Variable UL (VUL)

Like UL but with a mortality and expense (M&E) charge and fund-based returns instead of guaranteed interest.

af.av = (
    af.projection.rollforward(initial=af.av_init)
    .add(af.premium, "Premium")
    .deduct_nar(af.coi_rate, death_benefit=af.sum_assured, label="COI")
    .charge(af.me_rate, "M&E Fee")
    .charge(af.admin_rate, "Admin")
    .grow(af.fund_return, "Fund Return")
    .floor(0)
)

M&E is charged before the fund return so the fee reduces the base on which returns are calculated — matching how separate account charges work in practice.

Variable Annuity

Accumulation-phase VA with no COI (no death benefit risk charge during accumulation). M&E and fund return only.

af.av = (
    af.projection.rollforward(initial=af.av_init)
    .add(af.premium, "Premium")
    .charge(af.me_rate, "M&E Fee")
    .grow(af.fund_return, "Fund Return")
    .floor(0)
)

For VA products with guaranteed benefits (GMDB, GMWB), see Multi-State Rollforwards.

Credit Life

Reducing balance coverage where the benefit amount decreases with the loan balance. Simple charge and growth, no COI on NAR.

af.av = (
    af.projection.rollforward(initial=af.av_init)
    .charge(af.insurance_charge_rate, "Insurance Charge")
    .grow(af.interest_rate, "Interest")
)

Mixed Block with Conditional Charges

When modelling a block that contains both UL and VUL policies, use conditional charges for product-specific fees:

af.av = (
    af.projection.rollforward(initial=af.av_init)
    .add(af.premium, "Premium")
    .deduct_nar(af.coi_rate, death_benefit=af.sum_assured, label="COI")
    .charge(af.admin_rate, "Admin")
    .charge_if(af.is_vul, af.me_rate, "M&E Fee")  # VUL policies only
    .grow(af.interest_rate, "Interest")
    .floor(0)
)

The af.is_vul column is a list of 1.0 (VUL) or 0.0 (UL) values per period per policy. For policies where the condition is 0.0, the M&E charge step is skipped — AV passes through unchanged.

Participating Whole Life

Whole life with annual dividends added to the account value:

af.av = (
    af.projection.rollforward(initial=af.av_init)
    .add(af.premium, "Premium")
    .add(af.dividend, "Dividend")
    .charge(af.admin_rate, "Admin")
    .grow(af.interest_rate, "Interest")
)