The Projection Space#

The by-contract projection of the SPIA_US_S 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 anchor cell
>>> Projection.point_id = 8            # or switch the default

t counts policy months from the annuity date, 1-based: t = 1 is the first projected month and t = 0 is the month-zero state used as the base case of every recursion (lives_if(0, life) = 1, cum_annuity_pp(0) = 0, commute_frac_cum(1) = 0).

Input data

Inputs are external files: plain CSVs living in the model folder’s parent directory, products/immediate_annuity/, 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 SPIA_US_S 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 Data:

Reference

Cells

File

model_point_file

data.model_point_table()

model_point_table.csv

mort_table_file

data.mort_table()

mort_table.csv

improvement_scale_file

data.improvement_scale()

improvement_scale.csv

surr_charge_file

data.surr_charge_table()

surr_charge_table.csv

Naming

Cells names follow lifelib’s basiclife.BasicTerm_S and savings.CashValue_SE wherever those models have an analogue — pols_* for contract counts, plural nouns for cash flows, *_rate for rates, *_pp for per-policy amounts, claims(t, kind) with an uppercase kind string, result_cf() for the cash flow table. The technical notes use compact actuarial symbols instead. The mapping is:

Notes symbol

Cells

Meaning

t

(the cells argument)

Month from the annuity date

y(t) = ceil(t/12)

policy_year(t)

Policy year containing month t

(none)

duration(t)

Completed policy years, y(t)-1

(none)

duration_mth(t)

Months elapsed at end of month t

m

payment_freq()

Payments per year

timing

payment_timing()

arrears (default) or advance

T

is_payment_mth(t)

Month t is a payment date

(payment point)

payment_surv_mth(t)

Month survival is measured at

P

premium_pp()

Single premium

tau

premium_tax_rate()

State premium tax rate

P_net

premium_net_pp()

P(1 - tau), the amount annuitized

B(y)

annual_income(y)

Unreduced annualized income

inst(t)

annuity_pp(t)

Instalment per contract payable

(inst, untrimmed)

annuity_pp_sched(t)

Scheduled instalment before trim

g

cola_rate()

Fixed compound COLA rate

delta

survivor_pct()

Survivor percentage

trig

reduction_trigger()

either / primary

form

form()

Payout form, one of five

n

certain_mths()

Elected certain months

n_R

certain_mths_refund()

Derived installment-refund months

n_eff

certain_mths_eff()

Effective certain months by form

joint

is_joint()

Contract covers two lives

x1, x2

age_at_entry(life)

Issue ages (ANB), life = 1 or 2

(none)

age(t, life)

Attained age (ANB)

(none)

sex(life)

Sex of each covered life

l_i(t)

lives_if(t, life)

Survival probability to end of t

d_i(t)

lives_death(t, life)

Death density

l_last(t)

lives_if_last(t)

At least one covered life alive

d_last(t), d_term(t)

lives_death_last(t)

Last-death density

L(t)

payment_factor_life(t)

Life-contingent payment factor

C(t)

certain_floor(t)

Certain floor 1{t <= n_eff}

Phi(t)

payment_factor(t)

max(C(t), L(t))

G(t)

cum_annuity_pp(t)

Cumulative scheduled instalments

IF(t)

pols_if(t)

Contracts with an obligation open

(none)

pols_if_init()

Contracts in force at t = 0

(none)

annuity_year()

Calendar year of the annuity date

(none)

calendar_year(t)

Calendar year containing month t

q_x (table)

mort_rate_base(t, life)

Base-table annual mortality rate

G2_x

improvement_rate(t, life)

Generational improvement rate

AE

mort_ae_factor

A/E factor, 1.084 [std]

theta

rating_factor()

Substandard multiplier

q_rated(x, cal)

mort_rate(t, life)

Annual rate after AE and theta

q_m(t)

mort_rate_mth(t, life)

Monthly rate 1-(1-q)^(1/12)

(none)

mort_basis()

table or scenario run [std]

(none)

death_mth(life)

Scenario month of death [std]

omega

omega_age

Limiting age, 120

(stopping rule)

horizon_mths()

Months to the age stop rule

(none)

proj_len()

Last projected month

(inst after commutation)

annuity_pp_paid(t)

inst(t)*(1 - theta_cum*C)

E[ANN(t)]

annuity_payments(t)

Expected annuity outgo

E[CR(t)]

claims(t, “REFUND”)

Cash-refund lump sum

(per contract)

claim_pp(t, kind)

Refund amount per contract

E[COMM(t)]

commutations(t)

Commutation paid to the owner

E[EXP(t)]

expenses(t)

Maintenance expense

CF(t)

liability_cf(t)

Total gross liability outgo

(none)

net_cf(t)

-liability_cf(t), insurer sign

commutation_enabled

commutation_enabled()

Contract has the withdrawal right

(same name)

issue_state_excludes_withdrawal()

Oregon bars the withdrawal

qualified

qualified()

Qualified money; inert here

(none)

commute_elig(t)

Withdrawal allowed in month t

CV(t)

commuted_value(t)

Commuted value of certain tail

j(t)

commute_disc_rate(t)

Commutation discount rate

j0

commute_disc_rate_base

Base commutation rate, 4.00%

CMT10(t) - CMT10(0)

cmt10_shift

10-yr CMT change since issue

sc(y)

surr_charge_rate(y)

Surrender charge rate

sc(y) x W

surr_charge(t)

Surrender charge retained

W

commute_amount(t)

Gross withdrawal requested

theta_cum(t)

commute_frac_cum(t)

Cumulative commutation fraction

u(y)

commute_util_rate(y)

Commutation utilization [std]

c_e

expense_maint

Maintenance expense p.a.

pi

inflation_rate

Expense inflation

Every attribute of the notes’ Model point attributes table has a row above, and so do the model’s own additions, which the notes give no symbol but which drive the generational basis, the verification anchor and the commutation eligibility test: pols_if_init, annuity_year, calendar_year, mort_basis, death_mth and commute_elig. Only the outputs and diagnostics are left out — result_cf(), result_pols(), check_lives_roll_fwd() / check_lives_roll_fwd_resid() and check_payment_factor() / check_payment_factor_resid() — because they are not quantities the notes define. Most model point attributes keep the notes’ own name; issue_state_excludes_withdrawal is one of them and is entered as (same name) only because it is too wide for the first column.

Five names in that table needed care.

l and L differ in the notes only by case — survival probability versus the life-contingent payment factor — so they become lives_if and payment_factor_life. Likewise G(t), cumulative instalments, collides with G2_x, the improvement scale: cum_annuity_pp and improvement_rate. And theta is used twice, once as the substandard mortality multiplier and once as theta_cum, the cumulative commutation fraction: rating_factor and commute_frac_cum.

d_term(t) is not a separate quantity. The notes define it as d_1 on a single-life contract and d_last on a joint one; because lives_if_last() returns l_1 when the contract is single-life, lives_death_last is d_term in both cases and no second cells is needed.

CF(t) needs one sentence of warning. The notes define CF(t) = E[ANN] + E[CR] + E[COMM] + E[EXP], an outgo-positive total, and that is liability_cf(). But the CF column of the worked-example table is the annuity instalment alone — at t = 1 it shows 500.00, not the 505.00 that the definition gives once the $5.00 monthly maintenance expense is added. The worked example is therefore asserted against annuity_payments(), and net_cf() carries the house sign convention of Term_US_A (income less outgo, so -liability_cf). Both signs are published as columns of result_cf(), the notes’ outgo-positive total under liability_cf and the library-wide income-positive one under net_cf.

The two self-checks follow the library convention: check_lives_roll_fwd() and check_payment_factor() take no argument and return a bool covering every projected month, so one test can call the same no-arg check across every model in the library. The signed per-month residual — the more useful object once a check fails — stays available as check_lives_roll_fwd_resid() and check_payment_factor_resid(), and each boolean is implemented in terms of its own residual.

Two mortality bases: table and scenario

The notes’ worked example is not a probability-weighted run. It is a scenario: “the joint (secondary) annuitant dies during month 14; the primary survives throughout”, evaluated at l_1 = 1, l_2 = 0. The notes’ Model points section, by contrast, projects “on an expected (probability-weighted) basis”. Both readings are shipped, and which one applies is a model point column:

mort_basis = "table"

lives_if runs the notes’ generational recursion off the shipped mortality and improvement tables. Model points 7, 8, 9, 12 and 15; point 8 is the anchor cell on this basis and is the run to read for a realistic cash flow shape.

mort_basis = "scenario" [std]

lives_if(t, life) is the deterministic step function 1{t < death_mth(life)}, with a blank death_mth meaning the life survives the whole projection. Model points 1-6, 10, 11, 13 and 14, which reproduce the worked example and its traces exactly.

The scenario switch is a [std] modelling device, not a product feature; it exists because the notes’ verification anchor is a scenario and retuning assumptions to force a probability-weighted run onto it would be dishonest.

The certain period is a floor, not a second stream

payment_factor(t) = max(certain_floor(t), payment_factor_life(t)) is the single most important line in the model and the notes’ first listed pitfall. During the certain period the full, unreduced instalment is payable regardless of survival; an additive construction would pay 1 + L and silently double the guarantee. The max also reproduces, with no extra logic, NYL’s rule that a survivor reduction inside a certain period is deferred to the end of that period [S5] — see model point 5, where the reduction to delta begins at t = 121 and not at the month-14 death.

certain_only, where the notes’ expense formula outlives the contract

The notes carry the maintenance expense on IF(t) = max(C(t), l_alive(t)) and, in the same table, describe it as paid “while any payment obligation remains”. The two agree on the four life-contingent forms. They do not agree on certain_only, where the notes also set L(t) = 0: the last instalment falls at n_eff and nothing is owed afterwards, but l_alive is a survival probability, not a payment factor, and stays positive for the annuitant’s whole remaining lifetime. Read literally the formula keeps billing a contract that ended years earlier. pols_if() follows the prose and drops the life-contingent leg on certain_only — the one form with no life contingency in it — so IF(t) = C(t) there and max(C(t), l_alive(t)) on every other form. Model point 15 exercises it; no other form and no other model point moves.

Survival-measurement and refund-balance timing

Two timing conventions move the liability by one payment period each and are wired to the same switch, timing:

  • an arrears instalment falling at the end of month t requires survival to the end of month t; an advance instalment falls at the start of month t, which is the end of month t - 1, so survival is measured at t - 1 whatever the frequency. That is payment_surv_mth(). The notes write the advance point as t - 12/m, one full payment period earlier — but that measures from the arrears month of the same instalment, which falls one payment period later than the advance month. is_payment_mth() indexes an advance instalment by the month it falls in (t = 1, 4, 7, at m = 4), so t - 1 is the reading that holds at every frequency; the two agree at m = 12, the only frequency the notes spell out.

  • the cash-refund balance nets instalments already paid: G(t-1) on arrears, but G(t) in an advance payment month, because an instalment paid at the start of the month of death has been paid. That is claim_pp().

Deaths are decremented at end of month, so a survivor reduction takes effect from the instalment due at the end of the month of death — which is why the worked example’s trigger split bites at t = 14 and not at t = 15.

Cells Descriptions#

model_point()[source]#

The selected model point as a Series.

premium_pp()[source]#

P: the single premium of the selected model point.

A pricing input at t = 0, not a projected cash flow: the projection carries no premium income. It enters the projection only through the refund forms, where the guarantee is measured against it.

premium_tax_rate()[source]#

tau: the state premium tax rate deducted before income is determined [std].

Zero in the base run; no source quantifies a rate [S6][S7][S11].

premium_net_pp()[source]#

P_net = P(1 - tau), the amount annuitized [S6][S7][S11].

Reported for completeness. Because B(1) is exogenous - no insurer publishes payout factors, so the model does not derive income from premium - nothing in the projection consumes it.

pols_if_init()[source]#

Initial number of contracts in force; 1.0 on a single-contract model point.

form()[source]#

The payout form: one of the five forms of [S1][S2][S4][S5].

life_only, life_certain, cash_refund, installment_refund, certain_only.

is_joint()[source]#

True when the contract covers two annuitants.

age_at_entry(life)[source]#

x_i: issue age (ANB) of the primary (life = 1) or joint (life = 2) annuitant.

Age nearest birthday [std]: MassMutual defines contract age as ANB [S1] and the 2012 IAM/IAR family is tabulated ANB [R2][R9].

sex(life)[source]#

Sex (M / F) of the primary (life = 1) or joint (life = 2) annuitant.

survivor_pct()[source]#

delta: the survivor percentage, in {0.50, 2/3, 0.75, 1.00} [S1][S2][S5][S6][S7].

reduction_trigger()[source]#

trig: whether the reduction is triggered by either death or the primary’s.

A switch, not a parameter: the two conventions coincide on the primary’s death and differ only on the secondary’s [S1][S2][S3][S7].

certain_mths()[source]#

n: the elected certain period in months, 0 or 60-360 [S1][S2][S4][S5].

payment_freq()[source]#

m: payments per year, in {12, 4, 2, 1} [S1][S2].

payment_timing()[source]#

Whether instalments fall in arrears (the [std] default) or in advance.

No product document states the convention; VM-V’s prescribed weight-table cash flow model assumes payments at the end of each period, so arrears is the default [R1].

cola_rate()[source]#

g: the fixed compound COLA rate, in {0, .01, .02, .03, .04} [S1][S5].

commutation_enabled()[source]#

Whether the contract carries the certain-portion commutation right [S1].

issue_state_excludes_withdrawal()[source]#

True where the issue state bars the withdrawal feature (Oregon) [S1][S2].

qualified()[source]#

True for qualified money. Informational: the RMD overlay is not implemented.

rating_factor()[source]#

theta: the substandard mortality multiplier [std]; 1.0 in the base run.

q_rated = min(1, theta * q_be), the reference model’s carrier for anti-selection at outset and for rated ages [S8][R1][REG-R41].

annuity_year()[source]#

The calendar year of the annuity date [std]; the generational anchor.

The improvement exponent is calendar_year(t) - mort_table_year, so this is what makes the mortality basis generational rather than period.

mort_basis()[source]#

Whether the run is probability-weighted (table) or deterministic (scenario).

table runs the notes’ generational recursion off the shipped tables; scenario [std] replaces it with the step function 1{t < death_mth(life)} so the notes’ worked example - which is a scenario, not an expectation - reproduces exactly. See the Space docstring.

death_mth(life)[source]#

The scenario month of death of life; 0 if the life survives throughout.

Read only when mort_basis() == "scenario". A blank cell in the model point table means the life never dies in the scenario and is returned as 0, since t is 1-based and a death in month 0 is not a projectable event. A death “during month 14” is decremented at the end of month 14, so lives_if(t, life) is 0 from t = 14.

duration_mth(t)[source]#

Months elapsed from the annuity date at the end of month t; equal to t.

t is 1-based here, unlike CashValue_SE’s 0-based months, so the identity is trivial - the cells exists so the monthly models share one vocabulary.

duration(t)[source]#

Completed policy years at the start of month t: (t - 1) // 12.

policy_year(t)[source]#

y(t) = ceil(t/12): the policy year containing month t; 1 for t = 1..12.

calendar_year(t)[source]#

The calendar year of the policy year containing month t.

age(t, life)[source]#

The attained age (ANB) of life in the policy year containing month t.

horizon_mths()[source]#

Last month at which some covered life has not yet passed the limiting age.

12*(omega_age - min x_i): the notes stop once t/12 + x_i > omega for every covered life, and stopping on the primary’s age alone would truncate a younger joint annuitant’s tail. It is not the month in which the youngest life attains the limiting age, which is one month later - see proj_len().

proj_len()[source]#

Projection length in months: the mortality horizon, or the certain period if longer.

horizon_mths() implements the notes’ age stop rule literally - stop once t/12 + x_i > omega for every covered life - so the last projected month is 12*(omega_age - min x_i). Under the ANB age convention, where age() advances on each policy anniversary, the youngest life’s attained age in that month is omega_age - 1: the projection stops one month before any life attains omega_age, and the q = 1 row of the shipped tables is never reached inside it.

The notes’ other stop test, IF(t) < 1e-6, is therefore not subsumed - on model point 8 the projection ends at pols_if(696) = 3.41e-06, above that threshold, and a test pins the figure. The age rule is the one implemented, because it keeps proj_len() independent of the projection it bounds; what it truncates is a tail of 3.41e-06 of a contract, and one further month would take it to zero.

is_payment_mth(t)[source]#

Whether month t is a payment date.

Arrears: T = {12k/m}, so t = 3, 6, 9, … at m = 4. Advance: the k-th instalment falls one full payment period earlier, at the start of month 12(k-1)/m + 1, so t = 1, 4, 7, … At m = 12 every month is a payment month on either convention.

payment_surv_mth(t)[source]#

The month at which survival is measured for the instalment falling in month t.

Arrears: the end of month t. Advance: the end of month t - 1, because an advance instalment falls at the start of month t, whatever the frequency; 0 for the first instalment, where lives_if is 1. Using end-of-period survival for advance payments understates the liability by about one period’s mortality per payment.

The notes put the advance point at t - 12/m, “one full payment period earlier”. That measures from the arrears month of the same instalment, which falls one payment period after the advance month; is_payment_mth() indexes an advance instalment by the month it falls in, so the two readings coincide only at m = 12. At m = 4 the second instalment falls at the start of month 4 and requires survival to the end of month 3 - not to the end of month 1, which would pay it to a life the projection has already recorded as dead.

annual_income(y)[source]#

B(y): the unreduced annualized income in policy year y.

B(y) = B(y-1)(1 + g) on each anniversary of the annuity date [S1][S4], compound and irrevocable. Escalation applies to the unreduced level and continues after a survivor reduction, because the contract reduces payments to delta of the current income payment [S2]. NYL instead starts the first increase one year after the first income payment [S5]; that one-period difference is a [std] convention choice not taken here. B(1) is exogenous - no insurer publishes payout factors, so the model does not derive it from the premium.

annuity_pp_sched(t)[source]#

The scheduled unreduced instalment per contract in month t, before any trim.

inst(t) = B(y(t))/m on payment dates and 0 elsewhere.

cum_annuity_pp(t)[source]#

G(t): cumulative scheduled instalments per contract through month t.

A deterministic as-if-alive schedule: instalments payable while any covered life is alive follow the deterministic escalation path, so the refund balance needs no path simulation. On a joint contract with a survivor reduction the cash refund is offered only at delta = 100% [S5], so the unreduced schedule is the paid schedule.

certain_mths_refund()[source]#

n_R: the certain period derived by an installment refund [S1][S4][S5][S6].

n_R = min{t in T : G(t) >= P} - payments continue until cumulative payments equal the premium. Under a level path this closes to (12/m) * ceil(m*P/B(1)), which is NYL’s published rule “guaranteed payment period = premium paid / annualized income benefit amount” rounded up to a payment date [S5]; the anchor check is P/B(1) = 100,000/6,000 = 16.667 years = 200 months. Searched rather than closed so a COLA path (which MassMutual does not offer with this form [S1]) still resolves. Capped at horizon_mths() if the premium is never recovered.

certain_mths_eff()[source]#

n_eff: the effective certain period in months, by form.

n for life_certain and certain_only [S1][S2][S4][S5]; n_R for installment_refund [S1][S5]; 0 for life_only and cash_refund [S1][S2].

annuity_pp(t)[source]#

inst(t): the instalment per contract actually payable in month t.

Equal to annuity_pp_sched() except on installment_refund, where the notes trim the final instalment at n_R to P - G(n_R - 12/m) [std] so cumulative payments land exactly on the premium. The trim is a no-op whenever P is an exact multiple of the instalment, which is the shipped anchor’s case.

mort_rate_base(t, life)[source]#

The base-table annual mortality rate for life in the policy year containing t.

The shipped table is an illustrative [std] annuitant table, not the 2012 IAM Basic table the notes prescribe: licensed tables may not be embedded, and the Basic table is in any case not printed in any source this library holds. Rates at and above the limiting age are 1.

improvement_rate(t, life)[source]#

G2_x: the generational improvement rate for life in the year containing t.

The shipped scale is an illustrative [std] stand-in for Projection Scale G2. Scaling this table is the notes’ first-listed sensitivity - Scale G2 is fixed and dated, and 2020-2024 experience says it has over-projected [R9].

mort_rate(t, life)[source]#

q_rated: the annual mortality rate applied to life in the year containing t.

The notes’ construction, in order:

q_base(x, 2012+k) = q_x(table) * (1 - G2_x)^k
q_be              = min(1, AE * q_base),      AE = 1.084 **[std]**
q_rated           = min(1, theta * q_be),     theta = 1 in base

k = calendar_year(t) - mort_table_year, so the basis is generational: each attained age in each future calendar year uses its own improved rate. The A/E factor belongs on the projected basis, matching the study’s measurement convention - on the unprojected table the study’s own answer is 99.6% [R9], and applying 1.084 there misstates the mortality level by about 8%.

mort_rate_mth(t, life)[source]#

q_m(t) = 1 - (1 - q_rated)^(1/12): the monthly mortality rate [std].

lives_if(t, life)[source]#

l_i(t): the probability that life is alive at the end of month t.

l_i(0) = 1. On the table basis l_i(t) = l_i(t-1)(1 - q_m(t)), deaths being decremented at end of month. On the scenario basis [std] the survival path is the step function 1{t < death_mth(life)}, with death_mth = 0 meaning the life survives the whole projection - which is what the notes’ worked example specifies. Returns 0 for life = 2 on a single-life contract.

lives_death(t, life)[source]#

d_i(t) = l_i(t-1) - l_i(t): the death density of life in month t.

lives_if_last(t)[source]#

l_last(t): the probability that at least one covered life is alive at end of t.

l_1 + l_2 - l_1*l_2 on a joint contract, under joint-life independence [std] - the SOA/LIMRA payout study is explicit that its data cannot inform the assumption, since it gives no recognition to a living secondary annuitant [R9]. On a single-life contract this is l_1, which is why no separate l_alive cells is needed.

lives_death_last(t)[source]#

d_last(t) = l_last(t-1) - l_last(t): the last-death density in month t.

This is also the notes’ d_term(t), the death that terminates the income stream: d_1 on a single-life contract and d_last on a joint one, which lives_if_last() already collapses into one expression.

certain_floor(t)[source]#

C(t) = 1{t <= n_eff}: the annuity-certain floor indicator.

payment_factor_life(t)[source]#

L(t): the life-contingent payment factor for the instalment falling in month t.

Survival is measured at payment_surv_mth(), not at t. By form and trigger:

single life:           L = l_1
joint, trig = either:  L = l_1*l_2 + delta*(l_1 + l_2 - 2*l_1*l_2)
joint, trig = primary: L = l_1 + delta*(1 - l_1)*l_2
certain_only:          L = 0

The either form pays the full instalment while both are alive and delta while exactly one is - the second bracket is P(at least one alive) - P(both alive) [S2][S3]. The primary form pays in full while the primary lives whatever the joint annuitant’s status, and delta only when the primary is dead and the joint annuitant alive [S2][S3][S5]. The two coincide on the primary’s death and differ only on the secondary’s, which is why the trigger is a switch and not a footnote.

payment_factor(t)[source]#

Phi(t) = max(C(t), L(t)): the master payment factor.

The max makes the certain period an annuity-certain floor rather than an additional stream: during the certain period the full unreduced instalment is paid regardless of survival, and the max prevents paying 1 + L. An additive construction silently doubles the guarantee - the notes’ first-listed pitfall. Because the floor pays the unreduced instalment, it also reproduces NYL’s rule that a survivor reduction inside a certain period is deferred to the end of that period, with no extra flag [S5].

pols_if(t)[source]#

IF(t): contracts with a payment obligation still open at month t.

pols_if_init * max(C(t), l_alive(t)) [std] - the in-force measure the notes use to carry the maintenance expense, which runs while any payment obligation remains, whether that obligation is life-contingent or certain.

One divergence from the notes’ literal formula, on certain_only only. That form has L(t) = 0, so the last instalment falls at n_eff and nothing is owed after it; but l_alive is a survival probability rather than a payment factor and stays positive for the annuitant’s remaining lifetime, so the unqualified max would keep accruing the maintenance expense on a contract that has ended - contradicting the same table’s “while any payment obligation remains”. The life-contingent leg is therefore dropped on certain_only, the one form with no life contingency in it, and IF(t) = C(t) there. Every other form is unaffected. See model point 15.

commute_util_rate(y)[source]#

u(y): the deterministic commutation utilization in contract year y [std].

Zero in the base run, so the payment engine is exercised in isolation: no public data on SPIA commutation take-up exists. The notes’ rate-driven alternative u(y, t) = min(u_max, u_base(y) * max(0, 1 + kappa*[CMT10(0) - CMT10(t)])) is not implemented - it is a pure shape assumption calibrated to nothing. Both constructions are best-estimate objects and are barred from a CARVM run: commutation is an elective benefit under AG 33, whose incidence rates must be maximised over rather than assumed [REG-R151].

commute_elig(t)[source]#

Whether a commutation may be taken in month t [S1].

All of: the contract carries the right; the issue state is not Oregon; the form has a certain period; the contract year is 2 or later; and this is the first month of that contract year, which enforces “one withdrawal per contract year”.

commute_disc_rate(t)[source]#

j(t): the commutation discount rate [std] [unverified].

j(t) = j0 + [CMT10(t) - CMT10(0)] with j0 = 4.00%. No fixed SPIA issuer publishes a commutation discount formula: MassMutual gives only the cap [S1], Pacific Life only “an interest-rate adjustment will apply” [S2], NYL only the 10-year CMT as the driver [S5]; the 4% comes from TIAA-CREF Life’s variable contract [S7]. cmt10_shift is a flat scalar here - a CMT path would be another input table - so the rate is level. Any run with commutation enabled inherits an unsupported assumption; flag it in output.

commuted_value(t)[source]#

CV(t): the commuted value of the remaining certain instalments [S1].

CV(t) = sum over s in T, t < s <= n_eff of inst(s) * (1 - theta_cum(t)) * v(t, s) with, per the commute_disc_convention Reference:

compound (default **[std]**):  v = (1 + j)^(-(s-t)/12)
simple   (per [S7]):           v = max(0, 1 - j*(s-t)/12)

Only the certain tail is discounted; the life-contingent payments after the certain period are untouched by a commutation [S1][S2][S5].

commute_amount(t)[source]#

W: the gross withdrawal taken in month t [S1]; zero in the base run.

W = u(y) * CV(t), then constrained by the published limits: at least $5,000, at most CV(t), and leaving every remaining guaranteed payment at or above $100. The residual floor is applied as a cap on W rather than a rejection, so a large requested utilization degrades to the largest admissible withdrawal; if that is below the $5,000 minimum, nothing is withdrawn.

commute_frac_cum(t)[source]#

theta_cum(t): the cumulative commutation fraction applying in month t.

Measured before any withdrawal taken in month t, because the notes’ processing order records the instalment (step 3) before the commutation (step 5):

theta_cum(t+) = theta_cum(t) + (1 - theta_cum(t)) * W / CV(t)

This is NYL’s pro-rata rule: future income payments through the end of the guaranteed period are reduced by the withdrawal percentage elected, with full payments resuming for life at the end of that period [S5][S2]. It applies to certain-period instalments only - applying it to the life-contingent tail contradicts every retrieved contract [S1][S2][S5], which is why annuity_payments() multiplies it by C(t).

surr_charge_rate(y)[source]#

sc(y): the surrender charge on a commutation in contract year y [S1].

8% in year 2 grading to 1% in year 9 and 0% from year 10 - the only published SPIA surrender-charge schedule found. Contract years below and above the table’s range take its first and last rows; year 1 is barred from withdrawing at all, so its rate is never reached through commute_amount().

surr_charge(t)[source]#

The surrender charge retained by the insurer on a commutation in month t [S1].

inflation_factor(t)[source]#

The expense inflation factor in month t: (1 + pi)^(y(t) - 1) [std].

annuity_pp_paid(t)[source]#

The instalment per contract after any prior commutation, before survival weighting.

inst(t) * (1 - theta_cum(t)*C(t)): a commutation reduces certain-period instalments only.

annuity_payments(t)[source]#

E[ANN(t)]: expected annuity outgo in month t.

inst(t) * Phi(t) * (1 - theta_cum(t)*C(t)), scaled by pols_if_init. This is the column the notes’ worked-example table labels “CF”: at t = 1 it is 500.00, the instalment alone, with the maintenance expense carried separately in liability_cf().

claim_pp(t, kind)[source]#

The claim amount per contract in month t, by kind.

"REFUND" - the cash refund lump sum max(0, P - G), zero on any form other than cash_refund [S1][S3][S5]. The balance is measured at G(t-1) on arrears, implementing “instalments already paid” for a mid-month death; in an advance payment month the instalment paid at the start of the death month has been paid, so G(t) is used or the lump sum is overstated by one instalment.

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

Expected claim outgo in month t; kind=None totals every kind.

The only kind on this product is "REFUND". It is weighted by lives_death_last(), the notes’ d_term - the death that terminates the income stream, which is the primary’s on a single-life contract and the last death on a joint one (a cash refund on a joint contract is offered only at delta = 100% [S5]).

commutations(t)[source]#

E[COMM(t)]: the commutation payment made to the owner in month t [S1].

W * (1 - sc(y)) - the withdrawal net of its surrender charge. Zero in the base run, where utilization is zero.

expenses(t)[source]#

E[EXP(t)]: maintenance expense in month t [std].

(c_e / 12) * (1 + pi)^(y(t)-1) * IF(t): $60 per contract p.a. inflating at 2.5%, paid monthly while any payment obligation remains. No insurer publishes expense assumptions; MassMutual’s “zero fees” [S1] refers to charges to the policyholder, not to the insurer’s cost. Acquisition cost is out of scope - the premium is single and the cost is priced in.

liability_cf(t)[source]#

CF(t): total gross liability outgo in month t, the notes’ cash flow definition.

E[ANN] + E[CR] + E[COMM] + E[EXP]. Outgo positive, the notes’ own sign, and the sum of the cash flow columns of result_cf(). There is no premium income in the projection - the single premium at t = 0 is a pricing input - and no surrender outgo, because the contract has no cash value at any time [S1][S4][S5]. Note that the worked-example table’s “CF” column is annuity_payments(), not this.

Both signs are published, here and in net_cf(), so that the notes’ stream survives verbatim under the name the notes give it while the library-wide income-positive convention is available under the name every other model uses.

net_cf(t)[source]#

Net cash flow to the insurer in month t: income less outgo, so -liability_cf.

Income positive, the sign convention of Term_US_A.net_cf, kept even though this product has no projected income so that every model’s net_cf can be compared or summed across the library. liability_cf() carries the opposite, outgo-positive sign of the technical notes; both are published as columns of result_cf() rather than one being made to stand for the other.

check_lives_roll_fwd_resid(t)[source]#

Residual between lives_if() and an independently rebuilt survival path.

Deliberately not the telescoping identity l_i(t-1) - d_i(t) - l_i(t): lives_death() is defined as that difference, so the identity is identically zero whatever lives_if() returns and constrains nothing. Each life’s survival is rebuilt here from the assumptions instead, with no reference to the recursion - on the table basis as the running product prod_{s=1..t} (1 - q_m(s, i)), which a misindexed or mis-based recursion breaks - and the residual is the sum of the per-life differences.

On the scenario basis lives_if is closed form, so there is no recursion to close and that leg only restates the step function; the substantive check is the table one. On a joint contract the last-survivor identity l_last(t) - [l_1 + l_2 - l_1*l_2] is added. That term is zero by construction, like check_payment_factor_resid(), and exists to catch a future redefinition of lives_if_last() that broke the independence construction.

check_lives_roll_fwd()[source]#

Whether the survival recursion closes at every projected month.

Takes no argument and returns a bool, the library-wide shape of a check_* cells, so one test can call the same check across every model. It is all(abs(check_lives_roll_fwd_resid(t)) < 1e-10) over t = 1 ... proj_len(); the signed residual of a failing month is the more useful object when it does fail, and stays available as check_lives_roll_fwd_resid().

The tolerance is absolute, not relative: the residual is a difference of survival probabilities that is zero when the model is right, and math.isclose(x, 0.0) is False for every nonzero x at the default relative tolerance.

check_payment_factor_resid(t)[source]#

Phi(t) - max(C(t), L(t)): the certain-period double-count guard.

Zero by construction. It is asserted anyway because an additive floor - C + L instead of max(C, L) - is the notes’ first-listed pitfall and would show up here as min(C, L).

check_payment_factor()[source]#

Whether the master payment factor is the certain floor at every projected month.

No argument, returns a bool, implemented as all(abs(check_payment_factor_resid(t)) < 1e-10) over t = 1 ... proj_len(). See check_payment_factor_resid() for what the residual catches and why a quantity that is zero by construction is checked at all.

result_cf()[source]#

Result table of cashflows, indexed by month t.

result_pols()[source]#

Result table of survival probabilities and payment factors, indexed by month t.