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_ifruns 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 function1{t < death_mth(life)}, with a blankdeath_mthmeaning 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
trequires survival to the end of montht; an advance instalment falls at the start of montht, which is the end of montht - 1, so survival is measured att - 1whatever the frequency. That ispayment_surv_mth(). The notes write the advance point ast - 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, …atm = 4), sot - 1is the reading that holds at every frequency; the two agree atm = 12, the only frequency the notes spell out.the cash-refund balance nets instalments already paid:
G(t-1)on arrears, butG(t)in an advance payment month, because an instalment paid at the start of the month of death has been paid. That isclaim_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#
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.
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].
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.
- 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.
- 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].
- 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].
- 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].
- 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].
- 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, sincetis 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, solives_if(t, life)is 0 fromt = 14.
- duration_mth(t)[source]#
Months elapsed from the annuity date at the end of month t; equal to t.
tis 1-based here, unlikeCashValue_SE’s 0-based months, so the identity is trivial - the cells exists so the monthly models share one vocabulary.
- 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 oncet/12 + x_i > omegafor 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 - seeproj_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 oncet/12 + x_i > omegafor every covered life - so the last projected month is12*(omega_age - min x_i). Under the ANB age convention, whereage()advances on each policy anniversary, the youngest life’s attained age in that month isomega_age - 1: the projection stops one month before any life attainsomega_age, and theq = 1row 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 atpols_if(696) = 3.41e-06, above that threshold, and a test pins the figure. The age rule is the one implemented, because it keepsproj_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 month12(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, wherelives_ifis 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))/mon 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 isP/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 athorizon_mths()if the premium is never recovered.
- certain_mths_eff()[source]#
n_eff: the effective certain period in months, by form.
nfor life_certain and certain_only [S1][S2][S4][S5];n_Rfor 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 atn_RtoP - G(n_R - 12/m)[std] so cumulative payments land exactly on the premium. The trim is a no-op wheneverPis 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
lifein 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
lifein 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
lifein 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
lifeis alive at the end of month t.l_i(0) = 1. On the table basisl_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 function1{t < death_mth(life)}, withdeath_mth = 0meaning 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_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_2on 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 isl_1, which is why no separatel_alivecells 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_1on a single-life contract andd_laston a joint one, whichlives_if_last()already collapses into one expression.
- 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
maxmakes 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 themaxprevents paying1 + 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_onlyonly. That form hasL(t) = 0, so the last instalment falls atn_effand nothing is owed after it; butl_aliveis a survival probability rather than a payment factor and stays positive for the annuitant’s remaining lifetime, so the unqualifiedmaxwould 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 oncertain_only, the one form with no life contingency in it, andIF(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)]withj0 = 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_shiftis 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 thecommute_disc_conventionReference: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 mostCV(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 byC(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].
- 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 bypols_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 inliability_cf().
- claim_pp(t, kind)[source]#
The claim amount per contract in month t, by
kind."REFUND"- the cash refund lump summax(0, P - G), zero on any form other than cash_refund [S1][S3][S5]. The balance is measured atG(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, soG(t)is used or the lump sum is overstated by one instalment.
- claims(t, kind=None)[source]#
Expected claim outgo in month t;
kind=Nonetotals every kind.The only kind on this product is
"REFUND". It is weighted bylives_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 ofresult_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 isannuity_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’snet_cfcan be compared or summed across the library.liability_cf()carries the opposite, outgo-positive sign of the technical notes; both are published as columns ofresult_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 whateverlives_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 productprod_{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_ifis 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 identityl_last(t) - [l_1 + l_2 - l_1*l_2]is added. That term is zero by construction, likecheck_payment_factor_resid(), and exists to catch a future redefinition oflives_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 acheck_*cells, so one test can call the same check across every model. It isall(abs(check_lives_roll_fwd_resid(t)) < 1e-10)overt = 1 ... proj_len(); the signed residual of a failing month is the more useful object when it does fail, and stays available ascheck_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 nonzeroxat 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 + Linstead ofmax(C, L)- is the notes’ first-listed pitfall and would show up here asmin(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 asall(abs(check_payment_factor_resid(t)) < 1e-10)overt = 1 ... proj_len(). Seecheck_payment_factor_resid()for what the residual catches and why a quantity that is zero by construction is checked at all.