Step Composition¶
The Product Development Workflow¶
Real product development isn't linear. You build a base product, then explore variations: "what if we add a rider charge?", "what if we remove the admin fee?", "what if we replace flat crediting with an index strategy?"
In a spreadsheet, each variation is a new copy of the model. In code-based tools, each variation is a new function. Both approaches lead to duplicated logic where a single typo in one variant produces a silent error.
Building Variants from a Base¶
The rollforward builder is immutable — every method returns a new builder without modifying the original. This means you can create the base product once and derive all variations from it:
from gaspatchio_core.rollforward import Step
# Define the base UL product
base_ul = (
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)
)
# Variation 1: Add a rider charge before interest
ul_with_rider = base_ul.insert_before(
"Interest", Step.charge(af.rider_rate, "Rider Fee")
)
# Variation 2: Replace percentage admin with flat dollar fee
ul_dollar_admin = base_ul.replace(
"Admin", Step.subtract(af.admin_dollar, "Admin ($)")
)
# Variation 3: Remove admin entirely
ul_no_admin = base_ul.remove("Admin")
# Variation 4: Add surrender charge at the end
ul_with_surrender = base_ul.append(
Step.charge(af.surrender_rate, "Surrender Charge")
)
# Run all variants in one collect()
af.av_base = base_ul
af.av_rider = ul_with_rider
af.av_dollar = ul_dollar_admin
af.av_no_admin = ul_no_admin
af.av_surrender = ul_with_surrender
result = af.collect()
Each variant differs from the base by exactly one change. The diff is explicit and auditable — no scanning through 50 lines of near-identical code to spot what's different.
Composition Methods¶
| Method | What It Does |
|---|---|
.insert_before(label, step) |
Add a step before the named step |
.insert_after(label, step) |
Add a step after the named step |
.remove(label) |
Remove the named step |
.replace(label, step) |
Swap one step for another |
.prepend(step) |
Add a step at the beginning |
.append(step) |
Add a step at the end |
All composition methods return a new builder — the original is never modified.
Addressing Steps by Label¶
Steps are addressed by their label. Every step has one:
- If you provide a label:
Step.charge(af.admin_rate, "Admin")→ label is"Admin" - If you omit it:
Step.charge(af.admin_rate)→ label is auto-generated as"Charge(admin_rate)"
Labels must be unique within a builder. If you try to add a step with a duplicate label, you get an immediate error:
ValueError: Duplicate label 'Admin' in rollforward.
Already used at step 3.
For models where you plan to compose, use explicit labels — they're stable even if you rename columns.
The Step Factory¶
The Step class provides factory methods for creating steps to use with composition:
from gaspatchio_core.rollforward import Step
Step.add(af.premium, "Premium")
Step.subtract(af.expense, "Expense")
Step.charge(af.rate, "Fee")
Step.grow(af.rate, "Interest")
Step.grow_capped(af.rate, floor=0.0, cap=0.12, label="Index Credit")
Step.deduct_nar(af.coi_rate, death_benefit=af.sum_assured, label="COI")
Step.floor(0.0)
Step.cap(1_000_000.0, "Max AV")
Step.add_if(af.condition, af.amount, "Conditional Add")
Step.charge_if(af.condition, af.rate, "Conditional Charge")
Inspecting Variants¶
Use .explain() to verify the step order of any variant:
print(ul_with_rider.explain())
Rollforward: initial=av_init, 6 steps
Step Operation Label Formula
---- --------------------- ----------- ----------------------------------
1 Add(premium) Premium av[t] = av[t] + premium[t]
2 DeductNAR(coi_rate) COI av[t] = av[t] - coi_rate[t] * ...
3 Charge(admin_rate) Admin av[t] = av[t] * (1 - admin_rate[t])
4 Charge(rider_rate) Rider Fee av[t] = av[t] * (1 - rider_rate[t])
5 Grow(interest_rate) Interest av[t] = av[t] * (1 + interest_rate[t])
6 Floor(0) Floor(0) av[t] = max(av[t], 0)
The Rider Fee appears at step 4, exactly where insert_before("Interest", ...) placed it.