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.
Inputs, processing, outputs
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 categories —
FIXED_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):
- Budget — weights sum to 1 (equality).
- 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 theENGINE_CONTRACTreference 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 floorsFIXED_MATURITY ≥ 0.20,CASH_SHORT_TERM ≥ 0.02); a config-suppliedmax_allocation/min_allocationstill takes precedence (overridable). See Limitation 1 for the residual gap (generic reference caps, not firm-calibrated). - ALM duration matching —
target − tol ≤ w'd ≤ target + tol(default 7.0 ± 0.5 years), two inequality constraints against the per-category duration vectord. - Credit-quality floor — weighted-average numeric rating
w'r ≥ min_rating(default 4.33 ≡ BBB), using the_RATING_NUMERICAAA=7 … CCC=1 ladder. - Liquidity floor —
w'ℓ ≥ min_liquidity_ratio(default 0.10), whereℓis the per-category liquidity weight (CASH=1.0, FIXED_MATURITY=0.8, etc.). - 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 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
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, commitbda809a). The table below is the deterministically re-run point solution frompython 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 and Known Gaps
- 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; floorsFIXED_MATURITY ≥ 0.20,CASH_SHORT_TERM ≥ 0.02) when the run config omits them —_build_constraintsfalls back to the contract defaults (saa_engine.py lines 449–453), resolving INV-024 (PR #52, commitbda809a). 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-calibratedmax_allocation/min_allocation(which override the contract defaults). The snapshot allocation remains illustrative, not investable. - 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. - Expected returns are fallback spreads, not firm assumptions. With no
expected_return,beta, ordefault_expected_returnssupplied,μfalls back tor_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. - 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_LOANSduration is itself a default proxy (8.0 yr). - 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.
- AssetManagementEngine (fee economics) is a separate, lightly-coupled
path. The companion
asset_managementengine 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. - 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 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
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.py —
SAAEngine v1.0.0 (insmodel.L4.saa_engine).
- ecosystem/InsModel/Models/firmmodel/engines/asset_management_engine.py —
AssetManagementEngine v2.0.0 (insmodel.L4.asset_management).
Test / snapshot:
- ecosystem/InsModel/Models/tests/mrm/test_gold_tier0.py —
TestGoldSAA, 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
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.yamlM-115, fingerprint46c3a05364d4072b). - 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 toENGINE_CONTRACTcaps, 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 deterministicscripts/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.
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.
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.