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.