The Projection Space#

The by-policy monthly projection of the ULSG_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 worked-example 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/guaranteed_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 ULSG_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

rop_file

data.rop_table()

rop_table.csv

Projection basis

t counts policy months, 1-based from the start of the projection: t = 1 is the issue month of a new-business model point and the first projected month of an in-force cell, which sits duration_mth_init() completed policy months after issue. The technical notes index the same months by their absolute policy month number, starting at duration_months + 1, so the worked example’s months 301-305 are t = 1 to t = 5 here, with duration_mth(t) = 300 + t - 1. State variables the notes define at t = 0AV_0, SG_0, L_0, CumPrem_0, l_0 = 1, g_0 = 0 – are the t == 0 branch of the corresponding recursion.

Within each month the notes’ twelve-step monthiversary order is followed exactly:

  1. status check – a policy whose 61-day grace has expired lapses at BOM with no value (is_lapsed(), grace_mth());

  2. premium, its two loads and the cumulative premium (premium_pp(), prem_to_av_pp(), prem_to_sg_pp(), cum_prem_pp());

  3. expense charges on both accounts and the withdrawal and its fee (maint_fee_pp(), sg_maint_fee_pp(), wd_pp(), wd_fee_pp()) – after which the account value is the notes’ AV'(t), av_pp_at(t, "BEF_COI"), and the shadow value its SG'(t);

  4. death benefit and the GPT corridor test (db_pp());

  5. the net amount at risk on both accounts, each the death benefit discounted one month at that account’s credited rate less that account’s balance floored at zero (net_amt_at_risk(), sg_net_amt_at_risk());

  6. cost of insurance on both accounts (coi_pp(), sg_coi_pp());

  7. the insufficiency test: if the account value cannot carry the deduction, the shortfall is either forgone by the insurer while the guarantee stands (mth_deduction_forgone_pp(), is_guar_supported()) or opens the grace period (is_shortfall(), grace_mth());

  8. end of month: interest on the post-deduction balance of each account (inv_income_pp(), sg_inv_income_pp()) and loan interest accrual (loan_bal_pp());

  9. the in-force test SG - L > 0 (is_guar_active(), sg_net_pp()) and the catch-up requirement it implies (catch_up_prem_pp());

  10. end of month: decrements, death first, then surrender, then return-of-premium exercise (pols_death(), pols_lapse(), pols_rop()).

Cash flows are undiscounted. Premiums and expenses fall at BOM and are weighted by pols_if(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); refunds by the same survivors times rop_rate(t). Loads, charges, interest credits and every shadow-account entry are internal transfers, not cash flows: the shadow account is notional and never payable [S2][S3], so it has per-policy cells only and no in-force weighted counterparts.

Naming

Cells names follow lifelib’s basiclife.BasicTerm_S and savings.CashValue_SE wherever those models have an analogue, and this library’s own UL_US_S – the chassis this product is built on – everywhere else. Names introduced here are the ones guaranteed UL genuinely adds: the shadow account (sg_*), the guarantee tests, the grace counter, the forgone deduction and the return-of-premium endorsement. The technical notes use compact actuarial symbols; the mapping is:

Notes symbol

Cells

Meaning

t

(the t argument)

Policy month, 1-based

duration_months

duration_mth_init

Elapsed months at start

(t - 1 in months)

duration_mth(t)

Completed policy months

y

policy_year(t)

Policy year

(y - 1)

duration(t)

Completed policy years

issue_age

age_at_entry

Issue age (ANB)

x_t

age(t)

Attained age (ANB)

sex

sex

Sex, M or F

risk_class

rate_class

Underwriting class [S4]

(none)

proj_len

Last projected month

face_amount, F

sum_assured

Initial face amount

(none)

sum_assured_at(t)

Face amount in month t

(units)

units(t)

Face in $1,000 units

guarantee_age

guarantee_age

Elected guarantee age

premium_pattern

premium_type

LEVEL / SINGLE / TEN_PAY

annual_premium

premium_pp_ann

Scheduled premium

premium_mode

premium_mode, premium_freq()

Payment mode, per year

(premium month)

is_premium_mth(t)

A premium falls due

phi_t

prem_persistency(t)

Premium persistency

P_t

premium_pp(t)

Premium received at BOM

pi

load_prem_rate

Base premium load, 25%

pi^g

load_prem_rate_sg

Shadow premium load, 8%

(1-pi) P_t

prem_to_av_pp(t)

Net premium to the AV

(1-pi^g) P_t

prem_to_sg_pp(t)

Net premium to the shadow

CumPrem_t

cum_prem_pp(t)

Cumulative premiums

CumPrem_0

cum_prem_init

Opening cumulative premium

W_t

wd_pp(t)

Withdrawal

(25 fee)

wd_fee_pp(t)

Withdrawal fee

e_pol

expense_pol_mth

Per-policy charge

e_u

expense_unit_mth

Base per-unit charge

e_u^g

expense_unit_mth_sg

Shadow per-unit charge

rc(t)

rider_charge_pp(t)

Rider charges (0)

(e_pol + e_u F/1000 + rc)

maint_fee_pp(t)

Base non-COI charges

(e_u^g F/1000)

sg_maint_fee_pp(t)

Shadow non-COI charges

DB_t

db_pp(t)

Death benefit

kappa(x)

corridor_factor(t)

GPT corridor factor

NAAR_t

net_amt_at_risk(t)

Base net amount at risk

NAAR_t^g

sg_net_amt_at_risk(t)

Shadow net amount at risk

(NAAR factors)

naar_factor, sg_naar_factor

1 + j_g, 1 + j^g

(guaranteed max scale)

coi_rate_scale()

The cell’s annual COI scale

m_t^max

coi_rate_guar(t)

Guaranteed max monthly COI

m_t

coi_rate(t)

Current monthly COI rate

m_t^g

sg_coi_rate(t)

Shadow monthly COI rate

(scale precision)

coi_rate_dp()

Quoting precision per $1,000

COI_t

coi_pp(t)

Base cost of insurance

COI_t^g

sg_coi_pp(t)

Shadow cost of insurance

mth_deduction_pp(t)

Base monthly deduction

(shadow MD)

sg_deduction_pp(t)

Shadow monthly deduction

D_t

mth_deduction_forgone_pp(t)

Deduction forgone

(deduction taken)

mth_deduction_taken_pp(t)

Deduction actually taken

AV_{t-1}

av_pp_at(t, “BEF_PREM”)

Opening account value

(after premium)

av_pp_at(t, “BEF_WD”)

Before the withdrawal

(after withdrawal)

av_pp_at(t, “BEF_FEE”)

Before the expense charges

AV_t’

av_pp_at(t, “BEF_COI”)

After the expense charges

AV_t’’

av_pp_at(t, “BEF_INV”)

After COI, floored at zero

AV_t

av_pp(t)

Account value, EOM

av_init

av_pp_init

Opening account value

(aggregate AV)

av_at(t, timing)

Account value in force

(none)

av_change(t)

Change in account value

SG_t’

sg_pp_at(t, “BEF_COI”)

Shadow after the charges

SG_t’’

sg_pp_at(t, “BEF_INV”)

Shadow after COI, no floor

SG_t

sg_pp(t)

Shadow value, EOM

sg_init

sg_pp_init

Opening shadow value

SG_t - L_t

sg_net_pp(t)

The in-force test quantity

(in-force test)

is_guar_active(t)

SG_t - L_t > 0

(step 6 test)

is_guar_supported(t)

SG_t’’ - L_{t-1} > 0

C_t

catch_up_prem_pp(t)

Catch-up premium required

j_c

inv_return_mth(t)

Monthly credited rate

j_g

guar_rate_mth()

Monthly guaranteed rate

j^g

sg_rate_mth()

Monthly shadow rate

(interest)

inv_income_pp(t)

Interest credited to the AV

(shadow interest)

sg_inv_income_pp(t)

Interest credited to SG

L_t

loan_bal_pp(t)

Loan balance

loan_init

loan_bal_init

Opening loan balance

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 surrender value

CSV_t

ncsv_pp(t)

Net cash surrender value

(SC retained)

surr_charge(t)

Surrender charge collected

g_t

grace_mth(t)

Months in grace

(grace trigger)

is_shortfall(t)

The deduction attempt fails

(cure payment)

cure_premium_pp(t)

Payment curing the grace

(lapse for insufficiency)

is_lapsed(t)

Terminated in grace

(Status column)

status(t)

The notes’ status string

rop_elected

rop_elected

Endorsement elected [S1]

rho

rop_ratio(t)

ROP refund ratio

w^ROP

rop_rate(t)

ROP exercise rate

(ROP window)

rop_anniversary(t)

20, 25 or 0

q_t^d

mort_rate_mth(t)

Monthly mortality rate

(annual q)

mort_rate(t)

Annual mortality rate

(improvement)

mort_improve_rate(t)

Annual improvement rate

(improvement factor)

mort_improve_factor(t)

Cumulative improvement

b(d)

lapse_rate_base(t)

Base annual lapse rate

G

lapse_rate_guar_mult()

Lifetime-guarantee factor

Phi

lapse_rate_pattern_mult()

Premium-pattern factor

Psi_t

lapse_rate_dyn_mult(t)

Funding-status factor

(annual w)

lapse_rate(t)

Total annual lapse rate

w_t

lapse_rate_mth(t)

Monthly lapse rate

l_t

pols_if(t)

In force at BOM of month t

(l_0)

pols_if_init

In force at the outset

(deaths)

pols_death(t)

Deaths in month t

(surrenders)

pols_lapse(t)

Surrenders in month t

(ROP exercises)

pols_rop(t)

Refund exercises

(grace lapse)

pols_lapse_grace(t)

Terminations in grace

(none)

pols_maturity(t)

Maturities: always zero

(premium income)

premiums(t)

Premium income

(death CF)

claims(t, “DEATH”)

Death claims

(surrender CF)

claims(t, “LAPSE”)

Surrender payments

(ROP CF)

claims(t, “REFUND”)

Refund payments

(withdrawal CF)

withdrawals(t)

Withdrawal payments

(expenses)

expenses(t)

Insurer expenses

(none)

premium_taxes(t)

Percent-of-premium: zero

NetCF

net_cf(t)

Net liability cash flow

g(P)

guar_min_sg(prem)

min over t of SG_t(P) - L_t

SG_t(P)

sg_pp_solve(t, prem)

Shadow value under P

P*

no_lapse_premium()

Solved no-lapse premium

Nine names needed care.

The notes’ risk_class is this model’s rate_class – the name Term_US_A and UL_US_S both use for the underwriting class, and the one the model point table column carries, so the notes’ word appears nowhere in the model. It is the only model point attribute renamed on cross-model grounds rather than for a reason internal to this product.

l_t in these notes is the in-force probability at the beginning of month t – the notes weight every cash flow of month t by l_t and roll forward with l_{t+1} = l_t (1 - q^d)(1 - w)(1 - w^ROP) – so it maps straight onto pols_if(t) with no offset. Note the contrast with the universal life chassis, whose notes define l(t) at the end of month t, making pols_if(t) = l(t-1) there.

The account value measured for the net amount at risk is AV'(t), which in these notes is the balance after the expense charges and before the cost of insuranceav_pp_at(t, "BEF_COI"). The universal life chassis measures it before the entire monthly deduction, av_pp_at(t, "BEF_FEE"). The guaranteed-UL notes flag the difference as a deliberate deviation; it is immaterial at the modelled charge levels but it is real, and "BEF_COI" exists so that the two models can be reconciled without reading the formulas.

The notes’ CSV_t = max(AV_t - SC_t - L_t, 0) already nets policy debt, so it is this model’s ncsv_pp(), not its csv_pp(). csv_pp() is the chassis’ AV - SC floored at zero, and keeping both means the surrender-charge and the indebtedness offsets can be read separately.

D_t is the monthly deduction the insurer forgoes because the account value is exhausted while the guarantee stands. It is not a receivable and must never accrue against future premiums or account value recoveries – treating it as one understates the guarantee cost, which the notes list among the pitfalls. mth_deduction_forgone_pp() therefore feeds nothing but the diagnostics, and mth_deduction_taken_pp() – what actually left the account – is what the account value roll-forward and the margins use.

The guarantee is tested twice at slightly different points, and the notes use the same words for both. is_guar_supported() is the step-6 test SG''_t - L_{t-1} > 0, which decides whether a failed deduction is forgone or opens the grace period; is_guar_active() is the step-9 in-force test SG_t - L_t > 0, measured after the shadow interest credit and the loan accrual. Running the second before the deduction attempt – the notes’ “order of tests” pitfall – shifts claim timing at exactly the durations where the net amount at risk is the whole death benefit.

maint_fee_pp is the notes’ non-COI monthly charge against the account value and is therefore insurer income, following CashValue_SE.maint_fee. expenses() is something different – the insurer’s own [std] acquisition, maintenance and claim expense, a cash flow. The two must not be confused: maint_fee is income, expenses is outgo.

A partial withdrawal is not a claim. It is a payment the owner elects to take out of a policy that stays in force, not an event that terminates one, so it is withdrawals() and a withdrawals column of result_cf(), and claims(t, "WITHDRAWAL") raises ValueError("invalid kind"). claim_pp() keeps its "WITHDRAWAL" branch, because the per-policy amount is what withdrawals() weights, and claims_from_av() keeps its own, because a withdrawal does release account value; only the claim aggregate excludes it, and its kind is None total must not double-count it.

coi_rate_dp() is the one place where the notes’ worked example and the notes’ own rule disagree by more than rounding; see its docstring and the README.

Cells Descriptions#

model_point()[source]#

The selected model point as a Series.

age_at_entry()[source]#

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

sex()[source]#

The sex of the selected model point.

rate_class()[source]#

The underwriting class of the selected model point (four NT, two tobacco) [S4].

sum_assured()[source]#

F: the initial face amount of the selected model point [S4][S6].

guarantee_age()[source]#

The elected secondary-guarantee age, any attained age from 90 to 121 [S1][S2][S9].

121 is the lifetime election, which is what the anchor cell carries and what switches lapse_rate_guar_mult() to the 0.55 multiplier [R7].

premium_type()[source]#

The premium pattern: "LEVEL", "SINGLE" or "TEN_PAY" [std].

A first-class model point attribute because funding pattern drives both the guarantee trajectory and observed lapse behaviour [R8].

premium_mode()[source]#

The premium mode: "A", "S", "Q" or "M" (EFT only) [S2].

premium_pp_ann()[source]#

The scheduled annual premium per policy, or the single premium for "SINGLE".

For the anchor cell this is the notes’ solved level no-lapse premium P* = 10,800 [std]; no_lapse_premium() re-derives it from the shadow recursion rather than reading it from here.

load_prem_rate()[source]#

pi: the base premium expense charge, 25% of every premium, all years [S3][S7].

Contractual here, unlike the universal life chassis where the load is a non-guaranteed element; it sits in the model point table for the same reason it does there, so that the table alone describes the policy.

av_pp_init()[source]#

AV_0: the base account value per policy at the outset, 0 at issue.

sg_pp_init()[source]#

SG_0: the shadow account value per policy at the outset, 0 at issue.

Not floored anywhere in the projection: a negative shadow balance measures the catch-up shortfall, and flooring it destroys catch_up_prem_pp().

loan_bal_init()[source]#

L_0: the policy loan balance per policy at the outset, 0 in every shipped point.

cum_prem_init()[source]#

CumPrem_0: cumulative premiums already paid at the outset.

The notes make this a model point attribute because it drives the return-of-premium refund [S1] and the 7-pay test [R5]. For the anchor cell it is the 25 annual premiums of $10,800 implied by the in-force snapshot [std].

pols_if_init()[source]#

l_0: the in-force probability at the outset, 1 for a single-policy point.

duration_mth_init()[source]#

Completed policy months already elapsed when the projection starts.

0 for a new-business model point, so that t = 1 is the issue month; 300 for the notes’ worked-example cell, whose months 301-305 are t = 1 to t = 5. This is the notes’ duration_months.

has_surr_charge()[source]#

Whether a surrender charge schedule applies to this model point.

surr_charge_id()[source]#

The surrender charge schedule ID, a row label of surr_charge_table.csv.

rop_elected()[source]#

Whether the return-of-premium endorsement applies [S1].

Built into the representative contract, so it is on for every point but the one that switches it off to isolate the guarantee mechanics.

coi_rate_dp()[source]#

Decimal places the declared COI scales are quoted to per $1,000; -1 = exact.

The one place where the notes’ rule and the notes’ worked example disagree by more than rounding. The rule is that the current scale is 65% and the shadow scale 55% of the guaranteed maximum, which at the worked example’s m^max = 8.615 gives 5.59975 and 4.73825. The worked example is computed with 5.60 and 4.74 – the same figures rounded to the cent per $1,000, which is how the notes quote them – and the difference is about $0.12 a month of base deduction and $0.65 of shadow deduction, an order of magnitude more than the cent-level rounding that explains the rest of that table.

Rather than pick one, the model ships both. A model point that leaves this column blank takes the rule at full precision; the worked-example anchor sets it to 2 and holds both declared scales to the cent per $1,000, which is a perfectly ordinary way for an admin system to carry a rate table and reproduces the notes’ table.

duration_mth(t)[source]#

Completed policy months at the beginning of policy month t.

duration_mth_init() + t - 1, so it is 0 in the issue month of a new-business model point and 300 in the first month of the notes’ worked example. Note the contrast with the notes’ own month index, which counts the current month as well; see surr_charge_rate().

duration(t)[source]#

Completed policy years at the beginning of policy month t.

policy_year(t)[source]#

y: the policy year containing policy month t, 1-based.

age(t)[source]#

The attained age (ANB) in policy month t: age_at_entry() + duration(t).

Age advances on the policy anniversary, not on the birthday, which is the ANB convention the whole model is built on [std]. Mixing an ALB basis into the COI or mortality lookups shifts both by up to half a year of mortality, which the notes list among the pitfalls.

proj_len()[source]#

Projection length in policy months.

12 * (charges_cease_age - age_at_entry()) - duration_mth_init(), the notes’ maximum projection length: the projection runs to attained age 121, where premiums and all charges cease. Coverage continues past that point under the contract [S7], but the illustrative mortality table reaches 1.0 at attained age 120, so nothing survives the horizon.

sum_assured_at(t)[source]#

F(t): the face amount in policy month t.

Level: face increases are not permitted [S2] and elective decreases, option changes and the face reduction some designs attach to a withdrawal are not modelled – the notes’ withdrawal reduces the account and shadow balances only. The cells is kept so the chassis’ shape is unchanged and a design with face movement can specialise it.

units(t)[source]#

U: the face amount in $1,000 units, sum_assured_at(t) / 1000.

Both per-unit charges are quoted per $1,000 of initial face per month, and the surrender charge per $1,000 of initial face; with a level face they coincide.

crediting_rate_ann(t)[source]#

i^c: the current declared annual effective credited rate, 3.50% [std].

A non-guaranteed element declared at insurer discretion within the guaranteed bounds and governed by ASOP 2 [REG-R26]. The base run holds the snapshot scale level, as the notes prescribe; re-rating is out of scope.

inv_return_mth(t)[source]#

j_c: the monthly credited rate, (1 + i^c)^(1/12) - 1, floored at j_g.

0.0028709 at the [std] 3.50% current rate, matching the notes. The floor is the contractual 2.0% guaranteed minimum [S3][S5][S7]; it does not bind at the snapshot scale.

guar_rate_mth()[source]#

j_g: the monthly guaranteed rate, (1 + i_guar)^(1/12) - 1 = 0.0016516.

sg_rate_mth()[source]#

j^g: the monthly shadow credited rate, (1 + i^g)^(1/12) - 1 = 0.0044717.

5.5% annual effective [std], comfortably below the AG 38 8E cap of a Moody’s-composite-yield index plus 3% that classifies a Design #1 shadow account [R1], and well above the 2.0% base guarantee – which is what makes the guarantee outlive the cash value.

naar_factor()[source]#

The base NAAR factor, 1 + j_g = 1.0016516.

The death benefit is discounted one month at the guaranteed rate, never the credited rate. Using the undiscounted death benefit instead changes the cost of insurance by about 0.17% a month at the 2% guarantee, which the notes list first among the pitfalls; the same convention must hold on both accounts.

sg_naar_factor()[source]#

The shadow NAAR factor, 1 + j^g = 1.0044717 [std].

The shadow account discounts the death benefit at its own credited rate, which is what the notes’ step 4 writes, so the two accounts see different net amounts at risk even before their balances diverge.

loan_rate_mth()[source]#

The monthly charged loan rate, (1 + r_L)^(1/12) - 1 at r_L = 5.0% [S4].

Charged in arrears and guaranteed; monthly accrual is the model’s discretization [std].

loan_cr_rate_mth()[source]#

The monthly rate credited on the loaned account value, 3.0% annual [S4].

Guaranteed, and 200 basis points below the charged rate – the contractual loan spread.

premium_freq()[source]#

Scheduled premium payments per policy year, from premium_mode() [S2].

Annual 1, semi-annual 2, quarterly 4, monthly (EFT only) 12. Non-annual modes carry modal factors in the source design; no carrier publishes them, so the scheduled annual premium is divided evenly [std] and every shipped model point is annual.

is_premium_mth(t)[source]#

Whether a scheduled premium falls due at BOM of policy month t.

LEVEL every 12 / premium_freq() months from issue. SINGLE the issue month only. TEN_PAY as LEVEL, for the first ten_pay_years policy years [std].

prem_persistency(t)[source]#

phi_t: the probability the scheduled premium is actually paid [std].

98% a year for a level payer, 100% for single-pay and ten-pay, which is what the notes prescribe; a missed premium is never made up, so it permanently lowers the shadow trajectory, and catch-up behaviour is not modelled in the base run.

The model point may override it, and the worked-example anchor overrides it to 1.00. That is not a tuning: the notes’ worked example is a contract-mechanics view with the behavioural assumptions suppressed, and premium persistency is a class (c) behavioural assumption. The notes are also not self-consistent about where phi belongs – their cash flow list writes premium income as l_t phi_t P_t while their step 2 credits (1 - pi) P_t to the account, which would hand the account value more than the insurer received. This model applies phi once, to the premium actually received, so the account value, the shadow account, the cumulative premium and the premium income all move together.

premium_pp(t)[source]#

P_t: the premium received per policy at BOM of policy month t.

The scheduled premium in a premium month times prem_persistency(), zero otherwise, and zero from attained age 121 when premiums are no longer accepted [S7]. Premiums are flexible in amount and timing after the first [S2][S4]; the model projects the scheduled pattern, which is what the guarantee was solved on.

prem_to_av_pp(t)[source]#

The net premium credited to the base account value, (1 - pi) P_t [S3][S7].

prem_to_sg_pp(t)[source]#

The net premium credited to the shadow account, (1 - pi^g) P_t [std].

The shadow load of 8% sits near the 7% market-wide load allowance AG 38 8B uses [R1] and far below the 25% base load, which is the whole point: the shadow account must credit premiums more generously than the real one for the guarantee to outlast the cash value.

prem_to_av(t)[source]#

Net premium credited to base account values, for the policies in force.

premiums(t)[source]#

Premium income at BOM of policy month t, weighted by the in force at BOM.

cum_prem_pp(t)[source]#

CumPrem_t: cumulative premiums paid per policy.

CumPrem_0 = cum_prem_init(); thereafter CumPrem_{t-1} + P_t, exactly as the notes write it. Withdrawals do not reduce it here – that is the cumulative-premium-test variation’s CumPrem^net, not this one – and it drives the return-of-premium refund [S1].

wd_pp(t)[source]#

W_t: the partial withdrawal per policy at BOM of policy month t.

Available after policy year 1 and not after attained age 121 [S2][S3][S4][S7]. The amount is the constant monthly figure in the model point’s wd_pp column, 0 in every shipped model point: the notes set utilisation to zero in the base model and give no pattern, so the mechanics are implemented and the behaviour is left to the data [std]. A withdrawal reduces the account value by the amount plus the fee and the shadow account dollar-for-dollar, with no fee [S4].

wd_fee_pp(t)[source]#

The $25 withdrawal fee, charged only in a month with a withdrawal [S2][S3][S4][S7].

Retained by the insurer, so it is account-value outgo but not a liability cash flow; it appears in margin_expense(), not in claims(). It is charged against the base account only, never the shadow account.

wd_fees(t)[source]#

Withdrawal fees retained by the insurer, for the policies in force.

corridor_factor(t)[source]#

kappa(x_t): the GPT corridor factor at the attained age [R4][REG-R13].

The IRC 7702(d)(2) applicable percentages, every row of them: 250% to attained age 40, then decreasing by a ratable portion for each full year through the statute’s breakpoints – 215% at 45, 185% at 50, 150% at 55, 130% at 60, 120% at 65, 115% at 70, 105% from 75 to 90 – and 100% from attained age 95 on, which is the statute’s last row. Ages beyond the table take its last row. Because guaranteed UL account values are deliberately thin the corridor never binds in any shipped model point – but it is the reason the death benefit is a max rather than the face amount.

db_pp(t)[source]#

DB_t: the death benefit per policy, max(F, kappa(x_t) max(AV'_t, 0)) [S2][S4].

Level death benefit option only, the guarantee-focused segment’s design [S2][S4]. AV'_t is the account value after the premium, the withdrawal and the expense charges and before the cost of insurance, and it is floored at zero so an exhausted account cannot pull the death benefit below the face amount.

net_amt_at_risk(t)[source]#

NAAR_t: max(DB_t / (1 + j_g) - max(AV'_t, 0), 0) [S3].

Two conventions here are the product’s, not conveniences: the death benefit is discounted one month at the guaranteed rate (naar_factor()), and the account value is measured after the expense charges and before the cost of insurance. The account input is floored at zero so that a deficit – the guarantee-support regime, where the account is exhausted and the insurer is funding the deduction – never inflates the net amount at risk above the discounted death benefit. In that regime the cost of insurance is charged on essentially the whole face amount, which is what dominates late-duration guaranteed UL cash flows.

sg_net_amt_at_risk(t)[source]#

NAAR_t^g: the shadow net amount at risk, max(DB_t / (1 + j^g) - max(SG'_t, 0), 0).

The same construction as net_amt_at_risk() on the shadow parameter set [std], discounting at the shadow credited rate and flooring the shadow balance at zero so that catch-up territory – a negative shadow account – does not inflate the shadow cost of insurance.

coi_rate_scale()[source]#

The guaranteed maximum annual COI scale for this model point’s cell.

A Series indexed by attained age, per $1,000 of net amount at risk, sliced once from coi_rates.csv for this sex and rate_class. The shipped table covers the anchor cell M / StdNT over attained ages 45-121 only; a model point on any other cell, or a younger attained age, needs the table extended first.

coi_rate_guar(t)[source]#

m_t^max: the guaranteed maximum monthly COI rate per $1,000 of NAAR.

The annual rate from coi_rates.csv divided by twelve – the notes’ simple-twelfth conversion, fixed [std]. It differs materially from 1 - (1 - q)^(1/12) at ages 85 and over, where q exceeds 0.10, and the notes are explicit that the two must not be mixed. Model 585 requires the guaranteed maxima to be stated in the policy [R3]; carriers do not publish them, so the shipped scale is illustrative [std], not the 2017 CSO table [REG-R17].

coi_rate(t)[source]#

m_t: the current monthly COI rate, 65% of the guaranteed maximum [std].

Current COI scales are not published by any carrier, so the factor is a pure modelling assumption and one of the first things to sensitivity-test. The scale is held to coi_rate_dp() decimals per $1,000, which reconciles the notes’ worked example with the notes’ own factor rule – see coi_rate_dp().

sg_coi_rate(t)[source]#

m_t^g: the shadow monthly COI rate, 55% of the guaranteed maximum [std].

Kept below the current base rate of 65% so the shadow account depletes more slowly than the real one, which is the defining behaviour of the product [S2][S7]. Rounded like coi_rate().

coi_pp(t)[source]#

COI_t: the base cost of insurance charge, m_t NAAR_t / 1000.

Zero from attained age 121, when all charges cease [S3][S7].

sg_coi_pp(t)[source]#

COI_t^g: the shadow cost of insurance charge, m_t^g NAAR_t^g / 1000 [std].

Notional: it never leaves the insurer and is not a cash flow. Zero from attained age 121, when the shadow charges cease with the base ones [std].

rider_charge_pp(t)[source]#

Rider charges deducted monthly, 0 in the base model [std].

Rider charges are one of the sourced monthly charge categories [S2][S3][S4][S7][S9], but neither rider in scope carries one: the terminal illness accelerated benefit takes no premium [S2][S9], and the return-of-premium endorsement is built into the representative contract rather than charged for [S1]. The term is carried, as it is on the universal life chassis, so that a rider module can be added without changing the recursion.

maint_fee_pp(t)[source]#

The non-COI part of the base monthly deduction, e_pol + e_u U + rc [S3][S7].

The $5.50 per-policy administrative charge [S3][S7], the $0.20 per $1,000 of initial face per month coverage charge [std] and rider charges. Zero from attained age 121, when charges cease [S3][S7].

The name follows CashValue_SE.maint_fee: this is a charge against the account value and therefore insurer income. It is not expenses(), which is the insurer’s own outgo.

sg_maint_fee_pp(t)[source]#

The non-COI part of the shadow monthly deduction, e_u^g U [std].

$0.05 per $1,000 of initial face per month and no per-policy charge – the simplest representative choice, since no carrier publishes shadow parameters and AG 38 8E only describes shadow accounts as carrying expense charges [R1].

mth_deduction_pp(t)[source]#

MD_t: the full base monthly deduction scheduled at BOM, maint_fee + COI.

This is what the worked example’s “Base deductions” column shows. What actually leaves the account is mth_deduction_taken_pp(); the remainder is mth_deduction_forgone_pp().

sg_deduction_pp(t)[source]#

The full shadow monthly deduction, sg_maint_fee_pp + sg_coi_pp [std].

The worked example’s “Shdw deductions” column. The shadow account is never floored, so it is always taken in full – there is no shadow analogue of the forgone deduction.

maint_fee_taken_pp(t)[source]#

The part of maint_fee_pp() the account value could actually carry.

The expense charges are deducted before the cost of insurance, so they are met first out of the balance standing after the premium and the withdrawal.

coi_taken_pp(t)[source]#

The part of coi_pp() the account value could actually carry.

The cost of insurance is deducted last, so it absorbs the shortfall first: this is the term that goes unpaid in the guarantee-support regime.

mth_deduction_taken_pp(t)[source]#

The monthly deduction actually taken from the account value in month t.

mth_deduction_forgone_pp(t)[source]#

D_t: the monthly deduction forgone because the account value is exhausted.

MD_t - (what the account could carry), which equals the notes’ -AV''_t whenever the balance before the charges is non-negative. While the guarantee stands (is_guar_supported()) the insurer simply funds it and coverage continues with the account value at zero; when the guarantee has failed the same shortfall opens the grace period instead.

It is not a receivable. It must never accrue against future premiums or account value recoveries – the notes list that among the pitfalls, because treating it as one understates the guarantee cost. Nothing in the projection reads this cells except the diagnostics and cure_premium_pp().

It reports the shortfall whenever there is one, which includes the grace months and the months after a lapse – where the notes’ D_t is not defined at all, the shortfall being their required grace payment instead, and where the zero floor above it is this model’s own [std] extension (av_pp_at()). Read it beside is_guar_supported(), not on its own.

maint_fee(t)[source]#

Non-COI monthly charges actually deducted from account values, in force.

coi(t)[source]#

Cost of insurance charges actually deducted from account values, in force.

mth_deduction(t)[source]#

Monthly deductions actually taken from account values, in force.

mth_deduction_forgone(t)[source]#

Monthly deductions forgone because the account value is exhausted, in force.

While is_guar_supported() holds this is the running cost of the “negative account economics” regime the notes describe: the insurer is paying for coverage on a policy whose account value is zero. In the grace months it is not – there the shortfall is the notes’ required grace payment on a policy about to terminate – so result_guar() prints it beside is_guar_active. After the lapse pols_if(t) is zero and so is this, whatever the per-policy cells say.

av_pp_at(t, timing)[source]#

Base account value per policy at an intra-month point of policy month t.

The BOM events change the balance in this order, and timing names the point just before each of them:

"BEF_PREM"

Before the premium: the closing balance of the previous month, AV_{t-1}.

"BEF_WD"

After the net premium, before the withdrawal.

"BEF_FEE"

After the withdrawal and its fee, before the expense charges.

"BEF_COI"

After the expense charges, before the cost of insurance. This is the notes’ ``AV’(t)``, and the balance the death benefit, the corridor test and the net amount at risk are all measured against. The universal life chassis measures them one step earlier, at "BEF_FEE"; the guaranteed-UL notes flag the difference as a deliberate deviation.

"BEF_INV"

After the cost of insurance and after the zero floor, before interest. Interest is credited on this post-deduction balance; reversing the two overstates the account value by about one month’s interest on the deduction every month. The floor is what the guarantee buys: the account value stops at zero and the shortfall becomes mth_deduction_forgone_pp() rather than a negative balance.

Documented deviation [std]. The notes floor the account value at zero only while the guarantee is active – their step 6 sets AV''_t = 0 in the guarantee branch and gives the grace branch no account-value recursion at all. This model applies the floor unconditionally, in the grace months and after a lapse as well. A negative balance would break the account-value roll-forward, which closes on the deduction actually taken, and nothing is taken from an exhausted account. No cash flow moves either way – a policy in grace surrenders for nothing by construction and pols_if(t) is zero once it has lapsed – but the per-policy account-value cells do keep running after the lapse, where they describe no policy. See mth_deduction_forgone() and the README section “The forgone deduction is the product”.

The end-of-month balance AV_t is av_pp().

inv_income_pp(t)[source]#

Interest credited to the base account value at EOM of policy month t.

The unloaned part of the post-deduction balance earns the current monthly rate and the loaned part the guaranteed loaned rate of 3.0% [S4]:

(AV''_t - L_{t-1}) x j_c + L_{t-1} x loan_cr_rate_mth()

With the account value exhausted the credit is zero, which is why the worked example shows no interest from month 304.

av_pp(t)[source]#

AV_t: the base account value per policy at the end of policy month t.

AV_0 = av_pp_init(); thereafter the floored post-deduction balance plus one month’s interest. An exhausted account never goes negative and the shortfall is recorded as mth_deduction_forgone_pp() instead.

Floored at zero throughout – in grace and after a lapse as well as under a live guarantee. The notes floor it “only while the guarantee is active”; applying the floor unconditionally is a [std] deviation that moves no cash flow but does leave this cells running after the policy has gone. av_pp_at() sets out why.

av_at(t, timing)[source]#

Base 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. timing takes the same values as av_pp_at(), plus "EOM" for the closing balance before decrements.

inv_income(t)[source]#

Interest credited to base 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_change(t)[source]#

Change in the base account value in force over policy month t.

av_at(t + 1, "BEF_PREM") - av_at(t, "BEF_PREM"), following CashValue_SE.

loan_bal_pp(t)[source]#

L_t: the policy loan balance per policy at the end of policy month t.

L_0 = loan_bal_init(); thereafter L_{t-1} x (1 + r_L)^(1/12) at the guaranteed 5.0% charged in arrears [S4], accrued monthly [std]. New loans and repayments are not modelled – the notes give no utilisation pattern – so this only rolls the model point’s opening balance forward. Indebtedness is deducted from the guarantee in-force test (sg_net_pp()), from death proceeds and from the surrender value; the shadow account itself is not reduced by it [S4][S2].

sg_pp_at(t, timing)[source]#

Shadow account value per policy at an intra-month point of policy month t.

The same timings as av_pp_at(), on the shadow parameter set:

"BEF_PREM"

The closing shadow balance of the previous month, SG_{t-1}.

"BEF_WD"

After the shadow net premium (1 - pi^g) P_t, before the withdrawal.

"BEF_FEE"

After the withdrawal, which reduces the shadow account dollar-for-dollar and carries no fee [S4] [std], before the per-unit charge.

"BEF_COI"

After the shadow per-unit charge, before the shadow cost of insurance. This is the notes’ ``SG’(t)``.

"BEF_INV"

After the shadow cost of insurance, before interest. This is the notes’ SG''(t) and it is not floored: a negative shadow balance is the catch-up shortfall, and flooring it destroys catch_up_prem_pp() and misprices restoration.

sg_inv_income_pp(t)[source]#

Interest credited to the shadow account at EOM, SG''_t x j^g [std].

Credited on the post-deduction shadow balance with no floor and no loaned/unloaned split: the shadow account is notional and carries no loan of its own.

sg_pp(t)[source]#

SG_t: the shadow account value per policy at the end of policy month t.

SG_0 = sg_pp_init(); thereafter SG''_t x (1 + j^g). Notional throughout: it exists only to run the in-force test and is never payable [S2][S3].

sg_net_pp(t)[source]#

SG_t - L_t: the shadow account net of indebtedness, the in-force test quantity.

Indebtedness is deducted from the guarantee measure rather than from the shadow account itself [S4][S2]. The mainstream design; the harshest observed alternative voids the guarantee outright on any loan [S5].

is_guar_active(t)[source]#

The step-9 in-force test: SG_t - L_t > 0 [S4][S2][S9].

Measured at EOM, after the shadow interest credit and the loan accrual. While it holds the policy cannot lapse however exhausted the real account value is; when it fails, an exhausted account opens the grace period. Strictly greater than zero, as the notes require: a >= 0 target on a monthly grid can leave the guarantee failing on the final monthiversary.

is_guar_supported(t)[source]#

The step-6 test: SG''_t - L_{t-1} > 0, measured before the interest credits.

This is the one that decides what happens to a failed deduction – forgone by the insurer, or grace. It is deliberately a different measurement point from is_guar_active(), and it is evaluated after the full monthly deduction attempt: testing before the deduction lets a policy lapse a month early or late and shifts claim timing at exactly the durations where the net amount at risk is the whole death benefit.

catch_up_prem_pp(t)[source]#

C_t: the premium that would restore the guarantee, max(0, -(SG_t - L_t)) / (1 - pi^g).

The negative net shadow balance grossed up for the shadow premium load [std]; paying it brings SG - L back to zero and the guarantee with it [S7][R1 ex. 7]. A diagnostic only: the notes state expressly that catch-up behaviour is not modelled in the base run, so no policy ever pays it. This is why the shadow account must never be floored at zero.

surr_charge_rate(t)[source]#

SC per $1,000 of initial face in policy month t [std].

max(0, sc_init - (sc_init / runoff_years) x m / 12) where m is the notes’ own month index duration_mth(t) + 1 – the current month counts. With the shipped 15-year schedule at $18 per $1,000 this is the spec’s 18 x max(0, (180 - m) / 180): $17.90 in the issue month, zero from the last month of policy year 15. Reading the notes’ month index as duration_mth(t) would shift the entire run-off by a month.

surr_charge_pp(t)[source]#

SC_t: the surrender charge scheduled per policy in policy month t.

Quoted on the initial face amount. This is the schedule, not the amount collected: see surr_charge().

csv_pp(t)[source]#

The cash surrender value per policy, AV_t - SC_t, floored at zero.

The floor is [std]: a negative cash surrender value would be a payment from the policyholder. On this product it binds for years – guaranteed UL account values are deliberately thin and the 15-year surrender charge starts at $9,000 on a $500,000 face.

ncsv_pp(t)[source]#

CSV_t in the notes: the net cash surrender value, max(AV_t - SC_t - L_t, 0).

What a surrendering policyholder is paid, and the notes’ surrender outgo. The notes’ symbol CSV_t already nets indebtedness, so it is this cells and not csv_pp(); the chassis keeps the two apart so the surrender-charge and the debt offsets can be read separately.

surr_charge(t)[source]#

Surrender charge actually collected from the policies surrendering in month t.

(AV_t - CSV_t) x pols_lapse(t), so it is capped by the account value where the csv_pp() floor binds – which on this product is most of the first fifteen years. Insurer income, and part of margin_expense().

is_shortfall(t)[source]#

Whether the monthly deduction attempt failed: AV'_t - COI_t < 0.

The notes’ step 6 condition, evaluated after the full deduction attempt. On its own it says nothing about lapse: while is_guar_supported() holds the shortfall is forgone by the insurer and coverage continues; only when the guarantee has failed as well does it open the grace period.

cure_premium_pp(t)[source]#

The payment that would cure a grace: the deduction shortfall, grossed up.

D_t / (1 - pi) [std] – the notes define the required grace payment as the amount curing the deduction shortfall, and a premium reaches the account value net of the load. A diagnostic: the notes give no cure probability, so a policy that enters grace always lapses when the 61 days expire.

grace_mth(t)[source]#

g_t: months elapsed in the grace period, 0 when not in grace [S7].

The counter advances only when the deduction attempt failed and the guarantee is not supporting the policy; a failed deduction under an active guarantee is forgone and never opens a grace. g_0 = 0.

is_lapsed(t)[source]#

Whether the policy has terminated for insufficiency at or before BOM of month t.

The 61-day grace period [S7] is taken as grace_months = 2 policy months [std]; when it expires without the required payment the policy lapses at BOM with no value – the cash surrender value is zero in grace by construction. Lapse for insufficiency requires all three of the notes’ conditions: the deduction attempt failed, SG - L <= 0, and the grace expired uncured.

status(t)[source]#

The worked example’s Status column, in ASCII.

"IN FORCE"

the account value is carrying the policy;

"IN FORCE - GUARANTEE"

the account value is exhausted and the guarantee is carrying it – the notes’ “in force - guarantee”;

"GRACE"

the deduction failed with no guarantee behind it;

"LAPSED"

the grace expired uncured.

rop_anniversary(t)[source]#

The return-of-premium anniversary whose window contains month t, or 0 [S1].

The endorsement is exercisable during the 60 days following policy anniversaries 20 and 25 [S1][S3][S4]. On a monthly grid the window is taken as the anniversary month itself [std], so the exercise rate is applied once rather than spread over two monthiversaries.

rop_ratio(t)[source]#

rho: the fraction of cumulative premiums refunded in the window, 50% or 100% [S1].

rop_rate(t)[source]#

w^ROP: the fraction of eligible in-force exercising in the window [std].

5% at the year-20 window and 10% at the year-25 window. No public exercise study exists; the rationale for keeping them modest is that the 100% refund dominates the cash surrender value on a thin-account product, but exercising forfeits a now-cheap guarantee. Mis-setting them distorts years 20-26 of the cash flows.

class_factor()[source]#

The underwriting-class multiplier on the best-estimate mortality table [std].

mort_improve_rate(t)[source]#

The annual mortality improvement rate at the attained age in month t [std].

1.0% a year to attained age 85, grading linearly to 0% at attained age 95 and zero thereafter. Improvement compounds, so at the late attained ages where the net amount at risk is the whole death benefit it is one of the two assumptions that move the claims most.

mort_improve_factor(t)[source]#

The cumulative mortality improvement factor at policy month t [std].

1.0 in the first projected year, then one further year of improvement on each anniversary of the projection start, for at most mort_improve_max_years = 20 years as the notes prescribe. Improvement is applied on projection anniversaries [std]; for every shipped model point those coincide with policy anniversaries.

mort_rate(t)[source]#

The annual best-estimate mortality rate in policy month t.

Base table x class_factor() x the A/E factor, which is 100% in the base run [std], x mort_improve_factor(), capped at 1.0. The shipped table is a small illustrative one [std], not the 2015 VBT the notes recommend – that family is licensed and may not be reproduced here. Ages beyond the table take its last row, where the rate is 1.0; the cap is what keeps a class factor above 1 from pushing the terminal rate past certainty.

mort_rate_mth(t)[source]#

q_t^d: the monthly best-estimate mortality rate, 1 - (1 - q)^(1/12).

Note that the experience decrement uses the compound conversion while the contractual COI rate uses the simple twelfth (coi_rate_guar()). The notes prescribe exactly that split; the two must not be interchanged.

lapse_rate_base(t)[source]#

b(d): the base annual lapse rate by policy year [std].

4.0%, 3.0%, 2.5%, then 2.0% in years 4-5, 1.5% in 6-10, 1.0% in 11-20 and 0.75% thereafter, read from lapse_table.csv; policy years beyond the table take its last row. The shape is anchored to the public highlights of the SOA/LIMRA UL persistency and lapse studies [R7][REG-R20][REG-R21], whose detailed tables sit in a paid data package, so the levels are a standardization.

lapse_rate_guar_mult()[source]#

G: the guarantee-duration lapse multiplier, 0.55 for a lifetime election.

Lifetime secondary-guarantee lapse rates run 45% below non-lifetime rates on both count and amount bases in the 2015-2021 industry experience [R7]; the level is derived from that finding and the flat duration shape is [std]. This is the first-order assumption for a lapse-supported product: every lapse of a funded guarantee releases the insurer from a deeply in-the-money claim.

lapse_rate_pattern_mult()[source]#

Phi: the premium-pattern lapse multiplier [std].

Single-pay 0.6, ten-pay 0.8, level 1.0, in the direction [R8] reports – higher lapses for level-pay, lower for single-pay.

lapse_rate_dyn_mult(t)[source]#

Psi_t: the funding-status dynamic lapse factor [std].

1.0

guarantee active and the account value still positive;

0.6

guarantee active and the account value exhausted – the policy is deep in the money to the policyholder, the regime [R8]’s tail scenarios keep 40% of policies in after 31 years;

2.0

the guarantee has terminated and the policy is surviving on its account value alone – a shock. Where the account value has gone too the policy is already in grace and about to lapse, so the rate is academic there.

Dynamic lapse is used by 63% of surveyed ULSG writers, and lapse and tail investment returns are rated the most critical ULSG assumptions [R8]; the formula itself is a standardization.

lapse_rate(t)[source]#

The total annual lapse rate, min(0.5, max(0.003, b(d) G Phi Psi_t)) [std].

The 0.3% annual floor is applied after the dynamic factor, as the notes write it, and the 50% cap wraps the result; the two cannot conflict.

lapse_rate_mth(t)[source]#

w_t: the monthly lapse rate, 1 - (1 - w_annual)^(1/12).

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) = l_0 = pols_if_init(). A policy whose grace has expired is out at BOM with no value, which is why is_lapsed() is tested first.

pols_if_at(t, timing)[source]#

Number of policies in force at time t, by timing.

All three CashValue_SE timings coincide for this product, and all equal pols_if(): there is no new business inside a projection and the contract has no maturity date, so nothing changes the policy count between BOM and the end-of-month decrements.

pols_death(t)[source]#

Number of deaths at the end of policy month t, l_t x q_t^d.

pols_lapse(t)[source]#

Number of surrenders at the end of policy month t.

l_t (1 - q_t^d) w_t: death is applied before lapse, which is the notes’ ordering.

pols_rop(t)[source]#

Number of return-of-premium exercises at the end of policy month t.

l_t (1 - q_t^d)(1 - w_t) w_t^ROP, matching the notes’ l_{t+1} = l_t (1 - q^d)(1 - w)(1 - w^ROP). Exercise is a full surrender [S1][S3], so an exercising policy leaves with the refund and nothing else.

pols_lapse_grace(t)[source]#

Number of policies terminating for insufficiency at the end of policy month t.

Non-zero only in the month before the grace period expires, when every remaining policy is out. This is not a rate-based decrement: it is the contractual termination of a policy whose account value failed and whose guarantee had already gone, and it is needed for the in-force roll-forward to close. The policies leave with no value, so it generates no claim – the cash surrender value is zero in grace by construction.

pols_maturity(t)[source]#

Number of maturing policies: always zero.

Guaranteed UL has no maturity date – at attained age 121 premiums and charges cease and coverage continues [S7]. 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 outstanding indebtedness, standard UL treatment [std].

"LAPSE"

ncsv_pp(), the notes’ CSV_t = max(AV_t - SC_t - L_t, 0).

"REFUND"

min(rho CumPrem_t, 0.40 F) - L_t, floored at zero [S1]: the return-of-premium refund, capped at 40% of the face amount and net of debt. On the anchor cell the cap binds – 25 years of $10,800 premiums is $270,000 against a $200,000 cap – which is exactly why the cap exists.

"WITHDRAWAL"

W_t. The $25 fee is retained by the insurer and is not part of the payment. A withdrawal is not a claim – the aggregate cash flow is withdrawals(), not claims(t, "WITHDRAWAL"), which raises – but the per-policy amount keeps its branch here because withdrawals() weights it.

"GRACE"

Zero: a policy terminating in grace terminates without value [S7].

claims_from_av(t, kind)[source]#

The part of a claim released from the account value, by kind.

Death, surrender and refund all release the end-of-month account value AV_t, because decrements follow the interest credit; so does a policy terminating in grace, though its account value is zero by construction. "MATURITY" is zero: the contract has no maturity date.

"WITHDRAWAL" keeps its branch even though a withdrawal is not a claim: it is taken at BOM out of the account values of the policies still in force, and it is the same figure as withdrawals().

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 – and in the guarantee-support regime, where the account value is zero and the charge is forgone, it is the whole face amount with no charge behind it.

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

Claim outgo in policy month t, optionally by kind.

kind is "DEATH", "LAPSE", "REFUND", "GRACE", or None for the total. Death claims are weighted by pols_death(), surrenders by pols_lapse() and refunds by pols_rop(), all end-of-month events.

"WITHDRAWAL" is deliberately not a claim kind and raises: a partial withdrawal is a payment on the owner’s election out of a policy that stays in force, so it is withdrawals(). Keeping it out of this cells is what keeps the kind is None total from double-counting it against the withdrawals column of result_cf().

withdrawals(t)[source]#

Partial withdrawal payments in policy month t, for the policies in force.

W_t x l_t. Withdrawals are taken at BOM by policies still in force, so the weight is pols_if() and not a decrement. A withdrawal is a payment on the owner’s election, not a claim, which is why it is a cells and a result_cf() column of its own rather than a kind of claims(). The $25 fee is retained by the insurer and is wd_fees(), not part of this payment.

Zero in every shipped model point: the notes set withdrawal utilisation to zero in the base model and give no pattern, so the mechanics are implemented and the behaviour is left to the data [std].

inflation_factor(t)[source]#

The expense inflation factor, (1 + inflation_rate)^(y - 1) [std].

Expenses inflate by policy year, not by month, which is how the notes write the $75 per policy per year maintenance expense.

expenses(t)[source]#

The insurer’s own expenses in policy month t [std].

Acquisition in policy year 1 – $300 a policy in the issue month plus 90% of every first-year premium, which is the notes’ combined commission and issue allowance – maintenance of $75 a policy a year inflating at 2.5%, spread evenly over the months, and $300 of claim expense per death.

Not to be confused with maint_fee(), which is the charge against the account value.

premium_taxes(t)[source]#

Percent-of-premium expense, zero on this product.

The universal life chassis carries a 2.5% premium tax; the guaranteed-UL notes’ expense list has no percent-of-premium item at all – the commission sits inside the acquisition expense instead – so the rate is zero and the cells is kept only so result_cf() has the chassis’ shape. Adding a tax here would be an unsourced assumption.

margin_expense(t)[source]#

Expense margin: the charges the insurer keeps, net of its own outgo.

pi x GP + withdrawal fees + the deduction's non-COI part actually taken + surrender charges collected + the account value left behind by a policy terminating in grace - expenses - premium taxes. The last of those is zero by construction, because the account value is exhausted before a grace can begin; it is carried so check_margin() closes without a special case.

margin_mortality(t)[source]#

Mortality margin: coi() net of claims_over_av().

Deeply negative once the account value is exhausted: the cost of insurance is forgone while the death benefit is still the whole face amount. That is the “negative account economics” regime the notes describe, and it is what dominates late-duration guaranteed UL liability cash flows.

margin_rop(t)[source]#

Return-of-premium margin: the account value released less the refund paid.

(AV_t - claim_pp(t, "REFUND")) x pols_rop(t). Large and negative on a thin-account product, which is the point of the endorsement: it is an option against the insurer whose cost depends on cumulative premiums against the reserve released.

net_cf(t)[source]#

Net liability cash flow in policy month t, undiscounted.

premiums - death claims - surrender payments - refunds - withdrawal payments - expenses - premium taxes. Income-positive, as in every model of this library. Withdrawals are subtracted here as withdrawals() rather than through claims(), which no longer carries them.

Like the rest of this library the model projects gross liability cash flows: there is no discounting and no change in account value in this figure, because reserves are a separate layer that consumes these flows. Loads, charges, interest credits and every shadow-account entry are internal transfers and do not appear – see check_margin() for how they reconcile.

solve_len()[source]#

The number of months the funding-premium solve has to keep the guarantee alive.

12 x (guarantee_age() - age_at_entry()) - duration_mth_init(), the notes’ stopping time. A shorter guarantee age solves the same way with the earlier stopping time.

sg_pp_solve(t, prem)[source]#

SG_t(P): the shadow account under a hypothetical premium scale prem.

prem is the annual premium for a level or ten-pay pattern and the single premium for a single-pay one; the payment months are the model point’s own, so single-pay and n-pay premiums solve over their premium vectors exactly as the notes prescribe.

A self-contained replay of the shadow recursion with decrements off and premium persistency off – the solve is contractual, not behavioural, as the notes say – and with the death benefit held at the face amount. The notes justify the latter by capping the search domain at the guideline premium limitation [R4], inside which the corridor does not bind for this thin-account design.

It shares sg_coi_rate() and units() with the projection, so the COI lookups are cached across bisection iterates.

guar_min_sg(prem)[source]#

g(P): the smallest value of SG_t(P) - L_t over the guarantee period.

Monotone non-decreasing in P on the notes’ search domain, which is what makes bisection safe. Evaluated in increasing t so the shadow recursion never recurses deeply.

no_lapse_premium()[source]#

P*: the smallest premium on this model point’s pattern for which g(P) > 0.

The notes’ funding-premium solve. The bracket starts at zero and doubles the upper end until the guarantee is funded, then bisects to solve_tol = $0.01 of annual premium [std]. The target is g(P) > 0 strictly: a >= 0 target on a monthly grid can leave the guarantee failing on the final monthiversary.

A side calculation – nothing in the projection depends on it. For the new-business level-pay cell it returns about $10,800, which is the figure the notes calibrated the [std] shadow parametrization to produce; the illustrative COI curve shipped with the model is fitted so that it does. On the in-force anchor it answers a different question – the level premium needed from the projection start, given the opening shadow balance – and returns far more, because the notes’ opening shadow value is not the balance a fully funded policy would carry at duration 300.

check_av_roll_fwd()[source]#

Check the base account value roll-forward.

Returns True when, for every projected month, the opening account value in force of month t + 1 equals:

av_at(t, "BEF_PREM")
    + prem_to_av(t)
    - withdrawals(t) - wd_fees(t)
    - mth_deduction(t)
    + inv_income(t)
    - claims_from_av(t, "DEATH") - claims_from_av(t, "LAPSE")
    - claims_from_av(t, "REFUND") - claims_from_av(t, "GRACE")

This pins the notes’ processing order: that interest is credited on the post-deduction balance, that decrements come after the credit, and that what leaves the account is the deduction actually taken and not the deduction scheduled.

check_sg_roll_fwd()[source]#

Check the shadow account roll-forward, per policy.

Returns True when, for every projected month:

sg_pp(t) == sg_pp(t - 1) + prem_to_sg_pp(t) - wd_pp(t)
            - sg_deduction_pp(t) + sg_inv_income_pp(t)

The shadow account is notional and carries no decrements, so this is a per-policy identity with no in-force weighting. It is the check that the shadow account is never floored: if a zero floor crept in, this would fail the moment the balance went negative.

check_margin()[source]#

Check the net cash flow against the expense, mortality and refund margins.

Returns True when, for every projected month:

net_cf(t) == margin_expense(t) + margin_mortality(t) + margin_rop(t)
             + av_change(t) - inv_income(t)
             + loan_bal_pp(t) * pols_lapse(t)

The last three terms are what separates a gross liability cash flow model from CashValue_SE, whose net_cf already nets the change in account value and the investment income; the loan term is the debt extinguished against the account value when a policy with a loan surrenders. The identity holds while neither the csv_pp() nor the ncsv_pp() floor binds against a policy loan, which is the case for every shipped model point.

result_cf()[source]#

Result table of cashflows, a DataFrame indexed by policy month t.

pols_if is the in-force weight applied to that same row’s cash flows – the number in force at the start of the month – and the remaining columns are income-positive under net_cf: premiums - claims_death - claims_lapse - claims_rop - withdrawals - expenses - premium_taxes. The surrender column is claims_lapse, matching the "LAPSE" kind that produces it, and withdrawals are their own column rather than a claim.

result_pols()[source]#

Result table of policy decrements, a DataFrame indexed by policy month t.

result_av()[source]#

Result table of the two account values, per policy.

The columns are the columns of the worked example in the technical notes, in the notes’ own order – premium, net premium to each account, the deductions on each, the interest credited to each, the two closing balances – followed by the forgone deduction, which the notes write inline in the deductions cell, and the status.

result_guar()[source]#

Result table of the guarantee diagnostics, a DataFrame indexed by t.

The net amount at risk and cost of insurance on each account, the deduction the insurer forgoes across the policies in force – the running cost of the guarantee – the shadow account net of debt, whether the guarantee is active, the catch-up premium that would restore it and the grace counter.