ALM Cash Flow Projection & Duration Matching Project asset and liability cash flows and compute duration / convexity matching for ALM.
Asset-liability matching analysis. Inputs from in-force liability block + bond portfolio; outputs duration gap, convexity mismatch, and reinvestment risk metrics.
Inputs, processing, outputs
Methodology
M-100 implements the asset-liability management (ALM) discipline of classic immunization: measuring the interest-rate sensitivity of an insurer's asset cash flows against its liability cash flows and reporting the residual mismatch. It carries no firm-specific product model. The duration/gap computation documented in this card is performed by ALMEngine (firmmodel/engines/alm_engine.py, v2.2.0, registered as insmodel.L4.alm_engine). The registry composes M-100 from four engines, however: alongside insmodel.L4.alm_engine it lists insmodel.L4.asset_engine (M-102), insmodel.L4.asset_projection (M-106), and insmodel.L6.projection_orchestrator (M-701) under M-100's model_membership. Those three are upstream producers / sequencers of the asset (and, via the orchestrator, composed) cash-flow ladders that ALMEngine then consumes — AssetEngine supplies GA/SA asset values and portfolio metrics, AssetProjectionEngine supplies the monthly GA asset projection, and ProjectionOrchestrator sequences the engine chain — not additional computations invoked inside ALMEngine (see Dependencies). The card covers three composable computations the binding ALM engine exposes:
-
Single-instrument duration (
calculate_bond_duration) — for each fixed-income holding the engine enumerates the contractual cash-flow ladder (periods = maturity × frequency, couponface × coupon_rate / frequency, principal at maturity), discounts each flow at the instrument yield using the period discount factor(1 + ytm/freq)^(−t), and forms the present-value-weighted average time-to-receipt. This yields Macaulay durationD_mac = Σ(t·PV_t) / ΣPV_t, modified durationD_mod = D_mac / (1 + ytm/freq), and convexityΣ[PV_t·t·(t + 1/freq)] / [ΣPV_t·(1 + ytm/freq)²]. Convexity is the second-order curvature correction to the duration-implied price move. -
MBS effective duration with prepayment (
_calculate_mbs_base_duration→calculate_mbs_duration) — forbond_type == "mbs", the engine replaces the contractual ladder with a monthly projected cash-flow ladder: scheduled level-payment amortization plus a PSA-style prepayment overlay. Prepayment speed is set by an arctangent S-curveCPR = cpr_base + 0.39·(arctan(incentive·50)/π + 0.5)clipped to[0.02, 0.45], whereincentive = WAC − rate_level − 0.005(50 bps refi friction), and ramped over the first 30 seasoning months (PSA). The result is an option-adjusted effective duration that shortens as the pool moves in-the-money, plus negative convexity (−125·(1 + |incentive|·10)) reflecting the embedded prepayment option. -
Portfolio duration gap (
calculate_duration_metrics) — given asset and liability ladders (each a frame ofamount×duration), the engine value-weights duration within each side (Σ(amount·duration)/Σamount) and reports the duration gapD_asset − D_liability. It compares|gap|against the configuredduration_tolerance(default 0.5y) and raisesrequires_rebalancingwhen the book is outside the immunization band. A flat yield curve{t: ytm}is synthesized per holding inside_calculate_duration_analysisfor discounting; the engine does not consume a live market curve in this path.
The calculate(data, calculation_type="duration_analysis") entry point runs computation (1)/(2) per holding and appends a portfolio summary row (face-value-weighted portfolio_macaulay_duration, portfolio_modified_duration, portfolio_convexity). The duration gap (3) is invoked directly on asset/liability frames. Reinvestment-risk direction is read off the sign of the gap (see Key Assumptions): a negative gap means assets repay sooner than liabilities, exposing the carrier to reinvesting principal at lower future rates.
Scope of this card vs. the engine's full capability surface. ALMEngine v2.2.0 is broader than the duration/gap path documented here. Its headline module docstring describes a "comprehensive ALM engine ... for multi-currency insurers" and the code (and ALMEngine.yaml legacy_metadata) also implements: cross-currency USD/JPY hedging (calculate_hedged_bond_duration, calculate_global_portfolio_duration, optimize_fx_hedging, FX-forward valuation — the engine's headline purpose), key-rate durations (calculate_key_rate_durations), the efficient frontier / mean-variance optimization and multi-objective Pareto optimizer (the v2.2.0 additions), stress-duration impact (calculate_stress_duration_impact), and liquidity / multi-regime capital optimization (optimize_liquidity_allocation, optimize_multi_regime_capital, run_integrated_optimization). M-100's intended use, as documented in this card, covers only the duration / gap / convexity / DV01-mismatch path. The cross-currency, frontier, Pareto, key-rate, stress, liquidity, and capital-optimization modes are present in the engine and exercised by the engine's own test suite (see the efficient-frontier and Pareto rows in the Validation Packet, retained because they document engine behavior) but are out of scope for this card — they are not part of M-100's documented intended-use surface and would need their own scoping or model cards before being relied upon as M-100 outputs.
Key Assumptions and Their Justification
ALMEngine carries no formally-bound A-NNN registry entries (it is a derivative-of-inputs duration calculator, not an assumption-driven projection); the load-bearing choices are engine parameters and modeling conventions. (RAT-100-v1.0.1 COND-003 closed, 2026-06-08: the registry inputs.assumptions binding, which previously over-claimed A-002/A-010/A-030, is now empty to match this parameter-driven reality — those mortality/lapse/default IDs belong to the upstream liability producers M-001/M-002 and the credit path, not the immunization math.)
| ID / parameter | Name | Value | Derivation | Justification |
|---|---|---|---|---|
duration_tolerance |
Immunization band | 0.5 yr | config_default | Gap inside ±0.5y is treated as matched; outside triggers requires_rebalancing. Standard industry first-order immunization tolerance. |
precision |
Output rounding | 4 dp | config_default | Durations rounded to 4 dp, convexity to 1 dp, dollar duration to 2 dp inside calculate_bond_duration. |
day_count_convention |
Day count | ACT/365 | config_default | Declared in the engine contract; the duration path uses uniform t/frequency period spacing rather than calendar day-counting (see Limitation 3). |
cpr_base |
Base prepayment speed | 0.06 (6% CPR) | published_source | PSA-standard baseline conditional prepayment rate for agency MBS at-the-money; calibrated to the arctan S-curve so incentive=0 → ~6% CPR. |
psa_max_month |
Seasoning ramp | 30 months | published_source | PSA convention: CPR ramps linearly from origination over 30 months before reaching its terminal speed. |
| discount basis | Flat-curve discounting | per-holding ytm |
modeling_choice | The duration-analysis path builds {tenor: ytm} from each instrument's own yield rather than a term structure — adequate for single-instrument duration, a simplification for curve risk (see Limitation 2). |
| gap sign convention | Reinvestment-risk direction | D_asset − D_liab |
actuarial_standard | ASOP 7 cash-flow-testing convention: negative gap (assets shorter) ⇒ reinvestment risk; positive gap (assets longer) ⇒ price/disinvestment risk. |
Operational assumptions beyond parameters:
- Liability cash flows are supplied, not generated. calculate_duration_metrics consumes pre-computed liability amount × duration ladders. The engine itself does not project policyholder liabilities — those originate upstream (e.g. M-001/M-002 reserve engines or a liability cash-flow generator) and are passed in.
- Reinvestment risk is inferred, not a native output. There is no reinvestment_risk field in the engine. M-100 surfaces it as the direction implied by the gap sign plus the per-bp DV01 mismatch on liability notional (gap × notional × 0.0001). This is an honest derivation, not an engine-reported quantity (see Limitation 1).
- Convexity for vanilla bonds is positive; for MBS it is hardcoded negative. The vanilla path computes convexity analytically from the cash-flow ladder; the MBS path returns a parametric negative convexity rather than a fully re-priced grid.
Output Snapshot
Deterministic run of ALMEngine v2.2.0 — reproducible, requires no live firm data (python scripts/model_snapshots.py M-100 in InsModel; the bond-level duration figures are asserted by tests/mrm/test_gold_tier2.py::TestGoldALM, and the gap/rebalance path by tests/test_alm_completion.py).
Input (canonical): bond-level metrics from a 5% semi-annual 10-year par bond (coupon = yield, the frozen ALMEngine par-bond gold scenario — TestGoldALM; M-302 is alm_engine's legacy scheme-B engine governance id, not a separate registered model); duration gap from a $600M asset ladder (3.0/6.0/10.5y buckets) vs a $700M liability ladder (6.0/9.0/11.5y buckets).
| output | value | meaning |
|---|---|---|
| macaulay_duration (par bond) | 7.99 | PV-weighted average time-to-receipt, years |
| modified_duration (par bond) | 7.79 | D_mac / (1 + ytm/freq) — % price move per 100 bps |
| convexity (par bond) | 73.60 | second-order curvature correction (positive for vanilla bond) |
| asset_duration | 6.10 | value-weighted asset-ladder duration, years |
| liability_duration | 8.20 | value-weighted liability-ladder duration, years |
| duration_gap | −2.10 | D_asset − D_liab — assets are 2.1y shorter than liabilities |
| requires_rebalancing | True | |gap| = 2.1 > 0.5 tolerance ⇒ outside immunization band |
| dv01_mismatch | −147,000 /bp | gap × $700M × 0.0001 — net $ surplus move per bp |
| reinvestment_risk_direction | REINVESTMENT (assets shorter) | negative gap ⇒ proceeds reinvested at lower future rates |
The par-bond duration is the engine's textbook anchor: a 5% 10-year par bond has Macaulay duration ≈ 7.99y, and dividing by (1 + 0.025) gives modified duration ≈ 7.79y — these match the closed-form result the gold test re-derives independently. The defining ALM mechanic is the duration gap: with assets at 6.10y and liabilities at 8.20y, the −2.10y gap places the book well outside the ±0.5y immunization band, so requires_rebalancing fires. Because the gap is negative (assets shorter than liabilities), the book is exposed to reinvestment risk — asset principal returns before liabilities come due and must be reinvested, losing value if rates fall. The DV01 mismatch of −$147k/bp quantifies the first-order surplus sensitivity: a 100 bps parallel rate move shifts statutory surplus by roughly $14.7M before convexity. Closing the gap means extending asset duration (longer-dated bonds) toward the liability mark.
Captured 2026-06-04 · deterministic, no live data.
Limitations and Known Gaps
-
No native reinvestment-risk output. The engine exposes Macaulay/modified/effective duration, convexity, duration gap, and DV01 mismatch — but no dedicated reinvestment-risk metric. M-100 derives the reinvestment-risk direction from the duration-gap sign and its magnitude from the DV01 mismatch on liability notional. This is a faithful immunization-theory inference, not an engine-reported number; a purpose-built reinvestment-rate-stress output (projecting actual coupon/principal reinvestment at forward rates) is not yet built.
-
Discounting is a flat per-instrument curve, not a term structure. In the
duration_analysispath the engine synthesizes{tenor: ytm}from each holding's own yield (_calculate_duration_analysis), so every cash flow of a given bond is discounted at one flat rate. Key-rate / curve-twist risk requirescalculate_key_rate_durations(a separate method, not exercised here) and a real yield curve. Parallel-shift duration is correct; non-parallel curve risk is out of scope for this snapshot. -
Uniform period spacing, not calendar day-count. Although the contract declares ACT/365, the duration loop uses idealized
t/frequencyspacing (exact half-years for semi-annual). Accrued-interest and settlement-date effects are not modeled; the bias is immaterial for portfolio-level duration but means the figures are mid-period clean-price durations, not settlement-dated. -
MBS convexity is parametric, not grid-repriced.
calculate_mbs_durationreturns a hardcoded negative convexity−125·(1 + |incentive|·10)rather than re-pricing the pool across a rate-shock grid. Effective duration is cash-flow-projected (the S-curve CPR model), but the convexity number is a calibrated proxy. The MBS path is not exercised in this card's snapshot (the canonical input is a vanilla corporate par bond). -
Liabilities are an input, not generated here. M-100 measures the gap against supplied liability durations. If the upstream liability cash flows are stale or mis-projected, the gap inherits that error. Liability projection lives in the reserve/valuation engines (M-001 VM-20, M-002 VM-21), not in ALMEngine.
-
Firm-data path divergent (BV-032). All numbers in this card come from the deterministic canonical inputs, not from any live firm balance sheet. The InsModel firm-data ingestion path is divergent under BV-032, so the model card makes no claim grounded in 10-K or filing data; the Output Snapshot is strictly "given these inputs, the engine produces these outputs."
-
No explicit immunization second-condition check. Classic Redington immunization requires both duration matching and asset convexity ≥ liability convexity. The engine reports the duration gap and per-side convexity but does not assert the convexity-dominance condition; a fully immunized verdict would need that second test, which is not wired into
calculate_duration_metrics.
Tracked for ratification. Status of the previously-tracked items: (a) DONE (2026-06-08, COND-004) — the frozen duration-gap / DV01 golden regression case is now added (
TestGoldALM::test_gold_alm_duration_gap_dv01), and the Tier-2 scoped evidence pack is assembled atmodelling/validation_evidence/M-100/v1.0.0/; (b) wire the 3 registered support engines (asset_engine,asset_projection,projection_orchestrator) into the ALM composition so M-100 actually consumes them, or formally re-scope the registry to drop them if unused — open, output-changing, next revalidation; (c) build a native reinvestment-risk output (forward-rate reinvestment-stress projection) instead of inferring direction from the gap sign, and grid-reprice MBS convexity instead of the parametric−125·(1+|incentive|·10)proxy — open, output-changing; (d) DONE — the Tier-2 peer-review sign-off and 2L ratification (RAT-100-v1.0.1, approved) are recorded andlast_validated_onis set. The registryinputs.assumptionsover-claim (formerly tracked under Limitations) is resolved as COND-003 (binding emptied to match the parameter-driven reality).
Validation Packet
| check | what it asserts | evidence |
|---|---|---|
| Par-bond duration identity | 5% 10y par bond ⇒ Macaulay ≈ 7.99y, modified ≈ 7.79y, convexity > 0; re-derived by an independent in-test loop and matched to engine output within GOLD_DURATION_TOL (0.05y) |
tests/mrm/test_gold_tier2.py::TestGoldALM::test_gold_alm (passing) |
| Duration-gap & rebalance flag (gold) | value-weighted D_asset − D_liab on the frozen canonical ladders ⇒ asset 6.1y, liab 8.2y, gap −2.1y; requires_rebalancing fires iff |gap| > duration_tolerance; expected values re-derived by an independent in-test computation and matched to engine output |
tests/mrm/test_gold_tier2.py::TestGoldALM::test_gold_alm_duration_gap_dv01 (passing); reproduced by scripts/model_snapshots.py M-100 |
| DV01 mismatch (gold) | gap-implied per-bp dollar magnitude on liability notional gap × notional × 1bp ⇒ −147,000 $/bp; re-derived independently in-test and matched |
tests/mrm/test_gold_tier2.py::TestGoldALM::test_gold_alm_duration_gap_dv01 (passing) |
| MBS duration bounds | projected-cash-flow MBS duration is capped to [1, 15] years; 30y par MBS lands ~4–8y, 15y par MBS ~3–6y |
tests/test_alm_completion.py::TestMBSCashFlowDuration (8 cases incl. bounds, WAM monotonicity, seasoning, cpr_base) |
| MBS prepayment monotonicity | in-the-money pools (WAC ≫ rate) shorten duration; higher cpr_base shortens duration; effective ≤ base |
tests/test_alm_completion.py::TestMBSCashFlowDuration / TestMBSIntegration |
| MBS negative convexity | embedded prepayment option ⇒ convexity < 0 | tests/test_alm_completion.py::TestMBSIntegration::test_mbs_negative_convexity |
| Efficient-frontier sanity (engine-scope, out of M-100 card scope — see Methodology scoping note) | duration-constrained surplus frontier is monotone, allocations sum to 1, HY respects max_below_ig |
tests/test_alm_completion.py::TestEfficientFrontier (9 cases) |
| Pareto optimizer non-dominance (engine-scope, out of M-100 card scope — see Methodology scoping note) | no Pareto-frontier solution is dominated by another; ≥2 objectives required | tests/test_alm_completion.py::TestMultiObjectiveOptimization (8 cases) |
| Determinism / no live data | snapshot reproduces byte-identically with no firm-data dependency (BV-032-immune) | scripts/model_snapshots.py design note + python scripts/model_snapshots.py M-100 |
Frozen gap/DV01 golden case — CLEARED (RAT-100-v1.0.1 COND-004, 2026-06-08). The
duration-gap and DV01-mismatch path — the defining ALM mechanic — is now gold-asserted
by a dedicated CI-gated frozen regression case, TestGoldALM::test_gold_alm_duration_gap_dv01,
analogous to the par-bond duration gold. The test re-derives the value-weighted
asset/liability durations (6.1y / 8.2y), the gap (−2.1y), the rebalance flag (True), and
the DV01 mismatch (−147,000 $/bp) independently from the frozen canonical ladders and asserts
them against the real engine output (no fabrication — every expected value is recomputed in
the test). This was previously the one honest-pending sub-item; it is now closed, so
documentation_pack.validation_evidence is present in the registry.
References
Actuarial standards: - ASOP No. 7 — Analysis of Life, Health, or Property/Casualty Insurer Cash Flows (cash-flow-testing and ALM framework; basis for the duration-gap and reinvestment-risk reading). - ASOP No. 22 — Statements of Opinion Based on Asset Adequacy Analysis for Life or Health Insurers (asset-adequacy / immunization context in which duration matching is used). - ASOP No. 56 — Modeling (cross-cutting modeling standard: intended use, sensitivity, reliance, documentation).
Technical / methodological:
- F. M. Redington (1952), "Review of the Principles of Life-Office Valuations" — original immunization theory (duration matching + convexity-dominance second condition).
- F. Macaulay (1938) — original duration definition (PV-weighted average time-to-receipt).
- Fabozzi, Bond Markets, Analysis, and Strategies — modified duration, convexity, DV01, and PSA/CPR prepayment conventions used in the MBS path.
- PSA / SIFMA standard prepayment model — 30-month seasoning ramp and CPR↔SMM conversion SMM = 1 − (1 − CPR)^(1/12) implemented in _calculate_mbs_base_duration.
Source code:
- Engine: ecosystem/InsModel/Models/firmmodel/engines/alm_engine.py (ALMEngine v2.2.0, insmodel.L4.alm_engine) — calculate_bond_duration, _calculate_mbs_base_duration, calculate_mbs_duration, calculate_duration_metrics, _calculate_duration_analysis.
- Gold test: ecosystem/InsModel/Models/tests/mrm/test_gold_tier2.py::TestGoldALM (frozen par-bond duration).
- Completion test suite: ecosystem/InsModel/Models/tests/test_alm_completion.py (MBS cash-flow duration, efficient frontier, Pareto optimization).
- Snapshot: ecosystem/InsModel/Models/scripts/model_snapshots.py (snap_M_100).
Internal:
- Decision 018 (MRM Framework) — Management/decisions/018-mrm-framework-multi-regime.md.
- BV-032 (firm-data path divergence) — reason the card makes no 10-K-grounded claim; snapshots are the deterministic evidence substitute.
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-authoring pass: advanced from stub to hand-authored Methodology, Key Assumptions and Their Justification, Output Snapshot, Limitations and Known Gaps, Validation Packet, and References (grounded in
ALMEnginev2.2.0 +ALMEngine.yamllegacy_metadata + engine_registry M-100 entry). Stub marker advanced to ``. - 2026-06-06 — code-grounded documentation-accuracy pass against
alm_engine.py/ALMEngine.yaml+ engine_registry. Added dedicated Standards Coverage section (ASOP 7 / 22 / 56 +internal, cross-referenced to the[asop_56, internal]registry tag) and Dependencies section (upstream liability producers M-001/M-002; asset producersasset_engineM-102 /asset_projectionM-106; sequencerprojection_orchestratorM-701). Corrected the "engine-only ... driven entirely by ALMEngine" framing to reflect the registry's 4-engine composition, annotating the 3 supporting engines as upstream producers/sequencers (not invoked inside ALMEngine). Added a Methodology scoping note that M-100's intended use covers only the duration/gap path and that the engine's other modes (cross-currency USD/JPY hedging, efficient frontier, Pareto optimization, key-rate durations, FX/liquidity/capital optimization, stress testing) are out of card scope; annotated the frontier/Pareto Validation-Packet rows accordingly. Restated the duration-gap golden gap as an explicit pending — frozen gap/DV01 golden case + CI-gated regression. Noted gated/ratification items under Limitations. No model outputs, back-test numbers, or validation results were fabricated or changed.
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-100-v1.0.1
Latest ratification on file: RAT-100-v1.0.1. Authored by 2L (mrm-peer-reviewer) per Decision 028 charter §5 Pattern A.