The Projection Space#
The by-policy monthly projection of the IUL_US_S model.
The Space is parameterized by point_id, so Projection[1] is an ItemSpace
projecting model point 1:
>>> Projection[1].result_av() # the account value roll-forward
>>> Projection[1].result_seg() # the segment ladder
>>> Projection.point_id = 2 # or switch the default
Input data
Inputs are external files: plain CSVs living in the model folder’s parent
directory, products/indexed_ul/, read at run time rather than stored inside the
model. The model folder therefore holds nothing but formulas – no _data/, no
IOSpec, no embedded values – so a diff of the model shows logic changes only, and an
input can be edited or swapped without rewriting the model. This follows
annuallife.TradLife_A; contrast basiclife.BasicTerm_S, which keeps its inputs
inside the model through modelx’s IOSpec machinery.
The consequence worth knowing: the model is not portable on its own. Copying the
IUL_US_S folder without its parent’s CSVs produces a model that reads and then
fails on first evaluation.
The readers live in the unparameterized Data Space, reached here
through the data Reference, so each file is read once per model rather than once
per model point:
Reference (on Data) |
Cells |
File |
|---|---|---|
model_point_file |
data.model_point_table() |
model_point_table.csv |
coi_rates_file |
data.coi_rates() |
coi_rates.csv |
corridor_file |
data.corridor_factors() |
corridor_factors.csv |
mort_table_file |
data.mort_table() |
mort_table.csv |
class_factor_file |
data.class_factor_table() |
class_factor_table.csv |
lapse_table_file |
data.lapse_table() |
lapse_table.csv |
surr_charge_file |
data.surr_charge_table() |
surr_charge_table.csv |
The index path is not a file. The base deterministic run generates
index_level() from a level annual return, and a historical or simulated path is
substituted by overriding that one cells.
Projection basis
t counts policy months, 1-based: t = 1 is the issue month of a
new-business model point, and for an in-force point it is the first projected month,
sitting duration_mth_init() completed months after issue. State variables the
notes define at their own t = 0 – FA(0), S(0), L(0), l(0) = 1 –
are the t == 0 branch of the corresponding recursion. The notes’ own month index
is therefore duration_mth(), not t.
Within each month the notes’ monthiversary order is followed exactly:
Anniversary resets and premium. The attained age and the corridor factor move on the policy anniversary (
age(),corridor_factor()); the annual premium is received at BOM of the first month of each policy year and its load deducted (premium_pp(),prem_to_av_pp()), the net premium going to the fixed account.Segment maturity. A segment created twelve months earlier matures: its index credit is computed (
index_credit_pp()) and its whole value rolls into the fixed account (seg_roll_pp()), from which the sweep at step 6 re-enters it into a new segment under the standing instruction [std].Withdrawals and new loans are sourced fixed-account-first, then pro rata across live segments [S3] (
draw_from_fa_pp(),draw_from_seg_pp()); loan collateral moves intolca_pp().Death benefit and corridor on the post-premium, post-withdrawal account value (
db_pp()).Net amount at risk, the death benefit discounted one month at the guaranteed rate less the account value measured before the deduction (
net_amt_at_risk()) – the universal life base convention.Monthly deduction (
mth_deduction_pp()), sourced the same way as step 3 (mth_deduction_from_fa_pp(),mth_deduction_from_seg_pp()), then the shortfall / no-lapse test (is_shortfall()).Sweep.
w_ixtimes the remaining fixed-account balance creates the new segment of this month (sweep_pp(),seg_new_pp()).Interest, EOM. One month at the fixed-account rate on the post-sweep fixed balance and at the loan collateral rate on the collateral account (
inv_income_pp()); segments earn no interim interest [S2]; loan interest accrues (loan_bal_pp()).Decrements, EOM, death before lapse (
pols_death(),pols_lapse()).
Cash flows are undiscounted. Premiums, expenses and premium taxes fall at BOM and
are weighted by pols_if(), the count in force at the start of month t;
death claims by pols_if(t) * mort_rate_mth(t); surrender payments by
pols_if(t) * (1 - mort_rate_mth(t)) * lapse_rate_mth(t); and partial withdrawals by
pols_if(t) again, because a withdrawal is taken at BOM on the owner’s election and
is not a decrement.
Naming
Cells names follow UL_US_S – the chassis these notes defer to – and
through it lifelib’s basiclife.BasicTerm_S and savings.CashValue_SE: pols_*
for policy counts, av_* for account values, plural nouns for cash flows, *_rate
for rates, *_pp for per-policy amounts, timing and kind string arguments.
Every concept shared with the chassis carries the chassis name. The names that are new
are the ones the indexed crediting engine needs: the fixed (holding) account, the
segment ladder, the crediting formula, and the no-lapse guarantee test.
Notes symbol |
Cells |
Meaning |
|---|---|---|
t |
(the |
Policy month, 1-based |
(notes’ own t) |
duration_mth(t) |
Completed policy months |
y |
policy_year(t) |
Policy year, 1-based |
(y - 1) |
duration(t) |
Completed policy years |
(issue age) |
age_at_entry |
Issue age (ANB) |
Sex |
sex |
Sex, M or F |
Risk class |
rate_class |
Underwriting class, spec Table 1 |
DB option |
db_option |
Death benefit option, A or B |
Tax test |
qual_test |
IRC 7702 test; only GPT modeled |
Premium mode |
premium_mode |
ANNUAL [std] or MONTHLY |
x |
age(t) |
Attained age (ANB) |
(none) |
proj_len |
Last projected month |
F |
sum_assured |
Initial face amount |
F(t) |
sum_assured_at(t) |
Face after reductions |
F/1000 |
units(t) |
Face in $1,000 units |
P_t |
premium_pp(t) |
Gross premium per policy |
(planned) |
premium_pp_ann |
Planned annual premium |
(persistency) |
prem_persistency(t) |
Paid/planned factor 0.98^(y-1) |
l_prem |
load_prem_rate() |
Premium load rate |
NP_t |
prem_to_av_pp(t) |
Net premium to the accounts |
W_t |
wd_pp(t) |
Partial withdrawal, gross of fee |
(the $25 fee) |
wd_fee_pp(t) |
Withdrawal fee |
B_t |
loan_new_pp(t) |
New standard loan |
W_t + B_t |
draw_pp(t) |
The whole step-3 draw |
W^FA_t + B^FA_t |
draw_from_fa_pp(t) |
Step-3 draw sourced from FA |
W^seg_{k,t} + B^seg_{k,t} |
draw_from_seg_pp(t) |
Step-3 draw sourced from segments |
(segment k’s share) |
seg_wd_pp(t, m) |
Segment m’s part of that draw |
(cumulative) |
seg_wd_cum_pp(t, m) |
Draws out of segment m to date |
FA_t |
fa_pp(t) |
Fixed (holding) account |
(FA intra-month) |
fa_pp_at(t, timing) |
Fixed account by timing |
S_{k,t} |
seg_bal_pp(t, m) |
Balance of the segment born at m |
Sweep_t |
sweep_pp(t) |
Sweep into a new segment |
S_{k,m_k} |
seg_new_pp(m) |
Segment balance at creation |
(sum of segments) |
seg_bal_tot_pp(t) |
All live segment balances |
(active pool) |
seg_bal_active_pp(t) |
Segments available to a draw |
(pro-rata share) |
seg_share(t, m) |
Segment m’s share of that pool |
(segment count) |
seg_count(t) |
Live segments, at most 12 |
w_ix |
index_alloc_rate() |
Indexed allocation share |
I(t) |
index_level(t) |
Index level at monthiversary t |
r_k |
index_change(i_0, i_1) |
Point-to-point index change |
(index path) |
index_return_ann |
Level annual index return [std] |
c |
index_cap_at(t) |
Cap in force |
p |
index_par |
Participation rate |
f |
index_floor |
Floor |
cr_k |
index_credit_rate(r) |
max(f, min(c, p x r)) |
(credit base) |
seg_credit_base(…) |
Balance the credit applies to |
Credit_k |
index_credit(…) |
Index credit, engine form |
(matured value) |
seg_matured_value(…) |
Value rolling into the next segment |
(the segment’s r) |
seg_return(t) |
Index change of the segment maturing at t |
(credit at t) |
index_credit_pp(t) |
Credit added at BOM of month t |
Roll^FA_t |
seg_roll_pp(t) |
Matured value rolled into FA |
LCA_t |
lca_pp(t) |
Loan collateral account |
Existing loan balance |
loan_bal_init |
L(0), the opening loan balance |
L_t |
loan_bal_pp(t) |
Loan principal plus interest |
i_L^c |
loan_rate_ann |
Loan charged rate |
i_L^e |
loan_credit_rate_ann(t) |
Collateral credited rate |
AV_t |
av_pp(t) |
Account value, end of month |
AV_{t-1} |
av_pp_at(t, “BEF_PREM”) |
Account value, start of month |
(after premium) |
av_pp_at(t, “BEF_CREDIT”) |
Before the maturity credit |
(after credit) |
av_pp_at(t, “BEF_WD”) |
Before the withdrawal |
AV’_t |
av_pp_at(t, “BEF_FEE”) |
After premium and withdrawal |
AV’_t - MD_t |
av_pp_at(t, “BEF_INV”) |
After the monthly deduction |
(aggregate AV) |
av_at(t, timing) |
Account value in force |
(none) |
av_change(t) |
Change in account value |
i_fix |
fixed_rate_ann(t) |
Fixed-account annual rate |
i_g |
guar_rate_ann |
Guaranteed annual rate |
(i_fix monthly) |
inv_return_mth(t) |
Monthly fixed-account rate |
(i_g monthly) |
guar_rate_mth() |
Monthly guaranteed rate |
v_g |
naar_factor() |
1 + i_gm; DB is divided by it |
(interest credited) |
inv_income_pp(t) |
Interest on FA and LCA |
e_pol |
expense_pol_mth_at(t) |
Monthly policy fee |
e_unit |
expense_unit_mth_at(t) |
Per-unit charge per $1,000 |
(none) |
rider_charge_pp(t) |
Rider charges (0) |
(e_pol + e_unit + rc) |
maint_fee_pp(t) |
Non-COI monthly charges |
coi_t |
coi_rate(t) |
Current monthly COI rate |
(guaranteed COI) |
coi_rate_guar(t) |
Guaranteed maximum COI rate |
COI_t |
coi_pp(t) |
Cost of insurance charge |
NAAR_t |
net_amt_at_risk(t) |
Net amount at risk |
MD_t |
mth_deduction_pp(t) |
Monthly deduction |
MD^FA_t |
mth_deduction_from_fa_pp(t) |
Deduction sourced from FA |
MD^seg_{k,t} |
mth_deduction_from_seg_pp(t) |
Deduction sourced from segments |
(segment k’s share) |
seg_ded_pp(t, m) |
Segment m’s part of the deduction |
(cumulative) |
seg_ded_cum_pp(t, m) |
Deductions charged to segment m |
DB_t |
db_pp(t) |
Death benefit after corridor |
kappa_x |
corridor_factor(t) |
IRC 7702(d) corridor factor |
SC_t |
surr_charge_pp(t) |
Surrender charge scheduled |
(SC per $1,000) |
surr_charge_rate(t) |
Surrender charge rate |
(AV - SC) |
csv_pp(t) |
Cash value before loan |
CSV_t |
ncsv_pp(t) |
Cash surrender value, loan netted |
(SC retained) |
surr_charge(t) |
Surrender charge collected |
CumP_t |
cum_prem_net_pp(t) |
Premiums less withdrawals and loans |
(GPT/7-pay base) |
cum_prem_pp(t) |
Premiums less withdrawals |
CumMNLP_t |
cum_mnlp_pp(t) |
Cumulative no-lapse premium |
(MNLP rate) |
mnlp_rate() |
No-lapse premium per $1,000 p.a. |
(MNLP monthly) |
mnlp_pp_mth() |
Monthly no-lapse premium |
(no-lapse period) |
nlg_period_years() |
Length of the no-lapse period |
(no-lapse test) |
nlg_test_ok(t) |
CumP >= CumMNLP |
(NLG status) |
nlg_in_effect(t) |
In period and test passing |
(NLG expiry shock) |
nlg_expiry_shock(t) |
25% shock at expiry [std] |
(grace trigger) |
is_shortfall(t) |
CSV cannot cover MD, NLG failed |
(GPT limit) |
gpt_limit(t) |
max(GSP, GLP x years) |
(GPT test) |
gpt_ok(t) |
GPT compliance flag |
(7-pay limit) |
seven_pay_limit(t) |
7-pay cumulative limit |
(MEC flag) |
is_mec(t) |
7-pay failure flag |
q^d_t |
mort_rate_mth(t) |
Monthly mortality rate |
(annual q) |
mort_rate(t) |
Annual mortality rate |
(base lapse) |
lapse_rate_base(t) |
Base annual lapse rate |
(SC-expiry spike) |
lapse_rate_sc_mult(t) |
Year-11 shock multiplier |
r_cred,t |
cred_rate_ann(t) |
Trailing credited rate |
r_alt |
comp_rate_ann(t) |
Competitor / market rate |
(dynamic multiplier) |
lapse_rate_dyn_mult(t) |
Dynamic lapse multiplier |
q^w_t |
lapse_rate_mth(t) |
Monthly lapse rate |
(annual w) |
lapse_rate(t) |
Total annual lapse rate |
l_t |
pols_if(t) |
In force at start of month t |
(l_0) |
pols_if_init |
In force at outset |
(deaths) |
pols_death(t) |
Deaths in month t |
(lapses) |
pols_lapse(t) |
Lapses in month t |
(none) |
pols_maturity(t) |
Maturities: always zero |
(premium income) |
premiums(t) |
Premium income |
DB_t - L_t |
claims(t, “DEATH”) |
Death claims |
CSV_t |
claims(t, “LAPSE”) |
Surrender payments |
W_t - fee |
withdrawals(t) |
Withdrawal payments |
E_t |
expenses(t) |
Insurer’s own expenses |
(premium tax) |
premium_taxes(t) |
Premium tax |
(net loan cash flows) |
net_loan_cf(t) |
Loan advances and repayments |
CF_t |
net_cf(t) |
Net liability cash flow |
Seven names needed care.
A withdrawal is not a claim. The notes write the withdrawal outflow as its own term
of CF_t, alongside the death and surrender legs, and the library follows that: the
payment is withdrawals(), published in a withdrawals column, and
"WITHDRAWAL" is not a kind that claims() accepts – it would otherwise
sit in the kind is None total and be double-counted against the standalone cells.
The per-policy amount is still claim_pp() (t, "WITHDRAWAL"), because that is
where the W_t - $25 fee rule lives and withdrawals() weights it by
pols_if(): a withdrawal is taken at BOM by policies still in force, not by a
decrement. Surrenders keep the "LAPSE" kind and are published as claims_lapse,
so the column name matches the kind that produces it.
The notes’ ``t`` is not this model’s ``t``. The notes index policy months from
t = 0 at issue; here t is 1-based, so the notes’ index is
duration_mth(). Every mod 12 test – the anniversary premium, the segment
maturity – is written against duration_mth(t), and the surrender charge run-off
uses duration_mth(t) + 1 because the notes’ schedule counts the current month.
The notes’ ``CSV_t`` is :func:`ncsv_pp`, not :func:`csv_pp`. The notes define
CSV_t = AV_t - SC_t - L_t, with the loan already netted. The chassis and
CashValue_SE split that into csv_pp() (AV - SC) and ncsv_pp()
(CSV - L). The chassis names are kept, and the notes’ symbol maps to the second.
The withdrawal fee is inside ``W_t``, not on top of it. These notes say W_t is
“gross of $25 fee” and that the “withdrawal outflow = W_t - $25 fee”. The account is
therefore debited W_t, the policyholder receives W_t - 25 and the insurer keeps
the fee. The universal life chassis reads its own notes the other way – there the fee
is charged on top of the withdrawal – so av_pp_at() deliberately differs from
the chassis at "BEF_FEE".
The death claim nets ``L_t``, not ``L(t-1)``. These notes write
death claim outflow = DB_t - L_t; decrements are end-of-month events, after the
loan has accrued, so claim_pp() uses loan_bal_pp() at t. The chassis
uses the opening balance because its own notes write L(t-1).
``maint_fee`` is income, ``expenses`` is outgo. As on the chassis,
maint_fee_pp() is the non-COI part of the monthly deduction – a charge against
the account value – while expenses() is the insurer’s own $75 a year plus $150
at issue [std]. Confusing them double-counts.
The two cumulative premium accumulators are different. cum_prem_net_pp() is
the notes’ CumP_t, premiums less withdrawals and loans, which is what the
no-lapse test compares against CumMNLP_t. cum_prem_pp() is premiums less
withdrawals only, which is the base of the guideline premium and 7-pay tests. Using one
for the other quietly changes both answers.
The segment ladder
A segment is created at every monthiversary from the sweep and matures twelve months
later, so between step 2 and step 7 of any month eleven segments are live and after the
sweep twelve are – the “up to 12 concurrent segments” of [S3][S4]. Segments are indexed
by their creation month m: seg_bal_pp() (t, m) is the balance at the end
of month t of the segment born at m, and is zero outside m <= t <= m + 11.
Two properties of this arrangement are worth stating because the notes list both as pitfalls.
Each segment carries its own index start level I(m), so twelve segments a year
mature at twelve different point-to-point returns. Collapsing them into one annual
segment mis-times credits and distorts mid-segment surrender values [S3];
check_seg_ladder() and model point 3 (indexed allocation 0%) exist so the
difference is visible rather than assumed.
Deductions and withdrawals are sourced from the fixed account first and then pro rata
across live segments on their remaining balances [std]. Because a pro-rata draw
leaves the proportions unchanged, one set of shares (seg_share()) serves both the
step-3 draw and the step-6 deduction. The two claims on the pool are ordered, not
parallel: the step-6 deduction is capped against what the step-3 draw leaves behind,
so their sum can never exceed the pool, no segment balance can go negative, and no
segment can pay a negative index credit at maturity. Capping both against the same
pre-draw pool is the easy mistake, and check_seg_ladder() now asserts against it.
The credit base under the baseline convention
is exactly the remaining balance – amounts that left the segment mid-term earn no index
credit [S3]. The alternative convention the notes price, Transamerica’s
adjusted-beginning-value with half-weighted deductions [S3], is the "ADJ_BEGIN"
argument of seg_credit_base(), and both are asserted against the worked example.
Cells Descriptions#
- db_option()[source]#
The death benefit option:
"A"(level) or"B"(face plus account value).The Graded and Return of Premium options are minority designs excluded from the baseline by the product spec (F3).
- qual_test()[source]#
The IRC 7702 qualification test elected at issue; only
"GPT"is modeled.GPT is the baseline (spec F4) because the Overloan Protection Rider attaches only to GPT non-MEC policies [S3]; CVAT is documented as a variation, so
corridor_factor()raises on anything else rather than silently applying GPT corridor factors.
The premium pattern:
"LEVEL","SINGLE"or"TARGET"[std].
The premium mode:
"ANNUAL"[std] baseline, or"MONTHLY".The notes fix the baseline at annual premiums paid at BOM of policy month 1 of each policy year [std]; monthly mode is carried so the chassis’ level-pay pattern remains reachable.
The planned annual premium per policy; a billing target only, premiums are flexible.
- index_alloc_rate()[source]#
w_ix: the share of the sweepable fixed-account balance moved to the indexed account.
100% in the baseline [std]. A model point at 0% never creates a segment and accumulates entirely at the fixed-account rate, which is the control run for the segment ladder.
- av_pp_init()[source]#
AV(0): the account value per policy at the outset, 0 at issue.
For an in-force model point the whole opening balance is placed in the fixed account net of any loan collateral [std]: the notes give no opening segment ladder, so a projection from an in-force cell starts with no live segments and rebuilds the ladder over its first twelve months.
- duration_mth_init()[source]#
Completed policy months already elapsed when the projection starts.
0 for a new-business model point, so that
t = 1is the issue month; positive for an in-force cell. This is the notes’ issue-date / duration offset attribute.
- wd_first_year()[source]#
The first policy year in which a withdrawal may be taken.
2 in the shipped points: withdrawals are allowed after the free-look period [S3] and the model takes that as the first anniversary [std].
- loan_first_year()[source]#
The first policy year in which the distribution-scenario loan is drawn.
The notes’ loan utilization module borrows a level amount annually from a start age, e.g. 65 [std]; 21 for issue age 45 is that age expressed as a policy year.
- mnlp_rate()[source]#
The minimum monthly no-lapse premium rate per $1,000 of face, annualized [S3].
20.80 for M / Non-Tobacco / 45 / band 1 [S3]. The 250,000 example face is a higher band whose rate is not public, so the band-1 rate stands in [std].
- gsp()[source]#
The guideline single premium (GPT compliance input) [std] placeholder.
No retrieved source gives guideline premiums for this composite, so the shipped value is a plausible placeholder, not a computed IRC 7702 figure.
- duration_mth(t)[source]#
The notes’ own month index: completed policy months at the beginning of month t.
duration_mth_init() + t - 1, so it is 0 in the issue month of a new-business model point. Anniversary events testduration_mth(t) % 12 == 0.
- age(t)[source]#
The attained age (ANB) in policy month t:
age_at_entry() + duration(t).Attained age increments on the policy anniversary, not on the birthday, which is the ANB convention the whole model is built on [std] (spec Table 1, F1).
- proj_len()[source]#
Projection length in policy months.
12 * (maturity_age - age_at_entry()) - duration_mth_init(): the projection runs to attained age 121 [std], the notes’ [unverified] maturity inference (spec F5). The contract itself is described as continuing in force with no further charges at 121, so the horizon truncates the run rather than terminating the policy;pols_maturity()is identically zero.
- is_guaranteed_basis()[source]#
Whether the run uses class (a) contractual guarantees only.
basisis"CURRENT"in the base run, so the current non-guaranteed scales apply. Setting it to"GUARANTEED"switches the fixed-account rate to 1.00%, the cap to 2.00%, the premium load to 8%, the policy fee to $15, the per-unit charge to $0.40 in all years and the COI scale to its guaranteed maximum – the notes’ “guaranteed-basis projections use only this class”. Key sensitivity 1 in the notes is exactly how far apart the two runs are.
- load_prem_rate()[source]#
l_prem: the premium load rate, 5% current [std] / 8% guaranteed [std] (F12).
The current rate sits in the model point table because the load is itself a non-guaranteed element re-declarable under ASOP 2 [REG-R26].
- fixed_rate_ann(t)[source]#
i_fix: the fixed (holding) account annual effective rate.
4.50% current [S2], 1.00% guaranteed [S2]. A non-guaranteed element; the base deterministic run holds the snapshot level, as the notes prescribe.
- inv_return_mth(t)[source]#
The monthly fixed-account rate,
(1 + i_fix)^(1/12) - 1[std] conversion.0.0036748 at the 4.50% current rate [S2]. The name follows
CashValue_SE; only the fixed account and the loan collateral account earn it – segments earn no interim interest at all, which is what the 0% floor design buys [S2].
- guar_rate_mth()[source]#
i_gm: the monthly guaranteed rate,
(1 + i_g)^(1/12) - 1at i_g = 1.00% [S2].
- naar_factor()[source]#
1 + i_gm;v_g = 1 / naar_factor()is the notes’ one-month discount.The death benefit is discounted one month at the guaranteed rate, never the credited or index rate – the universal life base convention, carried over unchanged. 1.0008295 at the 1.00% guarantee [S2].
- loan_rate_mth()[source]#
The monthly charged loan rate,
(1 + i_L^c)^(1/12) - 1at 3.00% [std] (F18).
- loan_credit_rate_ann(t)[source]#
i_L^e: the rate credited on loan collateral, 2.00% years 1-10, 3.00% after [std].
The standardization keeps the universal carrier pattern of a ~1% net loan spread early grading to a 0% “wash” from year 11 [S3][S5][S7] (F18).
- index_cap_at(t)[source]#
c: the cap in force, 10.00% current [S2] / 2.00% guaranteed [S2] (F8).
The current cap is a snapshot: observed 10.00%-13.75% across carriers and print dates [S2][S3][S4][S5][S7], and re-declared at every segment start [S3][S4]. The base run holds it level; a cap re-declaration module driven by an option budget is described in the notes but not implemented, because the notes supply neither an option pricing model nor a net investment earnings rate path.
- index_level(t)[source]#
I(t): the S&P 500 price-return index level at monthiversary t [S2][S3].
The base deterministic run grows the index at a level
index_return_anna year:index_level_init * (1 + index_return_ann)^(duration_mth(t) / 12), so every segment matures at the same point-to-point return. 6.40% is the carrier-published 1988-2023 lookback for the 10%-cap account [S2].This cells is the single point of substitution. Override it with a historical or simulated path and the whole segment ladder follows: the notes’ stochastic module (real-world lognormal, mu = 6.0%, sigma = 16% [std]) is a driver around the model, not a formula inside it. Remember what the notes say about the level rate: AG 49-A bounds what may be illustrated, not what will be credited [R1][R6], and because the cap truncates the right tail while the floor only offsets the left, a deterministic run at the illustrated rate overstates credits against a stochastic mean at matched expected index growth.
- index_change(index_start, index_end)[source]#
r = I(m+12) / I(m) - 1: the point-to-point index change over a segment term.
Price return, dividends excluded [S2][S3]. Takes the two index levels rather than a month so that the worked example’s own levels – 4,500.00 to 5,040.00 in scenario A, to 3,825.00 in scenario B – can be put through the same formula the projection uses.
- index_credit_rate(index_return, cap=None)[source]#
cr_k = max(f, min(c, p x r)): the credited rate for one segment [S2][S3][R1].
Floor 0%, participation 100%, cap per
index_cap_at()[S2].capdefaults to the cap in force, which the base run holds level, so the default is read att = 1; passindex_cap_guarfor the guaranteed-basis credit – the notes price both on the worked example, at 10.00% and 2.00%.This is where the two-sided truncation lives: the cap binds on the way up, the floor on the way down, and the historical frequency of a 0% credit for a single index allocation was 12.55%-23.81% in a carrier’s 2005-2017 issue-date study [S8].
- seg_credit_base(seg_bal_init, seg_deductions, conv=None)[source]#
The segment balance the index credit is applied to.
"REMAINING"[std], the baselineseg_bal_init - seg_deductions: the actual balance left at maturity, so amounts withdrawn, borrowed or deducted mid-segment earn no index credit (withdrawal and loan forfeiture [S3]; extension to deductions [std])."ADJ_BEGIN", the documented variationseg_bal_init - seg_deductions / 2: Transamerica’s contractual adjusted beginning value, which subtracts withdrawals and loan transfers in full but one half of the monthly deductions taken during the segment [S3]. Passseg_bal_initalready net of withdrawals and loan transfers, asindex_credit_pp()does.
The notes list picking one and keeping the death benefit, cash value and credit formulas consistent with it among the modeling pitfalls;
seg_credit_base_convis the Reference that records the choice.
- index_credit(seg_bal_init, seg_deductions, index_return, conv=None, cap=None)[source]#
Credit_k = cr_k x credit base: the index credit paid at a segment’s maturity.
The engine form, taking the segment’s opening balance (net of any withdrawals and loan transfers), the deductions charged to it over its term, and its point-to-point index change.
index_credit_pp()is the projection’s caller.
- seg_matured_value(seg_bal_init, seg_deductions, index_return, conv=None, cap=None)[source]#
The matured segment value: remaining balance plus the index credit.
S_{k,m+12} x (1 + cr_k)under the baseline credit base, which is the value that rolls into the fixed account and is swept back into a new segment under the standing allocation instruction [std] (spec F11).
- prem_persistency(t)[source]#
The fraction of the planned premium actually paid in policy year y [std].
0.98^(y - 1): the notes give premium persistency in closed form rather than as a table, compounding at 98% a year. Premium persistency is the behavior dimension unique to flexible-premium products; the 2015-2021 LIMRA/SOA UL study is the recommended public calibration base [REG-R21] and the level here is a placeholder.The funding-stop state the notes pair with this factor – probability 1% a year, after which the policy runs charge-only – is a second account value path the notes do not say how to blend with the first, and is not implemented.
P_t: the gross premium per policy received at BOM of policy month t.
Annual mode [std]: the whole planned premium falls at BOM of the first month of each policy year,
duration_mth(t) % 12 == 0, and every other month is zero.LEVELplanned annual premium timesprem_persistency().SINGLEone premium in the issue month, capped at the guideline single premium.TARGETasLEVELbut capped so cumulative premium stays inside the guideline premium limit [std]; the cap looks atcum_prem_pp(t - 1), so there is no circularity.Zero from attained age 121, when charges cease and premiums are no longer accepted (spec F5) – inert in practice, because the projection horizon is that age.
- prem_to_av_pp(t)[source]#
NP_t = P_t (1 - l_prem): the net premium, credited to the fixed account.
The fixed account doubles as the interim / holding account: net premium sits there at the declared fixed rate until the next monthiversary sweep moves it into a segment [S1] [std] (spec F10).
Premium income at BOM of policy month t, weighted by the in force at BOM.
- wd_pp(t)[source]#
W_t: the partial withdrawal per policy at BOM of month t, gross of the fee.
A constant monthly amount from
wd_first_year(), taken from the model point’swd_ppcolumn and 0 in the baseline [std]: the notes give no withdrawal utilization pattern, so the mechanics are implemented and the behaviour is left to the data. Model point 4 switches it on.The account is debited
W_t; the policyholder receivesW_t - 25and the insurer keeps the $25 fee [S3]. This differs from the universal life chassis, where the fee is charged on top of the withdrawal – these notes write the withdrawal outflow asW_t - $25 fee, so the fee is insideW_there.
- wd_fee_pp(t)[source]#
The $25 withdrawal fee, charged only in a month with a withdrawal [S3].
Retained by the insurer, so it is part of
margin_expense(), not ofclaims(). It is not an extra debit to the account value: it comes out ofW_t.
- loan_new_pp(t)[source]#
B_t: a new standard loan taken at BOM of policy month t [std].
The notes’ distribution-scenario module borrows a level amount annually from a start age [S3]; here that is
loan_new_pp_annfrom the first month of policy yearloan_first_year(). Zero in the baseline – the notes set loan utilization to none – and switched on by model point 4.Participating (indexed) loans are a documented variation, not modeled: the baseline uses standard loans only [std] (F18), which decouples loan modeling from index scenarios.
- face_reduction_pp(t)[source]#
The face reduction a withdrawal forces under Option A, dollar for dollar [std].
Standard universal life practice, stated in the product spec but not explicit in the retrieved brochures. Under Option B the withdrawal reduces the account value only and this is zero.
- sum_assured_at(t)[source]#
F(t): the face amount after any withdrawal-driven reductions.
F(0) = sum_assured(); face increases, elective decreases and option changes are not modeled, so the only movement is the Option A withdrawal reduction.
- units(t)[source]#
The face amount in $1,000 units,
sum_assured_at(t) / 1000.The per-unit charge is quoted per $1,000 of face per month, so it multiplies this. The surrender charge is quoted per $1,000 of initial face, because face decreases do not reduce it [S3]; see
surr_charge_pp().
- corridor_factor(t)[source]#
kappa_x: the IRC 7702(d) corridor factor at the attained age [R4].
250% at ages 0-40 grading to 100% at 90-95. Ages beyond the table take its last row. Only the Guideline Premium Test is modeled (spec F4), so any other
qual_testraises rather than being treated as GPT.High funding drives the corridor rather than the face amount, which raises the death benefit, the net amount at risk and hence the cost of insurance: the notes list omitting this among the pitfalls, because it overstates late-duration account values and understates charges.
- db_pp(t)[source]#
DB_t: the death benefit per policy after the corridor test [S3][R4].
max(option_db, kappa_x x AV'_t)whereoption_dbis the face amount under Option A and face plus account value under Option B, andAV'_tis the account value after premium, the maturity credit and any withdrawal, and before the monthly deduction. Measuring the account value at that point is what removes the circularity: under Option B the death benefit depends on the account value and the net amount at risk on the death benefit, but with this ordering neither depends on the deduction.During a segment the death benefit reflects the segment balance without unrealized index credit [S3]: with a 0% floor design segments simply carry no interim interest, so nothing extra is needed here.
- net_amt_at_risk(t)[source]#
NAAR_t = max(0, DB_t x v_g - AV’_t), the universal life base convention.
The death benefit is discounted one month at the guaranteed rate (
naar_factor()) and the account value is measured before the monthly deduction. Using the credited or index rate in the discount, or the post-deduction account value, makes the cost of insurance implicit and requires iteration, and produces small systematic errors.
- coi_rate_scale()[source]#
The guaranteed maximum monthly COI scale for this model point’s cell.
A Series indexed by policy year, per $1,000 of net amount at risk, sliced once from coi_rates.csv for this
sex/rate_class/age_at_entry. The shipped table covers the notes’ anchor cell M / NT / 45 only; a model point on any other cell needs the table extended first.
- coi_rate_guar(t)[source]#
The guaranteed maximum monthly COI rate per $1,000 of net amount at risk.
The notes set the guaranteed basis at 2017 CSO ANB smoker-distinct ultimate [std]/[REG-R17]. That table is licensed and is not reproduced here: the shipped scale is illustrative [std], of realistic magnitude and shape, and is swapped by replacing coi_rates.csv. Policy years beyond the table take its last row.
- coi_rate(t)[source]#
coi_t: the monthly COI rate in force, per $1,000 of net amount at risk.
coi_curr_factor(65% [std]) times the guaranteed maximum on the current basis; the guaranteed maximum itself on the guaranteed basis. Carrier COI tables are not public [S3] and re-rating is governed by ASOP 2 [REG-R26], so the ratio is a pure modeling assumption and one of the notes’ dominant sensitivities.Note the units: per $1,000 of net amount at risk per month, so it is divided by 1,000 in
coi_pp(). It is not comparable withCashValue_SE.coi_rate, which is a rate per unit of account value.
- rider_charge_pp(t)[source]#
Rider charges, 0 in the baseline [std].
The notes carry this term in the monthly deduction so rider modules can be added without changing the recursion; every rider in the product spec is out of the baseline.
- expense_pol_mth_at(t)[source]#
e_pol: the monthly policy fee, $10 current [S3][S5] / $15 guaranteed [std] (F13).
- expense_unit_mth_at(t)[source]#
e_unit: the per-unit charge per $1,000 of face per month (F14).
$0.30 in policy years 1-10 and nothing thereafter on the current basis; $0.40 in all years on the guaranteed basis. The structure – charged for the first ten years currently, guaranteed for all years – is sourced [S3][S5]; the dollar scales live in policy data pages and are not public, so the levels are [std].
- maint_fee_pp(t)[source]#
The non-COI part of the monthly deduction per policy.
e_pol + e_unit x F/1000 + rider charges. The name followsCashValue_SE.maint_fee: this is a charge against the account value and therefore insurer income. It is notexpenses(), which is the insurer’s own outgo.
- maint_fee(t)[source]#
Non-COI monthly charges deducted from account values, for the policies in force.
- mth_deduction_pp(t)[source]#
MD_t = e_pol + e_unit x F/1000 x 1{yr<=10} + coi_t x NAAR_t / 1000.
maint_fee_pp()pluscoi_pp(), taken at BOM. The deduction at the start of month t pays for month t’s coverage.
- draw_pp(t)[source]#
The total amount leaving the fixed account and segments at step 3 of month t.
The withdrawal (gross of its fee) plus the collateral for any new standard loan. The loan collateral does not leave the account value – it moves into
lca_pp()– but it does leave the fixed account and the segments, and it is sourced by the same rule [S3].
- draw_from_seg_pp(t)[source]#
The part of the step-3 draw sourced pro rata from live segments [S3].
Only what the fixed account cannot cover, and never more than the live segments hold. Being step 3 it has the first claim on the pool; the step-6 deduction is capped against what is left after it (
mth_deduction_from_seg_pp()). Amounts leaving a segment mid-term earn no index credit [S3], which under the baseline credit base happens automatically.
- draw_from_fa_pp(t)[source]#
The part of the step-3 draw sourced from the fixed account, taken first [S3].
Defined as the residual so that the two sources always sum to
draw_pp()exactly; if neither the fixed account nor the segments can cover it, the fixed account carries the excess and goes negative rather than the draw being silently truncated.
- mth_deduction_from_seg_pp(t)[source]#
The part of the monthly deduction sourced pro rata from live segments [std].
The notes’ sourcing convention: fixed account first, shortfall pro rata across active segments. Never more than the segments still hold after the step-3 draw, which is why the cap is
seg_bal_active_pp(t) - draw_from_seg_pp(t)and not the pooldraw_from_seg_pp()was itself capped against: the withdrawal and the new loan collateral come out of the same segments earlier in the same month, so capping both against the pre-draw pool would let the two together take more than the segments hold. That drives a segment balance negative and, twelve months later, pays a negative index credit on it – a breach of the 0% floor ofcr_k = max(f, min(c, p x r))[S2][R1].check_seg_ladder()asserts both consequences away.Whatever the segments cannot cover stays with
mth_deduction_from_fa_pp(), so the fixed account carries the excess and goes negative rather than the deduction being silently truncated – the notes chargeMD_tin full and hand an uncovered deduction to the grace/lapse cascade, which is not implemented.Carrier practice differs – Transamerica instead half-weights in-segment deductions in its credit base [S3] – and the notes list picking one and keeping the formulas consistent among the modeling pitfalls; see
seg_credit_base().
- mth_deduction_from_fa_pp(t)[source]#
The part of the monthly deduction sourced from the fixed account, taken first.
The residual, so the two sources always sum to
mth_deduction_pp().
- seg_bal_active_pp(t)[source]#
The live segment balances available to a draw at BOM of month t.
The segments born at
t - 11 ... t - 1, valued at the end of montht - 1. The segment born att - 12is not in the pool: it matured at step 2 of this month and its value is already in the fixed account. The segment born attdoes not exist yet – it is created by the sweep at step 7.
Segment m’s pro-rata share of a draw at BOM of month t.
Its opening balance over the live pool. Because a pro-rata draw leaves the proportions unchanged, the same shares serve the step-3 draw and the step-6 deduction even though they are taken at different points in the month.
- sweep_pp(t)[source]#
Sweep_t = w_ix x FA balance after steps 1-6 [std] (spec F10).
The monthiversary sweep, which creates this month’s segment. Carrier practice varies – Pacific Life sweeps on the 15th [S1], Transamerica on the first day of a policy month [S3] – and the baseline standardizes on the monthiversary so segment dates align with monthly processing. Nationwide’s charge-holdback in the fixed strategy [S5] is a documented variation and is not modeled.
- seg_bal_pp(t, m)[source]#
S_{k,t}: the balance at the end of month t of the segment created at month m.
S_{k,t+1} = S_{k,t} - MD^seg - W^seg - B^seg, with no interim interest: the 0% floor design credits nothing during the segment term [S2], in contrast with Transamerica’s in-segment 0.75% [S3], which is a different guarantee and must not be mixed with a 0% annual floor.Zero outside
m <= t <= m + 11. A segment created atmis live through the end of monthm + 11and matures at step 2 of monthm + 12, before that month’s deduction – so it bears eleven in-segment deductions. The worked example stipulates twelve; see the README.
- seg_ded_cum_pp(t, m)[source]#
Deductions charged to segment m from its creation through the end of month t.
- seg_wd_cum_pp(t, m)[source]#
Withdrawals and loan transfers out of segment m through the end of month t.
- seg_count(t)[source]#
The number of segments with a positive balance at the end of month t.
At most
seg_term_mth(12), the contractual maximum [S3][S4]; zero throughout for a model point withindex_alloc_rate() == 0.
- seg_return(t)[source]#
r_k for the segment maturing at month t: its own point-to-point index change.
index_change(I(t - 12), I(t)). Each segment carries its own index start level, which is the whole point of a monthly ladder: collapsing twelve segments into one annual segment mis-times credits and distorts mid-segment surrender values [S3].
- index_credit_pp(t)[source]#
The index credit added to the account value at BOM of month t.
The segment created at
t - 12matures now: its credit isindex_credit()of its opening balance net of withdrawals and loan transfers, the deductions charged to it, and its own point-to-point return. Zero in the first twelve months, and in every month for a model point that never creates a segment.
- seg_roll_pp(t)[source]#
Roll^FA_t: the matured segment value rolled into the fixed account at step 2.
Remaining balance plus index credit. Under the standing allocation instruction it is swept straight back into a new segment at step 7 [std] (spec F11); routing it through the fixed account is what makes it available to pay this month’s deduction first, which is the notes’ own sourcing rule.
- fa_pp_at(t, timing)[source]#
FA: the fixed (holding) account balance at an intra-month point of month t.
The notes’ recursion
FA_{t+1} = [FA_t + NP_t - MD^FA - W^FA - B^FA - Sweep + Roll^FA] x (1+i_fix)^(1/12)unrolled, withtimingnaming the point just before each event:"BEF_PREM"The closing balance of the previous month,
FA_t."BEF_CREDIT"After the net premium, before the maturing segment rolls in.
"BEF_WD"After the roll, before the withdrawal and any new loan collateral.
"BEF_FEE"After the draw, before the monthly deduction.
"BEF_SWEEP"After the deduction, before the sweep. This is the balance the sweep takes
w_ixof."BEF_INV"After the sweep, before interest. Interest is credited on this balance; reversing the two overstates the account value by about one month’s interest on the deduction, every month.
- fa_pp(t)[source]#
FA_t: the fixed (holding) account balance at the end of policy month t.
FA(0)is the model point’s opening account value less its loan collateral: the notes give no opening segment ladder, so an in-force cell starts with everything in the holding account [std]. Thereafter the post-sweep balance plus one month’s interest at the fixed-account rate.
- lca_pp_at(t, timing)[source]#
LCA: the loan collateral account at an intra-month point of month t.
"BEF_PREM"is the closing balance of the previous month;"BEF_INV"adds the collateral for any new loan taken at step 3, which is the balance one month’s interest is credited on.
- lca_pp(t)[source]#
LCA_t: the loan collateral account at the end of policy month t.
Standard loans move loaned value out of the fixed account and the segments into a collateral account credited at
loan_credit_rate_ann()while the loan itself accrues at the charged rate; the collateral remains part of the account value, so a loan advance does not reduceAV– it reduces the cash surrender value through- L_t.LCA(0)is set equal to the model point’s opening loan [std].
- loan_bal_pp(t)[source]#
L_t: the policy loan balance at the end of policy month t.
L_{t+1} = (L_t + B_t) (1 + i_L^c)^(1/12), the notes’ own recursion: new loans from step 3 are added to principal and the whole balance accrues at the charged rate. Repayments are not modeled – the notes give no repayment pattern.
- inv_income_pp(t)[source]#
Interest credited to the account value per policy at EOM of policy month t.
The post-sweep fixed-account balance at the fixed-account monthly rate plus the loan collateral account at the collateral credited rate. Segments contribute nothing: they earn no interim interest under a 0% floor design [S2], and their whole return arrives as
index_credit_pp()at maturity. Keeping the two apart is what stops the floor being confused with an in-segment guarantee, which the notes list among the pitfalls.
- inv_income(t)[source]#
Interest credited to account values, for the policies in force.
Decrements fall after the credit, so every policy in force at BOM earns a full month’s interest.
- av_pp_at(t, timing)[source]#
AV: the account value per policy at an intra-month point of policy month t.
The account value is the fixed account plus every live segment plus the loan collateral account;
timingnames the point just before each BOM event:"BEF_PREM"Before the premium: the closing balance of the previous month,
AV_{t-1}."BEF_CREDIT"After the net premium, before the maturing segment’s index credit.
"BEF_WD"After the index credit, before the withdrawal. The roll of the matured segment into the fixed account is internal and does not change this total.
"BEF_FEE"After the withdrawal, before the monthly deduction. This is the notes’
AV'_t, the balance the death benefit, the corridor test and the net amount at risk are all measured against. A new loan does not change it: the collateral moves within the account value."BEF_INV"After the monthly deduction, before interest.
The end-of-month balance
AV_tisav_pp(), andcheck_av_components()asserts that it still equals the sum of its three parts.
- av_pp(t)[source]#
AV_t: the account value per policy at the end of policy month t.
AV(0) = av_pp_init(); thereafter the post-deduction balance plus one month’s interest. Interest is credited on the post-deduction balance, as on the universal life chassis; the index credit is not part of it – that arrives at BOM when a segment matures.
- av_at(t, timing)[source]#
Account value in force at an intra-month point of policy month t.
av_pp_at()times the number of policies in force, which is constant through the month because decrements are end-of-month events.timingtakes the same values asav_pp_at(), plus"EOM"for the closing balance before decrements.
- av_change(t)[source]#
Change in the account value in force over policy month t.
av_at(t + 1, "BEF_PREM") - av_at(t, "BEF_PREM"), followingCashValue_SE.
- surr_charge_rate(t)[source]#
SC per $1,000 of initial face in policy month t [std] scale (F17).
max(0, sc_init - (sc_init / runoff_years) x m / 12)wheremis the notes’ own month indexduration_mth(t) + 1– the current month counts. With the shipped schedule of $25.00 per $1,000 running off over 10 years this is $24.79 in the issue month, declining linearly to zero in the last month of policy year 10. The ten-year period is sourced [S1][S5][S7]; the dollar scale is not public and is a placeholder of realistic magnitude.
- surr_charge_pp(t)[source]#
SC_t: the surrender charge scheduled per policy in policy month t.
Quoted on the initial face amount, because face decreases do not reduce the surrender charge [S3][S7]. This is the schedule, not the amount collected: see
surr_charge().
- csv_pp(t)[source]#
AV_t - SC_t, floored at zero: the cash value before the loan is netted.The floor is [std]: the notes write the cash surrender value without one, but a negative value would be a payment from the policyholder. In the early policy years the scheduled surrender charge exceeds the account value and the floor binds, so the charge actually collected is the whole account value.
The notes’ own
CSV_t– which already nets the loan – isncsv_pp().
- ncsv_pp(t)[source]#
CSV_t = AV_t - SC_t - L_t: the cash surrender value, floored at zero.
This is what a surrendering policyholder is paid, and the notes’ surrender outflow.
- surr_charge(t)[source]#
Surrender charge actually collected from the policies lapsing in month t.
(AV_t - csv_pp(t)) x pols_lapse(t), so it is capped by the account value where thecsv_pp()floor binds. Insurer income, and part ofmargin_expense().
- cum_prem_pp(t)[source]#
Cumulative premiums less withdrawals: the base of the GPT and 7-pay tests.
CumPrem(0) = 0even for an in-force model point, because the notes give no opening accumulator [std], so the compliance flags are only meaningful for points projected from issue. The statutory “less a portion of withdrawals” is taken as the whole withdrawal [std].
- cum_prem_net_pp(t)[source]#
CumP_t: cumulative premiums less withdrawals and loans, for the no-lapse test.
The notes define the no-lapse accumulator this way and the guideline premium accumulator differently;
cum_prem_pp()is the other one. Using one for the other quietly changes both answers.
- nlg_period_years()[source]#
The length of the no-lapse period in policy years, by issue age [S3] (spec F6).
0-45: 20 years; 46-60: to attained age 65; 61+: 5 years. Comparators differ – Nationwide 20 years for issue ages 0-55,
75 - issue agefor 56-69, 5 years for 70+ [S5] – and the Transamerica structure is the one the baseline follows.
- mnlp_pp_mth()[source]#
The minimum monthly no-lapse premium per policy [S3].
mnlp_rate x F / 1000 / 12: the annual rate per $1,000 of face spread evenly over the year.
- nlg_in_effect(t)[source]#
Whether the no-lapse guarantee is in effect: inside the period and test passing.
While it is, the policy cannot lapse for insufficiency even with no cash surrender value [S3][S4], which is why
lapse_rate()suppresses voluntary lapse in that state as well [std].
- nlg_expiry_shock(t)[source]#
Whether the 25% no-lapse-expiry shock lapse applies in policy month t [std].
The notes apply a 25% shock lapse to underfunded policies on no-lapse expiry; “underfunded” is taken as a non-positive cash surrender value [std], and the shock is confined to the first policy year after the no-lapse period ends.
- is_shortfall(t)[source]#
The grace trigger: the cash surrender value cannot cover the monthly deduction.
AV'_t - SC_t - L_{t-1} < MD_tand the no-lapse test fails [S3][S4]. A diagnostic. The grace and lapse-for-insufficiency cascade (61 days, modeled as lapse att + 2months if unfunded [std]) is not implemented: the notes leave the in-grace account value treatment and the cash flow of a cure payment undetermined, so no policy is terminated for insufficiency here.
- gpt_ok(t)[source]#
Whether cumulative premium is still inside the guideline premium limit.
A compliance side-calculation with no cash flow of its own: a refused premium simply never enters the model.
- is_mec(t)[source]#
Whether the 7-pay test has failed by policy month t [R5].
A flag, not a cash flow: MEC status changes policyholder taxation, not insurer liability cash flows.
- mort_rate(t)[source]#
The annual best-estimate mortality rate in policy month t.
Base table times
class_factor()times the A/E factor, 100% in the base run [std] with no mortality improvement. The notes recommend 2015 VBT sex/smoker-distinct ANB validated against ILEC 2012-2019 [REG-R18][REG-R19]; those tables are licensed, so the shipped table is a small illustrative one [std]. Ages beyond the table take its last row, where the rate is 1.0.
- lapse_rate_base(t)[source]#
The base annual lapse rate by policy year [std].
6% in policy years 1-10, 4% thereafter, read from lapse_table.csv; policy years beyond the table take its last row. The levels are placeholders to be calibrated to the LIMRA/SOA persistency studies [REG-R20][REG-R21], whose detailed tables are behind a paid package.
- lapse_shock_year()[source]#
The policy year of the surrender-charge-expiry lapse spike.
The first policy year with no surrender charge – the run-off length plus one, which is 11 for the shipped ten-year schedule – derived from surr_charge_table.csv rather than hard-coded, so a different schedule moves the spike with it. Zero when the model point carries no surrender charge, which no policy year can equal.
- lapse_rate_sc_mult(t)[source]#
The surrender-charge-expiry spike multiplier, 2.0 in policy year 11 [std].
The ten-year surrender charge period [S1][S5][S7] creates a cliff in surrender economics; the size of the spike is a shape assumption, and UL lapse and surrender experience by duration is available in [REG-R21] for calibration.
- cred_rate_ann(t)[source]#
r_cred,t: the policy’s trailing credited rate, driving dynamic lapse [std].
The credited rate of the segment that matured this month once the ladder is running, and the fixed-account rate before that or when nothing is allocated to the indexed account. The notes say only “the policy’s trailing credited rate”; this is the reading the model uses.
- comp_rate_ann(t)[source]#
r_alt: the competitor / market alternative rate [std].
The base deterministic run sets it equal to the trailing credited rate, so
lapse_rate_dyn_mult()is exactly 1. Override this cells to switch dynamic lapse on. The rationale the notes give is real: caps are non-guaranteed elements and uncompetitive re-declarations – caps on one product fell from 13.75% to 12.00% between two print dates [S3][S4] – plausibly drive excess lapse.
- lapse_rate_dyn_mult(t)[source]#
The dynamic lapse multiplier [std].
min(2.0, max(0.5, 1 + 3.0 x (r_alt - r_cred,t))): a competitive credited rate halves lapse at the floor and an uncompetitive one doubles it at the cap. Equal to 1 throughout the base run.
- lapse_rate(t)[source]#
The total annual lapse rate [std].
Base rate times the surrender-charge-expiry spike times the dynamic multiplier, with two no-lapse-guarantee overrides the notes specify:
while the guarantee is in effect and the cash surrender value is non-positive, lapse is suppressed – policyholders paying no-lapse-premium-level premiums persist;
in the first policy year after the no-lapse period ends, an underfunded policy takes a 25% shock lapse.
lapse_rate_capis 1.0: these notes give no cap, unlike the universal life chassis, so the Reference is present for symmetry and inert.
- pols_if(t)[source]#
l_t: the number of policies in force at the beginning of policy month t.
Decrements are end-of-month events, so the number in force is constant through the month and every BOM cash flow is weighted by it.
pols_if(1) = pols_if_init().
- pols_if_at(t, timing)[source]#
Number of policies in force at time t, by
timing.All three
CashValue_SEtimings coincide for this product and equalpols_if(): there is no new business inside a projection, and the contract has no maturity that terminates coverage.
- pols_lapse(t)[source]#
Number of lapses at the end of policy month t.
pols_if(t) x (1 - q^d_t) x q^w_t: death is applied before lapse [std].
- pols_maturity(t)[source]#
Number of maturing policies: always zero.
At attained age 121 the policy is described as continuing in force with no further charges [unverified] (spec F5), so the projection horizon truncates the run rather than terminating the contract. The cells is kept so the in-force roll-forward identity has the same shape as in the term and annuity models of this library, where it is not zero.
- claim_pp(t, kind)[source]#
The claim amount per policy by
kind."DEATH"DB_t - L_t, the death benefit less policy debt. The loan is taken at its end-of-month value because decrements follow the loan accrual, which is how these notes write it."LAPSE"CSV_t, the cash surrender value with the surrender charge and the loan already netted –ncsv_pp()."WITHDRAWAL"W_t - $25 fee: the withdrawal is gross of the fee and the insurer keeps it. This kind survives here, whereclaims()no longer accepts it, because the per-policy rule has to live somewhere;withdrawals()is its only caller.
- claims_from_av(t, kind)[source]#
The part of a claim released out of the account value, by
kind.Death and lapse both release the end-of-month account value
AV_t, because decrements follow the interest credit."MATURITY"is zero: the contract does not mature within the projection.
- claims_over_av(t)[source]#
Death claims in excess of the account value released.
(claim_pp(t, "DEATH") - AV_t) x pols_death(t). The cost of insurance charge net of this is the mortality margin.
- claims(t, kind=None)[source]#
Claim outgo in policy month t, optionally by
kind.kindis"DEATH"or"LAPSE", orNonefor the total. Death claims are weighted bypols_death()and surrenders bypols_lapse(), both end-of-month events."WITHDRAWAL"is deliberately not accepted: a partial withdrawal is a payment on the owner’s election rather than a claim, so it is the standalone cellswithdrawals()and its ownresult_cf()column. Keeping it out of thekind is Nonetotal is what stops it being counted twice.
- withdrawals(t)[source]#
Partial withdrawal payments in policy month t:
(W_t - $25 fee) x l_t.Weighted by
pols_if(), because a withdrawal is taken at BOM by policies still in force – it is not a decrement. The per-policy amount isclaim_pp()(t, "WITHDRAWAL"), so the fee-inside-W_trule is stated once; the fee itself stays with the insurer aswd_fees().A withdrawal is not a claim, and this cells is why
claims()does not take a"WITHDRAWAL"kind.
- wd_fees(t)[source]#
Withdrawal fees retained by the insurer, for the policies in force.
Taken out of the withdrawal, so this is the difference between what leaves the account value and what the policyholder receives; it appears in
margin_expense(), not inclaims().
- net_loan_cf(t)[source]#
Net loan cash flow in policy month t: advances out, repayments in.
A loan advance is cash the insurer pays out while the account value is unchanged – the collateral moves inside it – so it is a liability cash flow of its own. Repayments are not modeled: the notes give no repayment pattern.
- inflation_factor(t)[source]#
The expense inflation factor,
(1 + inflation_rate)^(y - 1).inflation_rateis 0 in the base run: these notes give per-policy maintenance and per-issue expenses as flat placeholders with no inflation assumption, unlike the universal life chassis. The cells is kept so the chassis’ shape survives and an inflation assumption can be switched on with one Reference.
- expenses(t)[source]#
E_t: the insurer’s own expenses in policy month t [std].
$75a policy a year spread monthly, plus$150at issue. Placeholders: the notes have no public source for insurer expenses and say to calibrate to company studies.Not to be confused with
maint_fee(), which is the charge against the account value – income, not outgo.
Premium tax, 2.0% of premium [std] placeholder.
- margin_expense(t)[source]#
Expense margin: the charges the insurer keeps, net of its own outgo.
load x P + withdrawal fees + maint_fee + surrender charges - expenses - premium taxes. FollowsCashValue_SE.margin_expense; seecheck_margin()for the identity it takes part in.
- margin_mortality(t)[source]#
Mortality margin:
coi()net ofclaims_over_av().
- net_cf(t)[source]#
CF_t: the net liability cash flow in policy month t, undiscounted.
premiums - claims - withdrawals - expenses - premium taxes + net loan cash flows, which is the notes’ own sign convention (inflow positive) and the library-wide one.claims()is the death and surrender legs only, sowithdrawals()is a term of its own here rather than part of the claim total. Policy charges – the premium load, the monthly deduction, the surrender charge, the withdrawal fee – are internal transfers within the account value, not cash flows; they emerge in profit as the margins. Index credits and fixed-account interest are credits to the policyholder, not insurer cash flows, so they do not appear either – seecheck_margin()for how it all reconciles.
- check_av_components()[source]#
Check that the account value is still the sum of its three parts.
Returns
Truewhen, for every projected month,av_pp(t) == fa_pp(t) + seg_bal_tot_pp(t) + lca_pp(t).av_pp()is built from the notes’ aggregate recursion while the fixed account, the segment ladder and the loan collateral account are each built from their own; this is the test that the two constructions have not drifted, and it is the strongest single check on the segment bookkeeping.
- check_av_roll_fwd()[source]#
Check the account value roll-forward.
Returns
Truewhen, for every projected month, the opening account value in force of montht + 1equals:av_at(t, "BEF_PREM") + prem_to_av(t) + index_credits(t) - withdrawals(t) - wd_fees(t) - mth_deduction(t) + inv_income(t) - claims_from_av(t, "DEATH") - claims_from_av(t, "LAPSE")
This pins the notes’ processing order: the index credit arrives at BOM when a segment matures, interest is credited on the post-deduction balance, decrements come after both, and the withdrawal fee leaves the account value alongside the withdrawal itself.
- check_seg_ladder()[source]#
Check the segment ladder bookkeeping.
Returns
Truewhen, for every projected month, all four of the following hold:at most
seg_term_mthsegments are live;every segment that matured accounted for its creation balance exactly,
seg_new_pp(m) == seg_bal_pp(m + 11, m) + deductions + withdrawals– nothing may appear in or vanish from a segment except a deduction, a withdrawal or a loan transfer, because segments earn no interim interest;every live segment balance is non-negative;
every index credit is non-negative.
The last two are the 0% floor of
cr_k = max(f, min(c, p x r))[S2][R1] stated as an invariant. The accounting identity above closes even on a negative balance, andseg_count()counts only positive ones, so without these two an over-sourced draw could take a segment below zero and pay a negative credit on it at maturity without any check firing – seemth_deduction_from_seg_pp().
- check_margin()[source]#
Check the net cash flow against the expense and mortality margins.
Returns
Truewhen, for every projected month:net_cf(t) == margin_expense(t) + margin_mortality(t) + av_change(t) - inv_income(t) - index_credits(t) + loan_bal_pp(t) * pols_lapse(t) + net_loan_cf(t)
The terms after the two margins are what separates a gross liability cash flow model from
CashValue_SE, whosenet_cfalready nets the change in account value and the investment income.index_creditsjoinsinv_incomebecause the indexed account’s whole return arrives at maturity rather than monthly; the loan terms are the debt extinguished against the account value when a policy with a loan surrenders, and the advance itself. The identity holds while neither thecsv_pp()nor thencsv_pp()floor binds against a policy loan.
- result_cf()[source]#
Result table of cashflows, a DataFrame indexed by policy month
t.pols_ifis the number in force at the start of monthtand is the weight on that same row’s cash flows. The cash flow columns are income-positive and sum tonet_cf:premiums - claims_death - claims_lapse - withdrawals - expenses - premium_taxes + net_loan_cf. The surrender column isclaims_lapse, matching the"LAPSE"kind that produces it, and withdrawals are their own column because they are not claims.
- result_av()[source]#
Result table of the per-policy account value roll-forward.
The columns follow the notes’ processing order: opening account value, net premium, the index credit of any maturing segment, the account value before the deduction, the death benefit, the net amount at risk, the cost of insurance, the monthly deduction, the post-deduction balance, the interest credited, the closing account value, and then the surrender and loan values.
- result_seg()[source]#
Result table of the indexed segment ladder, a DataFrame indexed by
t.The index level, the fixed (holding) account, the sweep that creates this month’s segment, the total and count of live segments, the credit paid by the segment maturing this month and the return that produced it.