The Projection Space#

The by-policy projection of the WholeLife_US_A model.

The Space is parameterized by point_id, so Projection[1] is an ItemSpace projecting model point 1:

>>> Projection[1].result_cf()          # the worked example's anchor cell
>>> 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/whole_life/, 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 WholeLife_US_A folder without its parent’s CSVs produces a model that reads and then fails on first evaluation.

Each table has a filename Reference and a reader Cells, both on the Data Space, reached here through the data Reference:

Reference

Cells

File

model_point_file

data.model_point_table()

model_point_table.csv

cv_file

data.cv_table()

cv_table.csv

nsp_file

data.nsp_table()

nsp_table.csv

np_guar_file

data.np_guar_table()

np_guar_table.csv

mort_table_file

data.mort_table()

mort_table.csv

premium_rates_file

data.premium_rates()

premium_rates.csv

Naming

Cells names follow lifelib’s basiclife.BasicTerm_S and savings.CashValue_SE wherever those models have an analogue — pols_* for policy counts, plural nouns for cash flows, *_rate for rates, *_pp for per-policy amounts, claim_pp(t, kind) and pols_if_at(t, timing) for the argument-keyed families. The technical notes use compact actuarial symbols instead. The mapping is:

Notes symbol

Cells

Meaning

x

age_at_entry

Issue age (ANB)

x + t - 1

age(t)

Attained age at BOY of year t

x + t

age_anniv(t)

Attained age at anniversary t

T = 100 - x

proj_len

Last policy year

t0

duration_inforce

Durations already elapsed

t0 + 1

proj_start

First projected policy year

m

policy_term

Premium-paying period, years

F

sum_assured

Base face amount

G

premium_pp(t)

Gross annual premium

G^net_t

premium_net_pp(t)

Premium after dividend offset

A_t

rider_premium_pp(t)

PUA rider premium

i_g

int_rate_guar

Guarantee interest, 4.00%

i_d

int_rate_div

Dividend interest rate, 6.00%

i_L

int_rate_loan

Policy loan rate, 6.00%

q^g_{x+t-1}

mort_rate_guar(t)

Guarantee mortality

q^g_y

mort_rate_guar_at(y)

The same, keyed by age

q^sc_{x+t-1}

mort_rate_scale(t)

Dividend-scale mortality

q^e_{x+t-1}

mort_rate(t)

Best-estimate mortality

w_t

lapse_rate(t)

Surrender rate

w^dyn multiplier

dyn_lapse_factor(t)

Interest-sensitive overlay

l_{t-1}

pols_if(t)

In force at BOY t (weight)

l_t

pols_if_at(t, “AFT_DECR”)

In force at EOY t

l_{t-1}(1-q^e)

pols_if_at(t, timing)

In force inside year t

(none)

pols_death(t)

Deaths in year t

(none)

pols_lapse(t)

Surrenders at EOY t

(none)

pols_maturity(t)

Maturities at T

CV_t

cv_pp(t)

Guaranteed cash value

F - CV_t

net_amt_at_risk(t)

Guarantee net amount at risk

NSP_y

nsp(y)

Net single premium, endow 100

NP_g

np_guar()

Nonforfeiture net level premium

D^int_t

div_int(t)

Interest margin

D^mort_t

div_mort(t)

Mortality margin

D^exp_t (e^m_t)

div_exp(t)

Expense margin

D_t

div_base(t)

Base-block dividend, floored

D^PUA_t

div_pua(t)

PUA-block dividend

D_t + D^PUA_t

div_credited(t)

Dividend credited at EOY t

D^cash_t

div_cash(t)

Cash dividend per policy

(none)

div_to_pua(t)

Dividend applied to PUAs

dPUAF_t

pua_face_purch(t)

PUA face bought by dividend

dPUAF^rider_t

pua_face_rider(t)

PUA face bought by the rider

(none)

pua_face_offset(t)

PUA face from the RPD excess

PUAF_t

pua_face(t)

PUA face in force at EOY t

PUACV_t

pua_cv(t)

PUA cash value at EOY t

DA_t

div_accum(t)

Dividend accumulation balance

L_t

loan_bal(t)

Loan balance at EOY t

(none)

loan_int(t)

Loan interest capitalized

(none)

loan_draw(t)

Net new borrowing per policy

TF

term_blend_target

Term-blend target face

OYT_t

oyt_face(t)

One-year-term face in the blend

(none)

oyt_cost(t)

Dividend absorbed by the OYT

DB_t

claim_pp(t, “DEATH”)

Death benefit per policy

CSV_t

claim_pp(t, “LAPSE”)

Surrender value per policy

MAT

claim_pp(t, “MATURITY”)

Maturity benefit per policy

(none)

prem_cum(t)

Cumulative premiums paid

E_t

expenses(t)

Acquisition + maintenance

(none)

premium_taxes(t)

Premium tax outgo

(none)

premiums(t)

Premium income

(none)

rider_premiums(t)

PUA rider premium income

(none)

claims(t, kind)

Benefit outgo

(none)

div_cash_paid(t)

Cash dividend outgo

(none)

loan_draws(t)

Net loan advances

NetCF_t

liability_cf(t)

Net liability CF, outgo +

(none)

net_cf(t)

The same, income positive

Seven names needed care.

mort_rate is the best-estimate rate — the one that actually decrements the block — while the guarantee and dividend-scale rates are mort_rate_guar and mort_rate_scale. All three come off the same shipped table through the References ae_best_est and ae_scale. The notes call this out as a consistency trap: the same table feeds claim outgo and the dividend’s mortality margin with opposite signs, so raising best-estimate mortality raises claims and, if the scale factor moves with it, cuts the dividend.

policy_term is the premium-paying period m, not the coverage period. Coverage runs to proj_len() in every variant; on the base pay-to-100 design the two coincide, on PAY_10 they do not.

pols_lapse counts surrenders. The notes say “surrenders”, BasicTerm_S says pols_lapse, and CashValue_SE’s kind string is "LAPSE"; the model keeps the lifelib name so the kind vocabulary stays intact.

The notes’ CSV_t is the cash surrender value, not a file. It is reached as claim_pp(t, "LAPSE") so that all three benefit amounts share one cells and one kind vocabulary with CashValue_SE.

age(t) and age_anniv(t) are both needed: the notes index mortality at x + t - 1 (the age entering policy year t) but price paid-up additions bought at the end of that year at x + t. Getting them the wrong way round shifts every dividend purchase by a year.

pols_if(t) is the start-of-year count — the notes’ l_{t-1}, not their l_t. That is the library-wide convention (Term_US_A.pols_if(1) is pols_if_init(), CashValue_SE.pols_if(t) is pols_if_at(t, "BEF_MAT")), and it is the number every cash flow on the same result_cf() row is weighted by: the notes themselves write every term of NetCF_t over l_{t-1}. The notes’ end-of-year state variable is not lost — it is pols_if_at(t, "AFT_DECR"), and pols_if_at(t, "AFT_DECR") == pols_if(t + 1) by construction.

liability_cf(t) and net_cf(t) are the same stream with opposite signs — see the sign-convention rubric below.

Timing and kind arguments

pols_if_at(t, timing) takes "BEF_DECR" (start of year t, before any decrement — the same number as pols_if(t)), "BEF_SURR" (after deaths, before surrenders), "BEF_MAT" (after surrenders, before maturity) and "AFT_DECR" (after every decrement, the notes’ l_t) — the notes’ end-of-year processing order, deaths then dividend then surrenders then maturity. "BEF_DECR" and "BEF_MAT" are CashValue_SE’s names; "BEF_SURR" is added because this product settles deaths and surrenders in one end-of-year step, and "AFT_DECR" because CashValue_SE has no string for the point past the last decrement, which is where the notes’ state variable lives. claim_pp(t, kind) and claims(t, kind) take "DEATH", "LAPSE" and "MATURITY". Both raise ValueError("invalid timing") / ValueError("invalid kind") on anything else.

The dividend is rounded to the cent

div_round_digits = 2 rounds the credited base dividend before it buys paid-up additions. This is [std], and it is not cosmetic: the notes’ worked example adds the displayed margins (216.00 + 85.25 + 25.00 = 326.25) and then divides 326.25 by the net single premium. Carrying the unrounded 326.248 through instead moves the purchased paid-up-additions face from 776.79 to 776.78 — a full displayed cent, because the exact value sits just under the rounding boundary. Declared dividends are credited in whole cents, so the model rounds; setting div_round_digits = None turns it off and a test pins the size of the gap in both directions.

The worked example sets the PUA-block dividend aside

The notes’ worked-example table computes steps 11-15 from the base-block dividend alone, saying so explicitly: “For clarity the PUA-block dividend D^PUA_10 is omitted from this table; in the model it adds … to the amount in step 9.” The Reference pua_div_on ships False so the base deterministic run reproduces the worked example exactly, the same way Term_US_A ships conv_rate_base = 0. It is a reproduction switch, not a claim that paid-up additions are excluded from the dividend — they are dividend-eligible, and pua_div_on = True is the product-faithful setting. div_pua(t) implements the notes’ formula either way, and a test asserts its value on the anchor cell against the notes’ own parenthetical.

The four guarantee-basis tables are not one construction

The notes’ first “known modeling pitfall” is a mismatch between the cash value table and the NSP/annuity functions, and prescribes regenerating every guarantee-basis quantity from one 2017 CSO / 4% source. The shipped tables do not satisfy that, and no set of tables carrying the worked example’s anchors could. On any single mortality basis at interest i, endowment insurance and the annuity-due satisfy A_{x:n} = 1 - d ae_{x:n} with d = i/(1+i), so the notes’ own definition collapses to NNLP = 1000 d NSP_45 / (1 - NSP_45); the worked example’s NNLP = 13.00 then forces NSP_45 = 0.252616. But NSP_y = v(NSP_{y+1} + q_y (1 - NSP_{y+1})) >= v NSP_{y+1} for any q_y >= 0, so NSP_55 <= NSP_45 (1.04)^10 = 0.373933 — below the worked example’s NSP_55 = 0.42. Read the other way: NSP_55 = 0.42 forces NSP_45 >= 0.283737 and hence NNLP >= 15.236 per $1,000, 17% above the notes’ 13.00. The worked example’s steps 3 and 10 are mutually unreachable, whatever mortality table is used.

What the shipped tables do instead [std]: mort_table.csv is an illustrative Makeham curve pinned to q^g_54 = 0.00320; nsp_table.csv is a separate parametric curve pinned to NSP_55 = 0.42 and NSP_100 = 1; np_guar_table.csv holds 1000 NSP_x / ae_{x:(100-x)} with the annuity taken on the mortality table’s own survivorship, which is what makes NP_g = 13.00; and cv_table.csv is a monotone shape solved through CV_9 = 95.00 and CV_10 = 112.00 to 1000.00 at attained age 100. Reconciling the NSP curve with the mortality table needs a guarantee interest rate that falls from 5.99% at age 45 to 0.02% at age 99, and inverting the curve for the implied q at 4% gives a negative rate at every age up to 57 and a rate above 1 from age 89 on.

The two consequences the pitfall names are nevertheless absent, and are asserted by tests: NSP_100 = 1 exactly, so pua_cv(T) == pua_face(T), and the schedule reaches exactly face in the final policy year, so cv_pp(T) == sum_assured(). Neither block leaks at maturity. What is missing is the means — one basis — not the endpoints. Swap in a licensed 2017 CSO / 4% set and all four files must be replaced together; the worked example will then no longer reproduce, which is the honest price of the notes’ own arithmetic.

Sign convention — two names, one stream

The whole-life notes print NetCF_t with outgo positive, which is the opposite of the sign the other eleven reference models in products/ carry. Rather than pick one and lose the other, the model publishes both and names them apart:

liability_cf(t)

The notes’ NetCF_t, verbatim: outgo positive. Premium and rider income enter with a minus sign; expenses, premium tax, claims, cash dividends and net loan advances enter with a plus. A positive liability_cf is money leaving the insurer. This is the number to compare against the notes.

net_cf(t)

-liability_cf(t): income positive, the sign convention every model in products/ carries, so that result_cf()["net_cf"] can be summed or compared across products. A positive net_cf is money arriving at the insurer.

Both are columns of result_cf(). The pattern is SPIA_US_S and DIA_US_S’s, which face the same clash. Nothing about the whole-life notes’ own convention is being denied — it is being kept under a name that does not collide with the library-wide one, because a sign error in a 55-year liability projection is invisible in a summary statistic.

Cells Descriptions#

model_point()[source]#

The selected model point as a Series.

product()[source]#

The product code: WL_PAR, WL_FE_LEVEL or WL_FE_GRADED.

is_par()[source]#

True for the participating design, which is the only one that pays dividends.

premium_period()[source]#

The premium period code: TO_100, PAY_10, PAY_20 or TO_65.

age_at_entry()[source]#

The issue age (ANB) of the selected model point.

sex()[source]#

The sex of the selected model point; rates are sex-distinct throughout.

risk_class()[source]#

The underwriting class of the selected model point (PREF_NT / STD_NT / TOB).

sum_assured()[source]#

The base face amount F of the selected model point.

dividend_option()[source]#

The elected dividend option: PUA, CASH, ACCUM, REDUCE_PREM, or NONE for non-par.

pols_if_init()[source]#

Initial number of policies in force, l at duration_inforce().

duration_inforce()[source]#

t0: policy years already elapsed at the valuation date; 0 for new business.

puaf_inforce()[source]#

PUAF at t0: paid-up additions face already in force at the valuation date.

loan_inforce()[source]#

L at t0: policy loan balance already outstanding at the valuation date.

loan_utilization()[source]#

Fraction of the guaranteed cash value held as a policy loan [std].

0 in the base run; the notes’ variant is 0.20, maintained by borrowing and repaying at each anniversary.

term_blend_target()[source]#

TF: the term-blend rider’s target face amount; 0 switches the rider off.

is_blended()[source]#

True when the term-blend rider is active on this model point [std].

The blend needs the dividend to pay the one-year-term cost, so it is modelled only under the PUA dividend option and only on the participating design.

proj_len()[source]#

T = maturity_age - x: the last policy year, ending at attained age 100.

The contract itself matures at 121, but the guaranteed cash value equals face at 100 and the paid-up-additions cash value equals paid-up-additions face there, so from 100 the policy is economically an endowment at face. Truncating at 100 is a [std] simplification that moves the age 100-121 payments from death to maturity without changing their amount per survivor.

proj_start()[source]#

The first projected policy year, t0 + 1.

New business starts at 1; an in-force model point starts at the year following the durations it has already run.

policy_term()[source]#

m: the premium-paying period in years, from the premium period code.

This is not the coverage period, which is proj_len() in every variant.

age(t)[source]#

The attained age x + t - 1 entering policy year t.

Mortality in year t is indexed here; paid-up additions bought at the end of the year are priced at age_anniv(t).

age_anniv(t)[source]#

The attained age x + t at the anniversary that ends policy year t.

mec_flag()[source]#

True where the model point would need a Sect. 7702A test this model does not run.

The notes are explicit that the reference model does not police Sect. 7702 or Sect. 7702A limits and should flag model points that would fail rather than silently projecting them [std]. Limited-pay designs sit near the 7-pay limit and paid-up-additions rider payments consume 7-pay room, so both raise the flag.

premium_rate()[source]#

Final-expense annual premium per $1,000 of face, from premium_rates.csv [S7].

premium_pp(t)[source]#

G: the gross annual premium per policy payable at the start of policy year t.

Level and guaranteed while premiums are payable, zero after policy_term(). The participating design takes G from the model point ([std illustrative] — carrier rate books are not public); the final-expense variant computes it from the sourced rate table as (F / 1000) * rate(x, sex, class) + 36 [S7].

premium_net_pp(t)[source]#

G^net_t: the premium actually collected at the start of policy year t.

Under REDUCE_PREM the prior anniversary’s dividend offsets the premium, max(G - D_{t-1}, 0), and any excess buys paid-up additions through pua_face_offset(). The dynamic premium-offset overlay ([std], off by default) applies a prem_offset_share fraction of the same offset once the dividend has grown to cover the premium.

rider_premium_pp(t)[source]#

A_t: the paid-up-additions rider premium paid at the start of policy year t.

Level while base premiums are payable [std]; the notes set it within limits fixed at issue and do not schedule it.

prem_cum(t)[source]#

Cumulative gross premium paid per policy through the start of policy year t.

Only the final-expense graded plan uses it, for the 110%-of-premiums-paid death benefit in policy years 1-2 [S6][S7].

cv_pp(t)[source]#

CV_t: the guaranteed cash value per policy at the anniversary ending year t.

Read from cv_table.csv per $1,000 of face. The notes give the Standard Nonforfeiture Law adjusted-premium formula conceptually but prescribe a table input in practice, because contractual cash value tables are policy-form documents that are not public. The shipped schedule is [std], calibrated to the worked example’s CV_9 and CV_10 and reaching exactly face at attained age 100.

It is sex-distinct, as the notes require of every rate in this product: the male pay-to-100 schedule carries the worked example’s anchors, and the female schedule is that schedule’s funding-progress shape f_t = CV^M_t / (F NSP^M_{x+t}) applied to the female paid-up value F NSP^F_{x+t} [std]. The shape — how far along the way to paid-up status the schedule has come — is a design choice that does not depend on sex; the value it is progressing towards does, through NSP. See the guarantee-basis rubric in the Space docstring for what this construction is not.

net_amt_at_risk(t)[source]#

F - CV_t: the guarantee-basis net amount at risk carried by the mortality margin.

nsp(y)[source]#

NSP_y: net single premium per 1 of paid-up endowment-at-100 face at attained age y.

The purchase basis for paid-up additions, unloaded, on the guarantee basis [std]. NSP_100 = 1 by construction, which is what makes the paid-up-additions cash value equal paid-up-additions face at maturity.

The shipped curve is not the endowment-at-100 net single premium implied by mort_table.csv at 4%, and cannot be: see the guarantee-basis rubric in the Space docstring, which shows that the worked example’s own anchors rule out any single mortality basis at 4%.

np_guar()[source]#

NP_g: the nonforfeiture net level premium per policy, from np_guar_table.csv.

The dividend’s interest margin is credited on the guaranteed fund including the year’s net premium, so this quantity sits inside div_int() [std].

The notes define it over the endowment period, NNLP = F NSP_x / ae_{x:(100-x)}, and annotate the other nonforfeiture quantity — the adjusted premium P_adj — with “(m = premium period)”, so the (100 - x) subscript is deliberate: NNLP does not vary with the premium period. The shipped table therefore carries one value per (sex, issue age), repeated across the premium periods; the premium_period key is kept so a carrier table that does vary by m can be dropped in without a formula change.

mort_rate_guar_at(y)[source]#

q^g_y: the guaranteed mortality rate at attained age y, from mort_table.csv.

The shipped table is [std] illustrative and is not the 2017 CSO the notes name; that table is licensed and cannot be shipped here.

mort_rate_guar(t)[source]#

q^g at the attained age entering policy year t.

mort_rate_scale(t)[source]#

q^sc: the dividend scale’s experience mortality, ae_scale x q^g [std].

mort_rate(t)[source]#

q^e: the best-estimate mortality rate that decrements the block [std].

ae_best_est x q^g. The notes’ sensitivity list flags this as a consistency trap: the same table drives claim outgo here and the dividend’s mortality margin through mort_rate_scale(), with opposite signs.

dyn_lapse_factor(t)[source]#

The interest-sensitive lapse multiplier [std], for scenario runs.

min(1 + 2.0 * max(0, r_cmp - i_d - 0.01), 3.0), where r_cmp is the competitor or market rate in the scenario. Off unless dyn_lapse_on; the calibration is judgmental and the research base records no dynamic-lapse study for whole life.

lapse_rate(t)[source]#

w_t: the surrender rate applied to survivors at the end of policy year t [std].

Participating: 5.0% in year 1 grading linearly to 2.0% at year 10, level 2.0% thereafter. Final expense (simplified issue, so heavier): 12% year 1, 10% year 2, grading linearly to 6% by year 5 and level after. Zero in the final policy year — the notes’ “0 within 1 year of maturity” — so that the survivors of year T mature rather than surrender.

div_int(t)[source]#

D^int_t: the dividend’s interest margin, with direct recognition [std].

(i_d - i_g)(CV_{t-1} + NP_g - L_{t-1}) + (i_L - i_g) L_{t-1} — the loaned portion is credited at the loan rate rather than the portfolio dividend rate. With the snapshot i_L = i_d = 6.00% the adjustment is zero, which is a coincidence of the snapshot and not a model property.

div_mort(t)[source]#

D^mort_t: the dividend’s mortality margin, (q^g - q^sc)(F - CV_t) [std].

div_exp(t)[source]#

D^exp_t: the dividend’s expense margin, a flat per-policy amount [std].

div_base(t)[source]#

D_t: the base-block dividend credited at the anniversary ending year t.

max(D^int + D^mort + D^exp, 0), rounded to div_round_digits. The floor is [std]: dividends are non-negative distributions of surplus, so adverse experience does not claw back. No dividend is credited before div_first_year, a real cross-carrier design split that the notes keep as a parameter, and none at all on the non-participating final-expense design.

div_pua(t)[source]#

D^PUA_t: the dividend earned by the paid-up-additions block [std].

(i_d - i_g) PUACV_{t-1} + (q^g - q^sc)(PUAF_{t-1} - PUACV_{t-1}). Paid-up additions are dividend-eligible, and the compounding this creates is the notes’ first-ranked sensitivity. Switched off by pua_div_on in the shipped base run because the worked example’s table omits it; see the Space docstring.

div_credited(t)[source]#

D_t + D^PUA_t: the whole dividend credited to survivors at the end of year t.

oyt_face(t)[source]#

OYT_t: the one-year-term face the term-blend rider carries at the end of year t.

max(TF - F - PUAF_{t-1}, 0), capped at what the dividend can fund [std]. Two readings had to be settled here, and both are standardizations:

The notes write the gap as TF - F - PUAF_t, which is circular — the term cost is deducted from the dividend that buys those very additions — so the model uses the prior anniversary’s paid-up-additions face.

The notes are also silent on what happens when the dividend cannot pay for the whole gap. Leaving the formula uncapped would report a term face the model never charges for and would inflate the death benefit, so the layer is capped at D_t (1 + i_g) / q^sc_{x+t} — as much term as the dividend actually buys.

Whether the cap binds is a property of how the blend is funded, not of the design. On model point 8, where a $5,000 paid-up-additions rider premium funds the 2x target, it binds only in policy year 1 — where no dividend is payable at all under div_first_year = 2 — and the gap closes at year 8. On model point 14, the same 2x target with no rider premium, it binds in years 1-3 while the dividend is small and again in every year from 30 on as q^sc outruns it, and the block never crosses over. Crossover, after which the rider is pure paid-up additions, is where the gap itself reaches zero.

oyt_cost(t)[source]#

The dividend absorbed by the one-year-term layer, q^sc_{x+t} x OYT_t x v_g [std].

div_to_pua(t)[source]#

The part of the credited dividend that buys paid-up additions at the end of year t.

Under the PUA option, everything left after the term-blend cost. Under CASH it is paid out, under ACCUM it goes to div_accum(), and under REDUCE_PREM it offsets the next premium with the excess handled at the start of the following year by pua_face_offset() — except in the final policy year, where there is no year T + 1 to offset. The notes are silent on that last dividend; routing it anywhere else would drop it, so the whole of D_T is treated as REDUCE_PREM excess and buys paid-up additions at NSP_100 = 1 [std], which is the same rule the option already applies in every year whose premium the dividend has outgrown.

div_cash(t)[source]#

D^cash_t: the cash dividend paid per surviving policy at the end of year t.

div_accum(t)[source]#

DA_t: the dividend accumulation balance at the anniversary ending year t.

DA_{t-1}(1 + i_d) + D_t under the ACCUM option, zero otherwise. The credit rate reuses the dividend interest rate [std]: carriers declare an accumulation rate annually with the scale but publish no separate figure. The balance adds to the death, surrender and maturity proceeds.

pua_face_purch(t)[source]#

dPUAF_t: paid-up-additions face bought by the dividend, div / NSP_{x+t}.

pua_face_rider(t)[source]#

dPUAF^rider_t: paid-up-additions face bought by the rider payment at the start of year t.

A_t (1 - load) / NSP_{x+t-1}, a 10% load [std] chosen from the observed 7.5%-10% range on rider payments. Dividend purchases carry no load; only rider payments do.

pua_face_offset(t)[source]#

Paid-up-additions face bought at the start of year t by the REDUCE_PREM excess.

Once the prior dividend exceeds the premium it is offsetting, the excess buys paid-up additions [std] rather than being paid out. This carries D_{t-1}, so it can never carry D_T; that last dividend is routed through div_to_pua() instead — see its docstring.

pua_face(t)[source]#

PUAF_t: paid-up-additions face in force at the anniversary ending year t.

PUAF_{t-1} plus the dividend, rider and premium-offset purchases of year t. The t0 branch carries the model point’s puaf_inforce, which is what makes the worked example’s stipulated prior balance of 4,100 an input rather than a projection.

pua_cv(t)[source]#

PUACV_t: the cash value of the paid-up additions, PUAF_t x NSP_{x+t} [std].

Valuing the whole block at the attained-age net single premium is exact at the issue of each layer and again at age 100, and approximate in between.

loan_int(t)[source]#

Loan interest capitalized on the anniversary ending year t, L_{t-1} x i_L.

loan_bal(t)[source]#

L_t: the policy loan balance at the anniversary ending year t [std].

The notes’ variant holds the loan at loan_utilization x CV_t, maintained by borrowing and repaying at each anniversary; the base run sets loan_utilization to zero. Loans reduce the death, surrender and maturity proceeds.

loan_draw(t)[source]#

Net new borrowing per policy at the anniversary ending year t; negative repays.

The balance change net of the interest that capitalized into it, so the cash the insurer actually advances.

claim_pp(t, kind)[source]#

The benefit amount per policy in policy year t, by kind.

"DEATH" DB_t = F + PUAF_{t-1} + OYT_t + DA_{t-1} - L_{t-1}. Deaths fall at

the end of the year before the dividend is credited, so the benefit carries the prior anniversary’s paid-up additions [std]. The one-year-term layer is zero unless the term-blend rider is active, where F + PUAF + OYT is the notes’ “target face plus excess paid-up additions” written so that it still holds when the dividend funds only part of the gap. On the final-expense graded plan, natural-cause deaths in policy years 1-2 pay 110% of cumulative premiums paid and accidental deaths pay the full face from day one [S6][S7]; the two are blended by fe_accid_share [std], since the model carries one mortality decrement.

"LAPSE" CSV_t = CV_t + PUACV_t + DA_t - L_t, the surrender value at the end

of year t, including the dividend just credited.

"MATURITY" MAT = F + PUAF_T + DA_T - L_T, paid at T and zero in every other

year. On the non-participating design this is F - L_T.

pols_if_at(t, timing)[source]#

The number of policies in force at a point inside policy year t.

"BEF_DECR"

l_{t-1}, the start of year t, before any decrement; the same number as pols_if(t), and the weight on that year’s cash flows.

"BEF_SURR"

after deaths, before surrenders — the population the dividend is credited to and the one surrenders are taken from.

"BEF_MAT"

after surrenders, before maturity; the survivors of the year.

"AFT_DECR"

l_t, the notes’ end-of-year state variable: what is left once the year’s deaths, surrenders and — in the final policy year — the maturities have all been taken, so it is zero from T on. Equals pols_if(t + 1). The string is added to CashValue_SE’s set because that model has no name for the point past the last decrement, which is where these notes keep their in-force probability.

pols_if(t)[source]#

The number of policies in force at the start of policy year t.

The notes’ l_{t-1}: the population that pays the year’s premium, carries its expenses and is exposed to its decrements, so it is the weight on every cash flow of the same result_cf() row. Equal to pols_if_init() up to and including t0, and zero from T + 1 on, because everything still in force at T matures there and the contract terminates.

The notes’ end-of-year l_t is pols_if_at(t, “AFT_DECR”), which is this cells one year on.

pols_death(t)[source]#

Deaths in policy year t, q^e applied to the policies in force at its start.

pols_lapse(t)[source]#

Surrenders at the end of policy year t, w_t applied to survivors of the year.

pols_maturity(t)[source]#

Policies maturing at attained age 100; non-zero only in the final policy year.

Not a decrement — the modelled contract simply ends — but the in-force roll-forward does not close without it.

inflation_factor(t)[source]#

The expense inflation factor in policy year t.

premiums(t)[source]#

Premium income at the start of policy year t (an inflow).

rider_premiums(t)[source]#

Paid-up-additions rider premium income at the start of policy year t (an inflow).

premium_taxes(t)[source]#

Premium tax at the start of policy year t [std], on premium and rider income.

The notes’ processing order collects it at the beginning of the year alongside the expenses; their one-line NetCF formula omits it. The model follows the processing order and keeps it as its own line.

expenses(t)[source]#

E_t: acquisition and inflating maintenance expense in policy year t [std].

90% of the first year’s premium plus $250 per policy at issue, then $60 per policy per year inflating at 2%. An in-force model point never sees the acquisition charge, because its projection starts after policy year 1.

claims(t, kind=None)[source]#

Benefit outgo in policy year t, by kind; the total when kind is omitted.

div_cash_paid(t)[source]#

Cash dividends paid out at the end of policy year t, to survivors of the year.

loan_draws(t)[source]#

Net policy loans advanced at the end of policy year t [std], an outflow.

The notes report gross liability flows plus a separate loan account rather than netting the loan into a net-amount-at-risk presentation. Advances go to the policies still in force; the balance is recovered through the death, surrender and maturity benefits, which are all net of the loan.

liability_cf(t)[source]#

NetCF_t: the net liability cash flow in policy year t, outgo positive.

The technical notes’ formula verbatim, and the one to compare against them: premium and rider income enter with a minus sign; expenses, premium tax, death, surrender and maturity benefits, cash dividends and net loan advances enter with a plus, so a positive value is money leaving the insurer. Internal dividend applications — paid-up additions, accumulation, premium reduction — are not cash flows when credited: they emerge later through the death benefit, the surrender value and the maturity benefit.

These notes are the only ones in the library that print the outgo-positive sign, so the stream is published twice: here under the notes’ sign, and negated as net_cf(t) under the library’s. See the sign-convention rubric in the Space docstring.

net_cf(t)[source]#

The net cash flow in policy year t, income positive: -liability_cf(t).

Income less outgo, the sign every model in products/ carries, so that a result_cf()["net_cf"] column can be summed or compared across products. A positive value is money arriving at the insurer. liability_cf(t) is the same stream under the technical notes’ own outgo-positive sign; both are result_cf() columns because neither reading may be lost.

result_cf()[source]#

Result table of cashflows, indexed by policy year.

pols_if is the start-of-year count, which is the weight applied to every cash flow on the same row. Both signs of the net flow are published: net_cf is income-positive, the library-wide convention, and liability_cf is the technical notes’ outgo-positive NetCF_t; the two are negatives of each other.

result_pols()[source]#

Result table of policy counts and decrement rates, indexed by policy year.

result_cv()[source]#

Result table of the guaranteed and non-guaranteed values, indexed by policy year.

The account-value analogue for this product: the guaranteed cash value, the paid-up-additions block, the accumulation balance and the loan, plus the dividend that drives them and the two benefit amounts they feed.

check_pols_roll_fwd_resid(t)[source]#

The in-force roll-forward residual in policy year t; zero everywhere.

pols_if(t) - pols_if(t + 1) - deaths - surrenders - maturities, the notes’ l_{t-1} - l_t - .... Maturities are non-zero only in the final policy year, where the survivors neither die nor surrender: their contract ends. Without that term the last year appears to lose lives with no cause.

check_pols_roll_fwd()[source]#

True when the in-force roll-forward closes in every projected policy year.

The library-wide form of a roll-forward check: no argument, one bool over all t, so one test can call it across every model. check_pols_roll_fwd_resid(t) gives the signed residual of the year that failed. The tolerance scales with pols_if_init(), since the residual is an accumulation of rounding on that many policies.

check_pua_roll_fwd_resid(t)[source]#

The paid-up-additions roll-forward residual in policy year t; zero everywhere.

PUAF_t - PUAF_{t-1} - dividend purchases - rider purchases - offset purchases. The analogue of CashValue_SE.check_av_roll_fwd for a product whose accumulating state is a face amount rather than an account value.

check_pua_roll_fwd()[source]#

True when the paid-up-additions roll-forward closes in every projected year.

No argument, one bool over all t, matching check_pols_roll_fwd(); check_pua_roll_fwd_resid(t) gives the signed residual. The tolerance is relative to the block itself, which reaches six figures of face on a compounding projection.