Tier 2 · Management Reporting

P&L Attribution (Source of Earnings)

M-090 · lifecycle: monitoring · RAT-090-v1.0.2 (conditional)

Intended Use

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.


Components

Inputs, processing, outputs

data sources
DS-008
assumptions
A-001, A-002, A-010, A-070
engines
insmodel.L4.pnl_attribution_actuarial
contracts
pnl_attribution_v1
dimensions
D6

Methodology & Mechanics

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:

  1. 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).
  2. 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).
  3. Assumption-change impact (_compute_assumption_change_impact) — the NPV shock from re-setting forward-looking assumptions. For each named change it computes change_magnitude × in_force_count × |per-policy net income| × annuity_factor / horizon, where the monthly annuity factor uses discount_rate (default 4% p.a.) over assumption_npv_horizon months (default 120). This is a parametric, single-cohort NPV approximation, not a full re-projection.
  4. New-business contribution (_compute_new_business_contribution) = new_business_premium × new_business_margin (default margin 5%).
  5. Hedge P&L attribution — passed through from a hedge_pnl column when present, else zero.
  6. 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

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

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

Limitations and Known Gaps

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. Hedge P&L and "other items" are pass-through / zero. Hedge attribution is whatever is handed in via a hedge_pnl column (no decomposition into greeks/basis), and other_items is a hard-coded zero placeholder for manual adjustments. Neither is independently modeled.

  6. 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.

  7. 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_rate and 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 Evidence

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:

  1. 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 in tests/mrm/test_gold_tier4.py::TestSensitivityPnLAttribution (12 tests) and indexed in insightalm/modelling/validation_evidence/M-090/v1.0.0/README.md. The sweep targets are real engine output, frozen from the source — no fabricated numbers.
  2. 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

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

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 (fingerprint 21f2666813b7a45f, 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; no upstream_models bound 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) to tests/mrm/test_gold_tier4.py and assembled the validation-evidence pack at insightalm/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 registry documentation_pack (model_card/validation_evidence/change_log → present) and re-stamped doc-currency. No model outputs or back-test numbers fabricated.

2L Inventory Review

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.

HIGH INV-029 · P5 · validation-gap

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.

LOW INV-018 · P3 · proxy

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.


Validation Coverage

Per-tier expectations

Per MRM Framework §10.2 + §10.3, this model's regulatory_frameworks tag list activates the following overlays:

asop_56 internal
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

Ratification

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.