P&L Attribution (Source of Earnings) Decompose period-over-period earnings into actuarial sources (mortality variance, lapse variance, investment, expense, etc.) for management reporting.
Source-of-earnings analysis for both statutory and GAAP basis. Decomposes earnings movement into actuarial components for management and external reporting commentary.
Inputs, processing, outputs
Methodology
M-090 implements source-of-earnings (SoE) analysis — also called analysis
of movement (AoM) or P&L attribution — for an insurance block. It answers the
audit-grade question: why did this period's net income differ from what we
expected at the start of the period, and into which economic buckets does the
difference fall? The model is realized by PnLAttributionEngine
(firmmodel/engines/pnl_attribution_engine.py, engine id
insmodel.L4.pnl_attribution_actuarial, v1.0.0).
For each reporting period the engine consumes two aligned frames — actual period results and expected period results (the start-of-period plan / prior valuation roll-forward) — plus an optional dictionary of assumption changes. It then decomposes total net income into the following additive components:
- Expected earnings — net income the block should have produced on
beginning-of-period assumptions:
premium − benefit − expense + investment_income − reserve_change, evaluated on the expected frame (_compute_expected_earnings). - Experience variances (
_compute_experience_variances), each a sign-conventioned actual-vs-expected gap so that a positive number is a gain: - Mortality variance =expected_benefit − actual_benefit(fewer claims than expected → gain). - Lapse variance =(actual_in_force − expected_in_force) × per-policy expected earnings, where per-policy earnings =expected_net_income / expected_in_force. This is a proxy: lapse impact is inferred from the in-force-count deviation, not from a policy-level reserve release. - Expense variance =expected_expense − actual_expense. - Investment variance =actual_investment_income − expected_investment_income. - Reserve-change variance =expected_reserve_change − actual_reserve_change(a smaller-than-expected reserve build is a gain). - Assumption-change impact (
_compute_assumption_change_impact) — the NPV shock from re-setting forward-looking assumptions. For each named change it computeschange_magnitude × in_force_count × |per-policy net income| × annuity_factor / horizon, where the monthly annuity factor usesdiscount_rate(default 4% p.a.) overassumption_npv_horizonmonths (default 120). This is a parametric, single-cohort NPV approximation, not a full re-projection. - New-business contribution (
_compute_new_business_contribution) =new_business_premium × new_business_margin(default margin 5%). - Hedge P&L attribution — passed through from a
hedge_pnlcolumn when present, else zero. - Other items — a reserved zero column for manual / out-of-model adjustments.
Finally _reconcile sums the ten attributed components and books the gap to an
unexplained residual: unexplained = total_net_income − Σ(components), with
attribution_check = |unexplained|. Because expected earnings and the four
actual-driven variances are constructed as a closed-form re-expression of the
income identity, the residual is exactly zero when no assumption change is
applied; with an assumption change the residual carries the portion of net
income not explained by the parametric NPV proxy (see Limitations 1 and 4).
Key Assumptions and Their Justification
| Parameter | Default | Derivation | Justification |
|---|---|---|---|
new_business_margin |
0.05 | configured | Expected profit margin on new-business premium; the SoE "value of new business" line is approximated as premium × margin rather than a first-year embedded-value strain calc. 5% is a placeholder industry-style margin, not a calibrated firm value. |
assumption_npv_horizon |
120 months | configured | Projection window over which an assumption change is assumed to affect cash flows. Ten years is a conventional life-block horizon; capped at 600 months in code. |
discount_rate |
0.04 p.a. | configured | Annual rate used to build the monthly annuity factor for the assumption-change NPV. A flat long-run rate proxy; not a term-structure. |
| Sign convention | gains positive | engine design | All variances are oriented so a positive number is favourable to earnings, matching standard AoM / SoE presentation. |
| Lapse proxy | in-force-count delta × per-policy earnings | engine design | Lapse impact is inferred from the change in policy count, not measured from reserve releases — see Limitation 2. |
| Per-policy reserve proxy | net_income / in_force_count |
engine design | The assumption-change NPV uses net income per policy as a stand-in for the reserve base, because the engine is not handed a reserve balance. Approximation only — see Limitation 1. |
Prose. The engine is deliberately thin on calibrated inputs: its defaults
(new_business_margin, assumption_npv_horizon, discount_rate) are the only
tunable parameters, and all of them are generic placeholders rather than values
fitted to a firm's experience. The arithmetic that does the real work — the
income identity and the actual-vs-expected variance decomposition — is closed
form and needs no calibration, which is what makes the snapshot below
deterministic and reproducible. The two genuinely model-shaped pieces
(assumption-change NPV and new-business value) are parametric approximations and
are the parts most in need of strengthening (Limitations 1, 3, 4).
Output Snapshot
Deterministic 2-period run of PnLAttributionEngine v1.0.0 — reproducible,
requires no live firm data (python scripts/model_snapshots.py M-090 in
InsModel; the same canonical input is frozen by
tests/mrm/test_gold_tier4.py::TestGoldPnLAttribution).
Input (Period 1): premium 1,000 · benefit 400 (expected 420) · expense 100
(expected 110) · investment income 200 (expected 190) · reserve change 50
(expected 60) · in-force 100 · new-business premium 200 · net income 650
(expected 600). Assumption change applied: mortality_change = −0.02.
| component | value | meaning |
|---|---|---|
| expected_earnings | 600.00 | net income on beginning-of-period (expected) assumptions |
| mortality_variance | 20.00 | benefits 20 below expected → gain |
| lapse_variance | 0.00 | in-force equals expected in P1 → no lapse deviation |
| expense_variance | 10.00 | expenses 10 below expected → gain |
| investment_variance | 10.00 | investment income 10 above expected → gain |
| reserve_change_variance | 10.00 | reserve build 10 below expected → gain |
| assumption_change_impact | −10.70 | NPV charge from the −2% mortality re-set (parametric proxy) |
| new_business_contribution | 10.00 | 200 NB premium × 5% margin |
| hedge_pnl_attribution | 0.00 | no hedge_pnl column supplied |
| other_items | 0.00 | reserved manual-adjustment line |
| unexplained (residual) | 0.70 | net income not captured by the components |
| = total_net_income | 650.00 | reconciliation target |
| attribution_check | 0.70 | |residual|, gold-test bound is < 1.0 |
The five experience variances (mortality, lapse, expense, investment, reserve)
plus expected earnings and new business explain 660.00 of the 650.00 net income;
the assumption-change NPV proxy then books a −10.70 charge, leaving a +0.70
residual. With no assumption change the residual would be exactly zero — the
0.70 is entirely the gap between the parametric assumption-change approximation
and the realized income identity (Limitation 4). The gold test asserts the
Period-1 frozen values (expected 600, mortality 20, expense 10, investment 10,
reserve 10, new-business 10) and that attribution_check < 1.0; Period 2 adds a
frozen lapse-variance proxy of −13.64 from a 98-vs-100 in-force shortfall.
Captured 2026-06-04 · deterministic, no live data.
Limitations and Known Gaps
-
Assumption-change impact is a single-cohort parametric NPV, not a re-projection. The impact line multiplies a change magnitude by in-force count, net-income-per-policy used as a reserve proxy, and a flat-rate annuity factor. It does not re-run the liability model under the new assumption, ignores duration/convexity, and uses net income (not a reserve balance) as the base. It is directional, not valuation-grade.
-
Lapse variance is inferred from in-force counts, not measured. Lapse impact = in-force-count delta × per-policy expected net income. This conflates lapse with any other source of count movement (deaths, maturities, new business) and values each lost policy at the average earnings rate rather than at its reserve release. It is a proxy line, not a true lapse SoE.
-
New-business contribution is premium × flat margin. There is no first-year strain, no acquisition-cost amortization, and no embedded-value "value of new business" calculation — just a 5% default margin on premium. The number is illustrative.
-
The residual absorbs assumption-change approximation error. Because the experience variances close the income identity exactly, any imprecision in the parametric assumption-change line lands in
unexplained. The gold test only bounds this at < 1.0 for the canonical case; for larger blocks or larger assumption shocks the residual is not bounded by the engine and could be material. There is no automated alert on residual size beyond the test. -
Hedge P&L and "other items" are pass-through / zero. Hedge attribution is whatever is handed in via a
hedge_pnlcolumn (no decomposition into greeks/basis), andother_itemsis a hard-coded zero placeholder for manual adjustments. Neither is independently modeled. -
No firm-data grounding (BV-032). Every number here comes from a constructed canonical input, not from any company's filed financials. This card makes no 10-K-derived claims; the snapshot is "given these inputs, the engine produces these outputs" evidence only. Real-firm SoE would require actual-vs-expected frames sourced from a firm's planning and valuation systems, which are out of scope and subject to the BV-032 firm-data divergence caveat.
-
Single-currency, single-block, no inter-period roll-forward. Each period is decomposed independently against its own expected frame; the engine does not chain opening-to-closing balance-sheet movements across periods or reconcile to an embedded-value roll-forward.
Tracked for ratification (not applied in this documentation pass). The following are output-changing / modeling-code items left for ratification, noted here for transparency: replace the single-cohort flat-rate assumption-change NPV proxy with a real before/after reserve re-projection (INV-018); derive lapse variance from modeled reserve releases rather than the in-force-count proxy (INV-018); wire real hedge-P&L attribution and tighten the residual toward ~0 (INV-018); produce tier-2 backtest evidence reconciling components to a firm's realized source-of-earnings (MRM 10.2); produce tier-2 sensitivity-test evidence on
new_business_margin/assumption_npv_horizon/discount_rateand the proxy lines (MRM 10.2); obtain tier-2 independent effective-challenge / 2L peer-review ratification (lifecycle.peer_review.status: pending,ratification_ref: null).
Validation Packet
| Check | Where | What it asserts |
|---|---|---|
| Frozen gold test | tests/mrm/test_gold_tier4.py::TestGoldPnLAttribution::test_gold_pnl_attribution |
Period-1 and Period-2 component values frozen (expected 600; mortality 20; expense 10/3; investment 10; reserve 10; new-business 10/7.5; P2 lapse −13.64); attribution_check < 1.0; all attribution columns present. |
| Reconciliation invariant | _reconcile in engine |
unexplained = total_net_income − Σ(components); residual is exactly 0 when no assumption change is applied. |
| Sign-convention checks | gold test (P1 directions) | mortality, expense, investment variances and new-business contribution all > 0 for the canonical favourable case. |
| Snapshot reproducibility | scripts/model_snapshots.py M-090 |
Deterministic re-run of the canonical input matches the table in Output Snapshot (no live data). |
| Input contract validation | validate_input (base engine) + ENGINE_CONTRACT.inputs.required |
Requires period, premium, benefit, expense, investment_income, reserve_change, net_income, in_force_count. |
Tier-2 effective-challenge status (MRM 10.2). The checks above are mechanical (gold / reconciliation / sign-convention / snapshot reproducibility / input contract). MRM 10.2 additionally requires sensitivity and backtest evidence:
- Sensitivity tests — DONE (RAT-090 COND-002). Frozen parametric sweeps on
all three tunable parameters (
new_business_margin,assumption_npv_horizon,discount_rate) and both proxy lines (lapse in-force-count proxy, assumption-change NPV proxy) are intests/mrm/test_gold_tier4.py::TestSensitivityPnLAttribution(12 tests) and indexed ininsightalm/modelling/validation_evidence/M-090/v1.0.0/README.md. The sweep targets are real engine output, frozen from the source — no fabricated numbers. - Backtesting against realized outcomes — PENDING (RAT-090 COND-001, honest-conditional). Requires actual-vs-expected frames from a firm's planning / valuation systems to reconcile the attributed components to a firm's realized source-of-earnings. Out of scope today (see Limitation 6 / BV-032 — the snapshot uses a constructed input, not firm data). This is a missing data input, not an engine-build; it cannot be assembled without firm frames. No backtest evidence exists; do not infer any.
The remaining gap (COND-001 backtest) plus tier-2 independent effective-challenge
is tracked on RAT-090-v1.0.0 (decision: conditionally_approved); M-090 cannot
reach unconditional approval until firm-grounded frames exist.
References
Source-of-earnings / analysis-of-movement methodology: - IFRS 17 Insurance Contracts — analysis-of-movement / reconciliation of the contractual service margin and liability for remaining coverage (the contemporary disclosure form of source-of-earnings). Conceptual antecedent only — the engine does not implement or conform to IFRS 17 (see Standards Coverage). - US GAAP ASC 944-40-50 (LDTI) — disclosure roll-forward of the liability for future policy benefits and the DAC balance. Conceptual antecedent only — not a conformance target (see Standards Coverage). - IAA / SOA literature on Analysis of Surplus and Source of Earnings — the classical actuarial decomposition of period earnings into expected profit plus experience and assumption-change variances. - Embedded-value reporting guidance (EEV / MCEV principles) — "value of new business" and the experience-vs-assumption split that this engine approximates. - ASOP No. 56 — Modeling (cross-cutting: intended use, reliance, residual / unexplained-difference disclosure).
Engine and tests (InsModel):
- Engine: ecosystem/InsModel/Models/firmmodel/engines/pnl_attribution_engine.py
(PnLAttributionEngine, insmodel.L4.pnl_attribution_actuarial, v1.0.0).
- Gold test:
ecosystem/InsModel/Models/tests/mrm/test_gold_tier4.py::TestGoldPnLAttribution.
- Snapshot generator: ecosystem/InsModel/Models/scripts/model_snapshots.py
(snap_M_090).
Internal:
- Decision 018 (MRM Framework) — Management/decisions/018-mrm-framework-multi-regime.md.
- BV-032 firm-data divergence caveat — applies to any real-firm application of this model.
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 — Tier-2 hand-authoring of Methodology, Key Assumptions and
Their Justification, Output Snapshot, Limitations and Known Gaps, Validation
Packet, and References from
pnl_attribution_engine.py(insmodel.L4.pnl_attribution_actuarial, v1.0.0) + the frozen gold test + the deterministic snapshot. Stub marker advanced to ``. - 2026-06-05 — doc-currency stamp recorded in
modelling/model_doc_stamps.yaml(fingerprint21f2666813b7a45f,pnl_attribution_engine.py). - 2026-06-06 — code-grounded documentation-accuracy pass: added Standards
Coverage (ASOP 56 as the only binding standard; explicit no-statutory/no-GAAP
conformance; IFRS 17 / ASC 944-40-50 as conceptual antecedents only) and
Dependencies (model-of-models reliance on upstream-sourced actual/expected
frames +
assumption_changes; noupstream_modelsbound in registry; constructed snapshot input) sections; tightened the Description's "both statutory and GAAP basis" phrasing to basis-agnostic; added a "Pending validation (tier-2 gaps)" note disclosing the missing backtesting and sensitivity evidence; noted gated INV-018 / tier-2 / 2L items under Limitations. No model outputs, back-test numbers, or validation results were fabricated or changed. - 2026-06-06 — Decision-053 1L remediation of RAT-090-v1.0.0. COND-002
(sensitivity) closed: added
TestSensitivityPnLAttribution(12 frozen parametric sweeps on the three tunable parameters + two proxy lines) totests/mrm/test_gold_tier4.pyand assembled the validation-evidence pack atinsightalm/modelling/validation_evidence/M-090/v1.0.0/README.md; all sweep targets are real engine output. COND-001 (backtest) held honest-conditional — needs firm actual/expected frames (BV-032), not assemblable without fabrication. Closed FND-090-01: engine docstring identity corrected M-801 → M-090. Reconciled registrydocumentation_pack(model_card/validation_evidence/change_log → present) and re-stamped doc-currency. No model outputs or back-test numbers fabricated.
Open findings (2)
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.
P&L attribution: assumption-change line is a flat-rate NPV proxy; lapse variance is a count proxy
The assumption-change impact is a single-cohort flat-rate NPV using net-income-per-policy as a reserve proxy (no re-projection); lapse variance is an in-force-count proxy; new-business is premium x flat 5% margin; hedge_pnl is pass-through; other_items is hard zero. The period-1 residual (0.70) is the gap between the proxy and realized income.
Recommendation: Replace the assumption-change proxy with an actual before/after reserve re-projection; derive lapse variance from modeled releases; wire real hedge P&L. Tighten the residual to ~0.
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 | pending |
Conditionally approved — RAT-090-v1.0.2
2L issued RAT-090-v1.0.2 with the following conditions outstanding. Bridge audit Rule 2 treats this as a WARN; the model can remain in monitoring with the conditions on record.
| id | deadline | condition |
|---|---|---|
| COND-001 | 2026-09-04 | STILL OPEN (honest-conditional). No backtest evidence reconciling the attributed source-of-earnings components to a firm's realized earnings movement. Requires actual-vs-expected frames from a firm's planning / valuation systems, which do not exist (BV-032). Reviewer re-checked this pass: no new firm-grounded frames have appeared since v1.0.1 (only the v1.0.0 validation-evidence directory exists). The 1L correctly declined to fabricate; the gap is recorded truthfully in the card (Limitation 6, Validation Packet) and the validation pack (§4). This is a blocker for unconditional approval and is NOT cleared. Produce a backtest pack once firm-grounded frames exist, or hold the unconditional-approval bar. |
| COND-002 | CLEARED (re-confirmed this pass). Reviewer INDEPENDENTLY re-ran TestSensitivityPnLAttribution (12 collected, all pass) plus the gold test and the snapshot; the sweeps exercise all five load-bearing levers (new_business_margin, assumption_npv_horizon, discount_rate, lapse in-force-count proxy, assumption-change NPV proxy) with frozen real-engine targets that move monotonically/linearly in the economically correct direction. Adequate Tier-2 sensitivity evidence. |