Traditional Life Pricing Price traditional life products (term, whole life, universal life) for new business.
Pricing harness for traditional life insurance products. Distinct from annuity suite by methodology (premium-driven vs. account-value mechanics).
Inputs, processing, outputs
Methodology
M-071 is the traditional life pricing model (TraditionalLifeModel, engine id
insmodel.L4.traditional_life, internal P-006). A single engine prices both
Term Life and Whole Life off one parameter, product_subtype, because the two
share an intended use — new-business premium adequacy and profit testing for
mortality-driven protection products — and differ only in cash-value mechanics. It
composes five engines declared in get_required_engines(): MortalityEngine (death
cost), LapseEngine (surrender/lapse decrement), InvestmentEngine (portfolio yield on
reserves), ExpenseEngine (operating cost), and ReserveEngine (GAAP FPB reserve).
calculate() projects a cohort monthly. For each month it derives the attained age
and policy year, then:
- Mortality. Annual
qxis built from a base rate, an age slope, and a multiplicative experience factor (_get_mortality_rate()), clipped to[0.0001, 1.0], then converted to a monthly decrement1 − (1 − qx)^(1/12). Deaths drivedeath_benefit = deaths · face. - Lapse. Annual lapse comes from a duration-graded base curve (
_get_lapse_rate()). Term carries a shock lapse at the end of the level term (the renewal cliff) and an elevated post-term lapse (3× base, capped at 25%); Whole Life lapse is halved (cash-value retention). Converted to a monthly decrement. - Premium.
premium_inflow = in_force · annual_premium / 12— level premium, the pricing lever the model solves around. - Investment income.
reserve · monthly_yield, wheremonthly_yield = (1 + portfolio_yield)^(1/12) − 1. - Whole-Life-only cash mechanics. Cash surrender value (
_get_cash_surrender_value(), linear growth fromcsv_start_year, capped at 95% of face) drives surrender outflow, policy-loan income (loan_balance · loan_rate / 12), and dividends (csv · dividend_rate / 12). Term sets all of these to zero. - Expense. Per-policy maintenance (annualised /12) plus a percent-of-premium load.
- Net income and reserve roll-forward.
net_income = premium + investment_income + loan_income − death_benefit − surrender − dividend − expense; the reserve rolls forward by the same flow and is floored at zero. DAC is set up at issue asin_force · annual_premium · dac_rateand amortised straight-line overdac_amortization_years.
In-force is decremented each month by deaths + lapses; projection stops at the term
horizon (Term: term_years + 2) or when in-force falls below 1.
Profit testing (compute_profitability()) re-runs calculate() on a fresh pricing
cohort and computes: PVFP (monthly net income discounted at the risk-adjusted rate),
profit margin (PVFP / initial annual premium volume), an IRR approximation
(total_profit / premium_volume / years — not a true cash-flow IRR solve),
return on capital (PVFP over a capital charge of 3% of face for Term, 5% for Whole),
breakeven year (first month cumulative net income turns positive), and loss ratio
(total death benefits / total premiums). get_cash_flows() re-runs the projection across
multiple interest-rate scenarios by averaging each scenario's yield.
Key Assumptions and Their Justification
The load-bearing assumptions for the canonical Term run. All are engine parameters with
defaults in PRODUCT_CONTRACT["parameters"]; the snapshot uses the values asserted by
tests/test_traditional_life_model.py::make_model.
| Param | Name | Canonical value | Derivation | Justification |
|---|---|---|---|---|
base_mortality_rate |
Base annual qx at issue | 0.002 | published_source | Issue-age-40 male qx proxy; CSO-table order of magnitude. The model does not load a full CSO table — it uses base + slope (see Limitations). |
mortality_age_slope |
qx increase per year of age | 0.00015 | assumption | Linear age grading; a simplification of the convex CSO age curve. |
mortality_experience_factor |
Experience multiplier on base qx | 0.80 | data_calibrated | Underwriting selection: insured lives run below table; ASOP 25 credibility framing. |
base_lapse_curve |
Annual lapse by policy year | 8% Y1 → 2% Y10+ | data_calibrated | Duration-graded decay consistent with VM-20 §6 guardrails; early-duration lapse highest. |
shock_lapse_rate |
Lapse at end of level term | 30% | data_calibrated | The renewal cliff: premiums jump at term end, triggering mass lapse. First-order Term pricing risk. |
portfolio_yield |
General-account yield on reserves | 5% | market | Earned rate on backing assets; feeds investment income. |
annual_premium |
Level gross premium | $1,200 (Term) | product_design | The pricing lever; profit testing measures adequacy of this premium against mortality + expense + margin. |
expense_per_policy / expense_pct_premium |
Maintenance + premium-load expense | $75/policy + 8% of premium | data_calibrated | Industry expense-study range; per-policy plus acquisition-style load. |
dac_rate / dac_amortization_years |
DAC setup and amortisation | 50% of FY premium · 10 yr SL | accounting | Deferred acquisition cost; straight-line amortisation is a GAAP simplification (not interest-accreted EGP-based). |
csv_growth_rate / dividend_rate / policy_loan_rate |
Whole-Life cash mechanics | 3% · 2% · 5% | product_design | Engaged only for WHOLE_LIFE; Term zeroes them. |
Operational notes. Mortality and lapse are the two behavioural drivers that move
pricing: mortality sets the benefit cost, lapse sets how long premium and reserve persist.
The crediting/cash-value parameters are product-design inputs (contractual), not
calibrated experience. The reserve is not a statutory or VM-20 reserve — it is a
simplified roll-forward seeded at in_force · face · 0.01 · duration and is a pricing
artefact, not a held reserve (M-001 owns VM-20 statutory reserves).
Output Snapshot
Deterministic single-cohort run of TraditionalLifeModel (P-006) in its TERM
configuration — reproducible, requires no live firm data (python scripts/model_snapshots.py
M-071 in InsModel; mechanics asserted by tests/test_traditional_life_model.py, 32 tests
passing). The provider is a MagicMock.
Input: issue age 40 · Male · face $500,000 · level premium $1,200/yr · 20-yr term ·
base qx 0.002 · experience factor 0.80 · base lapse 8% Y1 → 2% · shock lapse 30% ·
portfolio yield 5% · 10,000 in-force · profit test at discount 6% over 240 months. Cash-flow
rows shown are month 1; pricing metrics are from compute_profitability().
| output | value | meaning |
|---|---|---|
| product_subtype | TERM | pure protection, no cash value |
| gross_annual_premium | 1,200.00 | level premium per policy (the pricing lever) |
| month1_premium_inflow | 1,000,000.00 | 10,000 in-force · $1,200 / 12 |
| month1_death_benefit | 717,232.26 | deaths · $500,000 face, month 1 |
| month1_investment_income | 203,706.19 | yield on the (seeded) reserve |
| month1_expense | 142,500.00 | per-policy maintenance + 8% premium load |
| month1_net_income | 343,973.93 | premium + NII − benefits − expense |
| driver_mortality_annual_qx | 0.00172 | annual qx at attained age 41 (base+slope)·0.80 |
| driver_lapse_annual_y1 | 0.0800 | year-1 annual lapse from the base curve |
| pvfp_at6pct | 19,327,930.17 | present value of future profit, 240mo @ 6% |
| profit_margin | 1.61 | PVFP / initial annual premium volume |
| irr_approx | 0.09 | approximate IRR (not a true cash-flow solve) |
| return_on_capital | 0.13 | PVFP / (3% of face · in-force) capital charge |
| breakeven_year | 1 | cumulative net income turns positive in year 1 |
| loss_ratio | 1.14 | total death benefits / total premiums |
The two pricing drivers are visible directly: annual qx of 0.00172 (the benefit cost) and
year-1 lapse of 8% (the persistency assumption). The PVFP and margin are pricing diagnostics
from compute_profitability(), not statutory profit. Two numbers deserve honest reading.
First, loss ratio 1.14 (>1): total death benefits exceed total premiums over the horizon,
which on a standalone premium basis would be inadequate — yet net income is positive because
the model credits investment income on a large seeded reserve ($50M at issue =
10,000 · 500,000 · 0.01 · 1). Second, profit margin 1.61 is implausibly high for term
pricing and is a direct artefact of that reserve seeding inflating investment income relative
to a deliberately low illustrative premium. These figures are internally consistent engine
outputs on the documented canonical inputs; they are not calibrated to a real product's
priced premium and should be read as mechanics demonstrations, not a profit claim. See
Limitations 1, 3, and 5.
Captured 2026-06-04 · deterministic, no live data.
Limitations and Known Gaps
-
Reserve is a pricing artefact, not a statutory reserve. The reserve is seeded at
in_force · face · 0.01 · durationand rolled forward by net cash flow, floored at zero. It is neither a VM-20 net-premium/deterministic reserve (M-001) nor a GAAP FPB reserve (M-020), despite ReserveEngine being a declared dependency. The seeded magnitude drives investment income and therefore inflates PVFP and profit margin — the snapshot's margin of 1.61 is a direct consequence and must not be read as a profit claim. -
Mortality is base-plus-linear-slope, not a CSO table.
_get_mortality_rate()applies(base_rate + age·slope)·experiencewith a single linear slope, clipped to[0.0001, 1.0]. It does not load the 2017 CSO select-and-ultimate structure, has no gender split beyond carrying agenderparameter that does not alter qx, and no smoker/non-smoker distinction. Acceptable as a pricing illustration; insufficient for a priced mortality basis. -
Premium is an input, not solved. The model profit-tests a given level premium; it does not solve for the premium that hits a target margin or IRR. "Premium adequacy" is assessed only by inspecting the resulting loss ratio / PVFP. The canonical $1,200 premium on $500K face produces loss ratio > 1, illustrating exactly this gap — the harness reports inadequacy rather than correcting it.
-
Profitability metrics are approximations. IRR is
total_profit / premium_volume / years, not a cash-flow IRR solve; return-on-capital uses a flat factor (3% face Term / 5% Whole) rather than a risk-based capital model; breakeven is read off cumulative undiscounted net income. Directional, not decision-grade. -
Deterministic, single-path projection. No stochastic mortality, no dynamic (moneyness/rate-dependent) lapse, no stochastic interest.
get_cash_flows()only averages each scenario's yield to a scalar — it does not run a genuine multi-path projection. -
DAC is straight-line, not EGP-amortised. DAC amortises on a fixed straight-line schedule rather than in proportion to estimated gross profits/margins, diverging from LDTI/ ASC 944 acquisition-cost mechanics.
-
Whole-Life cash mechanics are linear approximations. CSV grows linearly from
csv_start_year(capped at 95% of face); dividends and policy-loan income are flat-rate functions of CSV. No guaranteed nonforfeiture-law minimum test, no dividend-scale interest crediting, no policy-loan utilisation dynamics. -
No live firm-data path (BV-032). All figures derive from documented canonical inputs and a
MagicMockprovider. Nothing here is a 10-K-reported or experience-calibrated value; the PRU calibration notes in the source docstring are illustrative targets, not validated outputs.
Tracked for ratification. Remediation status of the items noted in earlier passes: the tier-2 sensitivity suite on the top-3 load-bearing assumptions (mortality experience/base+slope, base lapse curve, shock lapse rate) is now DONE (
tests/test_traditional_life_sensitivity.py, 13 tests; COND-071-02). The registry-vs-code engine-binding divergence is RESOLVED (COND-071-01; see Components). Still open: a Whole-Life-archetype deterministic snapshot (only TERM is snapshotted today); premium-adequacy / margin calibration (the engine profit-tests a supplied premium but has no premium-solver, and the canonical inputs produce loss ratio 1.14 / margin 1.61 inflated by a seeded reserve); the reserve-seeding pricing artefact (in_force·face·0.01·duration) that inflates investment income and PVFP; and the mortality basis itself (base+linear-slope clipped, not a CSO select-and-ultimate table; gender carried but inert; no smoker split;get_cash_flows()averages each scenario's yield to a scalar rather than running multi-path projection). None of these were changed in this pass.
Validation Packet
| evidence | status | reference |
|---|---|---|
| Unit/behaviour test suite | present | tests/test_traditional_life_model.py — 32 tests passing (mortality, lapse decrement, shock lapse at renewal, CSV monotonicity, dividends-after-CSV, policy-loan income, premium inflow, DAC amortisation, profitability, termsheet Term vs Whole, multi-scenario). |
| Deterministic snapshot | present | scripts/model_snapshots.py M-071 (TERM configuration, this card). |
| No-lapse boundary invariant | present | test_no_lapse_boundary — zero lapse ⇒ surrender outflow 0, in-force declines from mortality only. |
| Shock-lapse invariant | present | test_shock_lapse_at_renewal — lapse at term_years equals the shock rate, exceeds normal-year lapse. |
| Whole-Life CSV monotonicity | present | test_whole_life_csv_monotonic — CSV non-decreasing in duration. |
| SR 11-7 governance check | present | test_governance_compliance — check_product_compliance(P-006) passes; test_model_card_generation emits required sections. |
| Validation-evidence pack | present | modelling/validation_evidence/M-071/v1.0.0/README.md — Tier-2 scoped pack indexing conceptual soundness, behaviour suite, the COND-071-02 sensitivity suite, and the deterministic snapshot, each pointing at a reproducible source. |
| Whole-Life-archetype snapshot | absent | Only the TERM archetype is snapshotted; Whole Life is exercised by tests but not independently snapshotted here. |
| Independent challenge (2L) | pending re-review | Peer-permitted for tier-2. COND-071-01 (binding reconciled) and COND-071-02 (sensitivity suite) are remediated; the independent-challenge sign-off is 2L's to record on re-review (registry lifecycle.peer_review.status stays pending until 2L supersedes RAT-071-v1.0.0 with v1.0.1). |
| Sensitivity suite | present | Top-3 load-bearing assumptions sensitivity-tested: tests/test_traditional_life_sensitivity.py — 13 tests (mortality base+slope+experience, base lapse curve, shock lapse), shock-direction + bounded-magnitude, modelled on InsModel test_vm20_sensitivity.py. Indexed in modelling/validation_evidence/M-071/v1.0.0/README.md §3 (closes COND-071-02). |
| Back-test | pending | No realized-outcome reconciliation — BV-032 means no live-data path; needs a firm-calibrated outcome path before a backtest record can be opened. |
| Next revalidation | scheduled | 2028-05-01 (biennial, tier-2 — registry lifecycle.next_revalidation_due). last_validated_on: null (not yet validated). |
| Premium-adequacy / margin calibration | absent | No premium solver and no calibration of the canonical premium to a priced product (Limitations 1, 3). |
References
Engine source:
- ecosystem/InsModel/Models/firmmodel/products/traditional_life_model.py —
TraditionalLifeModel (P-006), engine id insmodel.L4.traditional_life.
- firmmodel/products/base_product.py — shared BaseProductModel contract
(calculate, compute_profitability, get_termsheet, validate_input).
- firmmodel/governance/legacy_metadata/TraditionalLifeModel.yaml — extracted component
metadata.
Tests / snapshot:
- tests/test_traditional_life_model.py (32 tests).
- scripts/model_snapshots.py M-071.
Pricing / profit-testing literature: - ASOP No. 56 — Modeling (intended use, sensitivity, reliance, documentation). - ASOP No. 25 — Credibility Procedures (experience-factor application to base mortality). - ASOP No. 2 — Nonforfeiture and Policyholder Dividends (Whole-Life CSV / dividend context). - Atkinson & Dallas, Life Insurance Products and Finance (SOA) — asset-share / profit-testing methodology and PVFP construction for traditional life pricing. - Black & Skipper, Life Insurance — term vs whole-life premium structure, the renewal shock-lapse cliff, and persistency in pricing. - SOA 2017 Commissioners Standard Ordinary (CSO) Mortality Tables — the published mortality basis the model's base+slope approximates (not loaded; see Limitation 2).
Internal:
- M-001 — VM-20 statutory reserves (engine-card depth exemplar; owns the held reserve this
model only approximates).
- M-070 — annuity pricing suite (sibling pricing card; shared BaseProductModel harness).
- BV-032 — firm-data divergence (no live-data path).
Change Log
Card change history. Code-side change history lives in git log of the component files.
- 2026-05-08 — stub created from registry data per Decision 023 Phase 5 / B-07.
- 2026-06-04 — hand-authored pass: Methodology, Key Assumptions, Output Snapshot,
Limitations, Validation Packet, and References authored from
traditional_life_model.py+TraditionalLifeModel.yamllegacy_metadata + bound A-NNN entries. Stub marker advanced to ``. - 2026-06-06 — code-grounded documentation-accuracy pass: added Standards Coverage
(CSO_2017 / ASOP_56 / ASOP_25 / ASC_944-40-LDTI, pointers only) and Dependencies
(upstream models: none — registry declares no
upstream_models) sections to match the M-001 section shape; reconciled the Components processing-engine roster toget_required_engines()(Mortality/Lapse/Investment/Expense/Reserve —benefit_engineis registry-bound but not returned by the code) with a registry-vs-code divergence note; added honest Validation Packet governance-status rows (2L challenge pending, sensitivity suite pending, back-test pending, next revalidation 2028-05-01) from registry lifecycle; added a tracked-for-ratification note under Limitations. No model outputs, back-test numbers, or validation results were fabricated or changed. - 2026-06-06 — RAT-071-v1.0.0 remediation pass (1L, Decision 053 charter §2.2):
COND-071-01 (binding) RESOLVED —
model_registry.yamlcomponents.processing.enginesaligned toget_required_engines(): droppedbenefit_engine(bound-but-unused), addedinvestment_engine,expense_engine,reserve_engine_ldti(exercised-but-unbound);engine_registry.yamlmodel_membershiplists made bidirectionally consistent; binding manifest regenerated;check_registry_integrityOK. COND-071-02 (sensitivity) DONE — authoredtests/test_traditional_life_sensitivity.py(13 tests, shock-direction + bounded- magnitude on mortality / base lapse / shock lapse) and assembled the Tier-2 validation-evidence pack atmodelling/validation_evidence/M-071/v1.0.0/README.md. Reconcileddocumentation_packflags to disk reality (card/validation/change_log nowpresent). Re-stamped doc-currency (fingerprinteb48b20cf61fbe10, current). COND-071-03 (independent sign-off) is left for 2L on re-review (1L may not record its own sign-off). The BV-032 back-test remains an honest pending item. No model outputs or validation results were fabricated.
Open findings (1)
Independent 2nd-line review (INV-2026-06) — implemented capability vs registered scope. Each carries a recommended fix and is tracked in insightalm-mrm until closed.
Validation evidence + change logs missing across most of the inventory
Only M-001/M-020/M-050 carried full documentation packs before this pass. Most models record validation_evidence: missing and change_log: missing with peer_review: pending. Gold tests freeze behaviour but many assert only structural invariants (e.g. reserve>0), not correctness against external truth. The flagship T0-vs-10-K match is circular (BV-032).
Recommendation: For each Tier-1 model: produce a validation-evidence pack (back-test vs disclosed results once BV-032 re-calibration lands, sensitivity suite, challenger comparison), a change log, and a 2L ratification. Sequence behind BV-032 (firm-data) for anything needing 10-K reconciliation.
Per-tier expectations
Per MRM Framework §10.2 + §10.3, this model's regulatory_frameworks tag list activates the following overlays:
| component | tier-2 expectation | status |
|---|---|---|
| Registry entry | required | present |
| Model card (§10.5 doc pack) | required | present |
| Validation evidence | required | present |
| Change log | required | present |
| Independent effective challenge (2L) | required | attested |
Ratified — RAT-071-v1.0.1
Latest ratification on file: RAT-071-v1.0.1. Authored by 2L (mrm-peer-reviewer) per Decision 028 charter §5 Pattern A.