Skip to content

Choosing the Right Step

Decision Tree

The most common question: "I have a charge or credit — which method do I use?"

Is the amount proportional to AV or absolute?

 Proportional (e.g., "0.15% of account value")
   Reducing AV  .charge(rate)
   Growing AV   .grow(rate)

 Absolute (e.g., "$15 per month")
   Adding       .add(amount)
   Removing     .subtract(amount)

 Depends on AV and another variable (e.g., "rate * max(0, SA - AV)")
   .deduct_nar(rate, death_benefit=...)

 Rate with floor and cap (e.g., IUL crediting)
   .grow_capped(rate, floor=0.0, cap=0.12)

 Conditional on a flag (e.g., "only charge VUL policies")
   .add_if(condition, amount)
   .charge_if(condition, rate)

Method Reference

Absolute Operations

These add or remove a fixed dollar amount, independent of the current account value.

Method Formula Use When
.add(amount) AV += amount[t] Premium deposits, bonus additions
.subtract(amount) AV -= amount[t] Flat dollar fees, withdrawals
# Monthly premium deposit
af.av = (
    af.projection.rollforward(initial=af.av_init)
    .add(af.premium, "Premium")
)

# Flat monthly admin fee
af.av = (
    af.projection.rollforward(initial=af.av_init)
    .add(af.premium, "Premium")
    .subtract(af.admin_fee_dollar, "Admin Fee")
)

Rate Operations

These apply a percentage charge or credit relative to the current account value.

Method Formula Use When
.charge(rate) AV *= (1 - rate[t]) M&E charges, admin fee as % of AV
.grow(rate) AV *= (1 + rate[t]) Interest crediting, fund returns
.grow_capped(rate, floor, cap) AV *= (1 + clamp(rate[t], floor, cap)) IUL crediting with floor and cap
# M&E charge of 0.10% per month on a VUL
af.av = (
    af.projection.rollforward(initial=af.av_init)
    .charge(af.me_rate, "M&E Fee")
    .grow(af.fund_return, "Fund Return")
)

# IUL with 0% floor and 12% annual cap
af.av = (
    af.projection.rollforward(initial=af.av_init)
    .grow_capped(af.index_return, floor=0.0, cap=0.12, label="Index Credit")
)

State-Dependent Operations

The charge amount depends on the current account value — the defining characteristic of products that need rollforward() instead of accumulate().

Method Formula Use When
.deduct_nar(rate, death_benefit=db) AV -= rate[t] * max(0, db[t] - AV) COI on net amount at risk

The net amount at risk (NAR) is max(0, death_benefit - AV). When AV is close to the death benefit, NAR shrinks and COI charges drop. This feedback loop is why COI can't be pre-computed.

# Cost of Insurance on net amount at risk
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")
    .grow(af.interest_rate, "Interest")
)

Bounds

Method Formula Use When
.floor(value) AV = max(AV, value) Non-negative AV constraint
.cap(value) AV = min(AV, value) Maximum AV limit

Most UL contracts guarantee a non-negative account value. Always end with .floor(0) unless the product spec says otherwise.

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)  # AV can never go negative
)

Conditional Operations

Some charges only apply to certain policies or certain periods.

Method Formula Use When
.add_if(condition, amount) if cond[t] > 0: AV += amount[t] Conditional premium/bonus
.charge_if(condition, rate) if cond[t] > 0: AV *= (1 - rate[t]) Conditional charges

The condition is a list column of 1.0 (true) or 0.0 (false) values, one per projection period.

# M&E fee only for VUL policies in a mixed block
af.av = (
    af.projection.rollforward(initial=af.av_init)
    .add(af.premium, "Premium")
    .charge(af.admin_rate, "Admin")
    .charge_if(af.is_vul, af.me_rate, "M&E Fee")
    .grow(af.interest_rate, "Interest")
    .floor(0)
)

Control Flow

Method Formula Use When
.lapse_if_zero() if AV ≤ 0: zero remaining periods Single-state no-fund lapse
.capture(name) Snapshot current AV for later use Intermediate values, cross-state references

.lapse_if_zero() checks the account value after all preceding steps in the current period. If AV is non-positive, all remaining periods are zeroed. The current period's value is still recorded.

.capture(name) does not change AV — it stores the current value for use in downstream calculations or cross-state operations.

Multi-State Operations

These methods are only valid in multi-state rollforwards where two or more accounts interact.

Method Formula Use When
.on(state_name) Switch target state (sticky) Directing subsequent steps to a specific account
.ratchet_to(other_state) state = max(state, other_state) GMDB high-water mark
.pro_rata_with(capture_ref, amount) state *= (1 - amount[t] / ref_value) GMWB proportional benefit base reduction
.lapse_when(all_non_positive=[...]) Zero all states when all named states ≤ 0 Secondary guarantee coordinated lapse

.on(state_name) is sticky — once called, all subsequent steps target that state until you call .on() again with a different name. The first declared state is the default before any .on() call.

.ratchet_to(other_state) sets the current state to max(current_state, other_state). The other state's value is read at the point in the step sequence where the ratchet appears — so if AV has already been grown by fund return, the ratchet sees the post-growth value.

.pro_rata_with(capture_ref, amount) reduces the current state proportionally: state *= (1 - amount / captured_value). The capture_ref is the name of a previously captured value via .capture(). If the captured value is zero, the state is left unchanged (withdrawal against zero AV is a no-op).

.lapse_when() is a top-level configuration, not a step in the sequence. It is checked at the end of each timestep after all steps have executed. At most one lapse_when per rollforward. Only states named in the list are checked — other states continue normally. For single-state lapse, use .lapse_if_zero() instead.

rf = (
    af.projection.rollforward(av=af.av_init, guarantee=af.guarantee_init)
    .on("av")
    .add(af.premium, "Premium")
    .grow(af.fund_return, "Fund Return")
    .on("guarantee")
    .ratchet_to("av", "GMDB Ratchet")
    .lapse_when(all_non_positive=["av", "guarantee"])
)

af.av = rf["av"]
af.guarantee = rf["guarantee"]

See Multi-State Rollforwards for full VA + GMDB and GMWB examples.