Tier 3 · Internal Risk Management

Strategic Asset Allocation

M-115 · lifecycle: monitoring · RAT-115-v1.0.0

Intended Use

Strategic Asset Allocation Recommend long-horizon strategic asset allocation given liability profile and risk tolerance.

Optimization across asset classes subject to capital and liability constraints. Consumed by investment-committee decisions, not financial statements.


Components

Inputs, processing, outputs

data sources
DS-001 · DS-021
assumptions
A-030, A-050, A-060, A-090
engines
insmodel.L4.saa_engine
insmodel.L4.asset_management
contracts
saa_results_v1
dimensions
D2 · D4

Methodology & Mechanics

Methodology

M-115 implements strategic asset allocation (SAA) for an insurance general account as a constrained Markowitz mean-variance optimization. The principal engine is insmodel.L4.saa_engine (SAAEngine v1.0.0); the companion insmodel.L4.asset_management (AssetManagementEngine v2.0.0) translates a chosen allocation into fee-revenue / operating-economics for the asset-management-subsidiary view. Both are Tier-3: the outputs inform the investment committee and ALM strategy but do not flow to statutory or GAAP financial statements, so no formal MRM ratification is required (Decision 028 reserves 2L ratification for Tier-1/Tier-2). The card is nonetheless authored to full methodology depth.

The optimizer allocates across 8 NAIC asset categoriesFIXED_MATURITY, STRUCTURED_SECURITIES, MORTGAGE_LOANS, POLICY_LOANS, ALTERNATIVES, CASH_SHORT_TERM, EQUITY_SECURITIES, OTHER_INVESTED — by maximizing the quadratic utility

U(w) = w'μ − (λ/2) · w'Σw

equivalently minimizing −w'μ + (λ/2)·w'Σw, where μ is the expected-return vector, Σ the covariance matrix, and λ the risk-aversion coefficient (default 5.0). The problem is solved with SciPy SLSQP (maxiter=500, ftol=1e-12), starting from an equal-weight feasible point, with an analytic Jacobian −μ + λ·Σw. Post-solve, weights are floored at zero and renormalized to sum to one (_optimize_mean_variance).

The optimization is subject to the following insurance-specific constraints (_build_constraints):

  1. Budget — weights sum to 1 (equality).
  2. Long-only with per-category caps/floors — bounds [min_allocation, max_allocation] per category. When the run config omits these dicts, the engine defaults the bounds to the ENGINE_CONTRACT reference caps/floors rather than to [0, 1] (_build_constraints, saa_engine.py lines 449–453: max_alloc = params.get("max_allocation", contract_max)). So per-category caps are active by default (e.g. POLICY_LOANS ≤ 0.10, FIXED_MATURITY ≤ 0.90, ALTERNATIVES ≤ 0.10, plus floors FIXED_MATURITY ≥ 0.20, CASH_SHORT_TERM ≥ 0.02); a config-supplied max_allocation / min_allocation still takes precedence (overridable). See Limitation 1 for the residual gap (generic reference caps, not firm-calibrated).
  3. ALM duration matchingtarget − tol ≤ w'd ≤ target + tol (default 7.0 ± 0.5 years), two inequality constraints against the per-category duration vector d.
  4. Credit-quality floor — weighted-average numeric rating w'r ≥ min_rating (default 4.33 ≡ BBB), using the _RATING_NUMERIC AAA=7 … CCC=1 ladder.
  5. Liquidity floorw'ℓ ≥ min_liquidity_ratio (default 0.10), where is the per-category liquidity weight (CASH=1.0, FIXED_MATURITY=0.8, etc.).
  6. Maximum volatility (optional)w'Σw ≤ max_volatility² when configured.

Expected returns are resolved by priority (_resolve_expected_returns): (1) an explicit expected_return column; (2) CAPM E[rᵢ] = r_f + βᵢ·(r_m − r_f) from a beta column; (3) configured default_expected_returns; (4) a fallback r_f + category spread table. Covariance is resolved (_resolve_covariance) from either a volatility column or a synthetic per-category volatility table, combined with a constant pairwise correlation (default ρ = 0.30) to form Σ = D·C·D, then projected to the nearest PSD matrix by eigenvalue clipping. A full Ledoit-Wolf shrinkage estimator (_estimate_covariance) is implemented for the historical-return-series path but is not exercised by the canonical (column-free) input.

The engine also traces a 20-point efficient frontier (min-variance at each target return between the feasible min/max return), computes portfolio metrics (return, volatility, Sharpe, duration, liquidity ratio, RBC C-1 capital charge w'·c1), and emits a rebalance flag when the max absolute drift of current vs optimal weights exceeds rebalance_trigger (default 0.05). Results are published on the saa_results_v1 contract.


Key Assumptions

Key Assumptions and Their Justification

The four formally-bound A-NNN entries plus the engine's operational capital-market and constraint assumptions:

ID / param Name Value (canonical) Derivation Justification
A-030 Strategic return assumptions category spreads over r_f data_calibrated Long-horizon SAA return inputs; informs μ.
A-050 Asset-class volatility / correlation synthetic vol table, ρ=0.30 published_source Constant-correlation structure is a deliberate simplification of a full covariance estimate.
A-060 Duration / ALM target 7.0 ± 0.5 yr data_calibrated Liability-duration proxy for general-account matching.
A-090 RBC C-1 capital factors NAIC C-1 defaults published_source NAIC RBC C-1 base factors per asset category.
risk_aversion (λ) Risk-aversion coefficient 5.0 parameter Utility trade-off between return and variance; mid-range for a GA.
min_rating Credit-quality floor 4.33 (BBB) parameter Investment-grade weighted-average floor.
min_liquidity_ratio Liquidity floor 0.10 parameter Minimum liquid fraction for claims-paying / surrender risk.
risk_free_rate Risk-free rate 0.04 market_data Sharpe-ratio and CAPM baseline.

Capital-market assumptions (engine reference defaults, used when no columns supplied):

Asset category E[return] Volatility Duration (yr) Rating Liquidity RBC C-1
FIXED_MATURITY r_f+1.2% 0.04 6.5 5.0 (A−) 0.80 0.010
STRUCTURED_SECURITIES r_f+1.8% 0.06 4.0 4.5 0.40 0.015
MORTGAGE_LOANS r_f+2.0% 0.05 5.0 4.5 0.20 0.030
POLICY_LOANS r_f+1.0% 0.01 8.0 7.0 (AAA) 0.05 0.002
ALTERNATIVES r_f+5.0% 0.15 0.0 3.0 (BB) 0.10 0.200
CASH_SHORT_TERM r_f+0.1% 0.005 0.25 7.0 (AAA) 1.00 0.003
EQUITY_SECURITIES r_f+6.0% 0.16 0.0 3.0 (BB) 0.70 0.300
OTHER_INVESTED r_f+1.5% 0.08 3.0 4.0 (BBB−) 0.30 0.050

These defaults are engine fallbacks, not calibrated firm assumptions. They make the canonical snapshot reproducible without live or reference data; a production run supplies firm-specific returns, a real covariance matrix, and firm-calibrated per-category caps/floors that override the generic ENGINE_CONTRACT reference caps the canonical run uses by default.


Output Snapshot

Output Snapshot

Deterministic single-run of SAAEngine v1.0.0 on the canonical 8-category input with the gold-test configuration (duration_target 7.0 ± 0.5, risk_aversion 5.0, min_liquidity_ratio 0.10, min_rating 4.33, risk_free_rate 0.04). Reproducible, requires no live firm data (python scripts/model_snapshots.py M-115 in InsModel; the same canonical input is exercised by tests/mrm/test_gold_tier0.py::TestGoldSAA, which asserts structural invariants rather than the frozen weight vector).

Input: 8 NAIC asset categories, no per-asset columns supplied → expected returns, volatilities, durations, ratings, liquidity, and RBC factors all taken from engine reference defaults; per-category caps/floors not explicitly configured, so the engine applies the ENGINE_CONTRACT reference caps/floors by default (post-INV-024 behavior; see Methodology constraint 2 and Limitation 1).

Snapshot status: re-captured 2026-06-06 (post-fix), pending gold re-freeze. The earlier snapshot (81.25% POLICY_LOANS, [0,1] bounds) reflected pre-fix behavior and is superseded by the INV-024 cap fix (PR #52, commit bda809a). The table below is the deterministically re-run point solution from python scripts/model_snapshots.py M-115 — a documentation refresh of an existing engine output, not a new validation/back-test result. Establishing this post-fix vector as the new authoritative gold/frozen baseline is a gold re-freeze that requires principal / 2L ratification and is not performed in this documentation pass.

output value meaning
w[FIXED_MATURITY] 0.8633 optimal weight — duration-bearing core, at/near 0.90 cap
w[STRUCTURED_SECURITIES] 0.0000 optimal weight
w[MORTGAGE_LOANS] 0.0167 optimal weight
w[POLICY_LOANS] 0.1000 optimal weight — binds the 0.10 contract cap
w[ALTERNATIVES] 0.0000 optimal weight
w[CASH_SHORT_TERM] 0.0200 optimal weight — binds the 0.02 floor
w[EQUITY_SECURITIES] 0.0000 optimal weight
w[OTHER_INVESTED] 0.0000 optimal weight
expected_return 0.0517 portfolio E[r] = 5.17%
expected_volatility 0.0351 portfolio σ = 3.51%
sharpe_ratio 0.3333 (E[r] − r_f) / σ
portfolio_duration 6.5000 binds the lower ALM band (7.0 − 0.5)
liquidity_ratio 0.7190 ≥ 0.10 floor (well satisfied; FIXED_MATURITY-heavy book)
capital_charge_rbc_c1 0.0094 weighted RBC C-1 charge
rebalance_recommended True drift vs equal-weight prior > 0.05
weights_sum 1.0000 budget constraint satisfied

With the contract caps active by default, the optimizer now allocates the bulk to FIXED_MATURITY (~86%), the duration-bearing core (cap 0.90), with POLICY_LOANS binding its 0.10 cap, a small MORTGAGE_LOANS sleeve, and CASH_SHORT_TERM at its 0.02 floor. The degenerate 81%-policy-loan concentration of the pre-fix run no longer occurs. The duration constraint binds at its lower bound (6.50 = 7.0 − 0.5) and remains an active constraint; the liquidity floor is comfortably satisfied given the fixed-maturity weight. The residual caveat is that the caps are generic reference caps, not firm-calibrated, so this allocation remains illustrative, not investable (see Limitation 1).

Re-captured 2026-06-06 · deterministic, no live data · scripts/model_snapshots.py M-115 · gold re-freeze gated (see Limitations).


Limitations

Limitations and Known Gaps

  1. Per-category caps are generic reference caps, not firm-calibrated. Per-category allocation caps/floors are now applied by default from the engine's ENGINE_CONTRACT (e.g. POLICY_LOANS ≤ 0.10, FIXED_MATURITY ≤ 0.90, ALTERNATIVES ≤ 0.10; floors FIXED_MATURITY ≥ 0.20, CASH_SHORT_TERM ≥ 0.02) when the run config omits them — _build_constraints falls back to the contract defaults (saa_engine.py lines 449–453), resolving INV-024 (PR #52, commit bda809a). This prevents the degenerate single-category concentration (the pre-fix 81% POLICY_LOANS allocation under [0,1] bounds). The residual gap is that these are generic reference caps, not firm-specific calibrated limits; a production run should still supply firm-calibrated max_allocation / min_allocation (which override the contract defaults). The snapshot allocation remains illustrative, not investable.
  2. Synthetic covariance, not estimated. The canonical path builds Σ from a default per-category volatility table and a single constant pairwise correlation (ρ = 0.30). The Ledoit-Wolf shrinkage estimator (_estimate_covariance) exists but requires a historical return series that the column-free canonical input does not provide, so cross-asset correlation structure is not modeled.
  3. Expected returns are fallback spreads, not firm assumptions. With no expected_return, beta, or default_expected_returns supplied, μ falls back to r_f + a fixed category spread. These are plausible-but-generic inputs; the optimization is only as good as the firm-calibrated returns that replace them.
  4. Duration is a single scalar per category, no convexity / key-rate. ALM matching uses one effective-duration number per asset class; key-rate durations, convexity, and a liability cash-flow profile are not modeled. POLICY_LOANS duration is itself a default proxy (8.0 yr).
  5. No transaction costs, taxes, or turnover penalty. The rebalance flag is a pure drift threshold; the optimizer does not penalize trading away from the current book, so recommended moves may be uneconomic after costs.
  6. AssetManagementEngine (fee economics) is a separate, lightly-coupled path. The companion asset_management engine converts AUM × fee schedule into revenue/expense/operating-income (gold: 1B fixed-income institutional → 18 bps → $1.8M fees, 21% operating margin via ratio-of-revenue expenses). Its expense model is ratios-of-revenue, not a built-up cost base — adequate for strategy framing, not for entity P&L.
  7. Not validated against firm 10-K data. No 10-K reconciliation is claimed. The firm-financials path is divergent (finding BV-032); this snapshot is a synthetic canonical run on engine defaults with mock data only.

Tracked for ratification (not applied in this documentation pass). The following are output-changing / modeling-code items left for ratification, noted here for transparency: (a) re-freezing the M-115 gold/snapshot baseline to the post-fix diversified allocation (the re-captured vector above may be transcribed but not blessed as the new frozen baseline without principal / 2L sign-off); (b) adding a frozen weight-vector regression test now that caps are active — the INV-024 recommendation, still open; the current gold test asserts only structural invariants, not the weight vector; (c) resolving the firm-financials divergence (BV-032 / Limitation 7) and producing a SAA-vs-disclosed-allocation back-test (Tier-3, not required per §10.2, but gated if pursued).


Validation Evidence

Validation Packet

Check Status Evidence / tolerance
Gold regression (canonical I/O) present tests/mrm/test_gold_tier0.py::TestGoldSAA::test_gold_saa
Budget invariant enforced Σwᵢ = 1.0 (snapshot: 1.000000; test abs=1e-6)
Long-only invariant enforced wᵢ ≥ −1e-6 all categories (test-asserted)
Duration band satisfied 7.0 ± 0.6 (test); snapshot 6.50 binds lower band
Return floor satisfied E[r] > r_f (test); snapshot 0.0517 > 0.04
Sharpe positivity satisfied Sharpe > 0 (test); snapshot 0.3333
Liquidity floor satisfied liquidity_ratio ≥ 0.10 − 1e-6 (test); snapshot 0.7190
Capital-charge non-negativity satisfied capital_charge ≥ 0 (test); snapshot 0.0094
PSD covariance enforced _nearest_psd eigenvalue clipping (≥ 1e-10)
Per-category caps active partial Caps now active by default in the engine (ENGINE_CONTRACT, post-INV-024); POLICY_LOANS binds 0.10, FIXED_MATURITY near 0.90 in the re-captured snapshot. The gold test still asserts only invariants (sum-to-1, duration band, etc.), and the snapshot script supplies no explicit cap config (caps come from the contract default). A frozen weight-vector regression that exercises caps is pending (INV-024 recommendation, not yet present).
Back-test vs disclosed allocations missing blocked on firm-data divergence (BV-032); Tier-3, not required
2L ratification not required Tier-3 (Decision 028 reserves ratification for Tier-1/2)

The gold test asserts structural/economic invariants (sum-to-one, non-negativity, duration band, positive return/Sharpe, liquidity floor, non-negative capital) rather than frozen scalar weights, which is appropriate for an SLSQP solution whose exact weights can shift slightly with solver/library versions. The snapshot above reports the current point solution under those invariants.


References

References

Methodology (portfolio optimization): - Markowitz, H. (1952). "Portfolio Selection." Journal of Finance 7(1), 77–91. - Sharpe, W. F. (1964). "Capital Asset Prices: A Theory of Market Equilibrium under Conditions of Risk." Journal of Finance 19(3), 425–442. (CAPM expected-return path.) - Ledoit, O. & Wolf, M. (2004). "A well-conditioned estimator for large-dimensional covariance matrices." Journal of Multivariate Analysis 88(2), 365–411. (Shrinkage covariance path.) - Kraft, D. (1988). "A software package for sequential quadratic programming." DFVLR-FB 88-28. (SLSQP solver underlying scipy.optimize.minimize.)

Insurance ALM / SAA: - NAIC Risk-Based Capital (RBC) C-1 asset-risk base factors — capital-charge inputs _DEFAULT_RBC_C1_FACTORS. - Society of Actuaries — Strategic Asset Allocation for Insurers / general- account ALM literature (duration-matching, liquidity and credit-quality floors for liability-driven investing). - ASOP No. 56 — Modeling (cross-cutting standard: intended use, assumptions, reliance, documentation).

Engine source: - ecosystem/InsModel/Models/firmmodel/engines/saa_engine.pySAAEngine v1.0.0 (insmodel.L4.saa_engine). - ecosystem/InsModel/Models/firmmodel/engines/asset_management_engine.pyAssetManagementEngine v2.0.0 (insmodel.L4.asset_management).

Test / snapshot: - ecosystem/InsModel/Models/tests/mrm/test_gold_tier0.pyTestGoldSAA, TestGoldAssetManagement. - ecosystem/InsModel/Models/scripts/model_snapshots.py M-115.

Internal: - Finding BV-032 — firm-financials path divergence (no 10-K reconciliation claimed). - Decision 028 — ratification tiering (Tier-3 requires no 2L ratification).


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-3 hand-authoring of full methodology depth: Methodology, Key Assumptions and Their Justification, Output Snapshot, Limitations and Known Gaps, Validation Packet, and References from saa_engine.py / asset_management_engine.py + bound A-NNN entries. Stub marker advanced to ``.
  • 2026-06-05 — doc currency stamp recorded (model_doc_stamps.yaml M-115, fingerprint 46c3a05364d4072b).
  • 2026-06-06 — code-grounded documentation-accuracy pass; card found stale vs the INV-024 SAA cap fix (PR #52, commit bda809a). Added missing Standards Coverage and Dependencies sections (gold-standard parity with M-001); corrected Methodology constraint 2 (engine now defaults bounds to ENGINE_CONTRACT caps, lines 449–453 — caps active by default, config-overridable); rewrote Limitation 1 (former "omits caps" claim now FALSE — caps applied by default, residual gap = generic not firm-calibrated); re-captured the Output Snapshot by re-running the deterministic scripts/model_snapshots.py M-115 (post-fix vector: FIXED_MATURITY ~0.86, POLICY_LOANS binds 0.10 cap; superseding the pre-fix 81% POLICY_LOANS table) — flagged that re-freezing this as the new gold baseline is gated for principal / 2L ratification; updated Validation Packet snapshot scalars and the per-category-cap row; noted gated/ratification items (gold re-freeze, frozen weight-vector regression, BV-032 back-test) under Limitations. No model outputs, back-test numbers, or validation results were fabricated; the snapshot numbers were obtained by deterministically re-running the no-live-data script.

2L Inventory Review

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.

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.


Ratification

Ratified — RAT-115-v1.0.0

Latest ratification on file: RAT-115-v1.0.0. Authored by 2L (mrm-peer-reviewer) per Decision 028 charter §5 Pattern A.