Tier 2 · Internal Risk Management

Structured Product Pricing — Constant-CPR Prepay / Spread-to-Curve Family

M-134 · lifecycle: monitoring · RAT-134-v1.0.1

Intended Use

Structured Product Pricing — Constant-CPR Prepay / Spread-to-Curve Family Price MBS, ABS, and other amortizing structured products with a constant/bucketed-CPR prepayment model, weighted-average-life calculation, and a spread-to-curve (parallel-OAS) discounting framework.

Family-level model covering structured-product pricing: constant / bucketed Conditional Prepayment Rate (CPR -> SMM) prepayment modeling, a deterministic amortizing cash-flow waterfall, weighted-average life (WAL), durations / convexity / spread-duration / key-rate DV01, and a spread-to-curve option-adjusted spread (parallel zero-curve shift) for mortgage-backed and asset-backed securities. NOTE (COND-001, RAT-134): a stochastic-rate lattice / binomial / trinomial tree, backward-induction / Monte-Carlo OAS, and a PSA ramp (100/PSA, refinancing-incentive, burnout, seasoning) are NOT implemented; embedded optionality enters only through the supplied CPR and the parallel-OAS shift. Tracked as a capability gap under INV-007. Implementations: FinView L1 (structured) + Gold Copy L1 (mortgage challenger).


Components

Inputs, processing, outputs

data sources
DS-001 · DS-007 · DS-031 · DS-059
assumptions
A-090, A-091, A-207, A-220, A-221, A-222, A-223, A-280
engines
finview.L1.structured_pricer
goldcopy.L1.mortgage_pricer
contracts
structured_pricing_v1
upstream
M-137
dimensions
D2

Methodology & Mechanics

Methodology

M-134 prices amortizing, prepayable fixed-income structured products — agency and non-agency MBS pass-throughs, ABS, and CMBS — by projecting a deterministic cash-flow waterfall under a constant prepayment speed and discounting it off a zero curve with an option-adjusted spread. The engine is FinView-resident: StructuredBondPricer (finview.L1.structured_pricer, finlib/structured_pricer.py, tier-2-gate, owner engine-owner-pricing) built on the cash-flow generator BondCashflows (finview.L0.bond_cashflows, finlib/bond_cashflows.py, tier-3-infra). Its source lives in [redacted], not in InsModel; the insurance stack consumes it cross-repo via sys.path insertion, exactly as the M-040 CECL snapshot does.

1. Prepayment model (CPR → SMM). Prepayment is the load-bearing economic input. The model takes an annual Conditional Prepayment Rate (CPR) and converts it to a per-period Single Monthly Mortality (SMM) via the standard identity SMM = 1 − (1 − CPR)^(1/freq) (BondCashflows.generate_schedule). At a 6% CPR with monthly pay, SMM ≈ 0.5143% per month. CPR can be supplied directly (scenario pricing), or recovered from a market price by compute_implied_cpr, which solves brentq on [0.001, 0.60] for the CPR that reprices the bond. A batch service (data_layer/services/cpr_fitting.py) fits market-implied CPRs across the structured universe and aggregates them into a CPR surface keyed by (agency, coupon bucket, seasoning bucket) — a calibrated, not-prescribed, empirical prepayment grid.

2. Cash-flow waterfall. For each period BondCashflows computes, on the beginning balance: coupon (balance × coupon/freq), scheduled principal (bullet, level-pay, or passthrough level-payment amortization), then a prepayment of SMM × (balance − scheduled_principal), an optional default overlay (hazard rate → period default prob, with recovery), and the resulting ending balance. Total principal = scheduled + prepaid; this is what shortens the bond. The waterfall is flat — a single pool/tranche, not a multi-tranche sequential-pay or PAC/support structure (see Limitations).

3. Pricing and OAS. price_from_curve interpolates the zero curve to each cash-flow date, adds the OAS (oas_bps/10000) as a parallel shift to the zero rates, discounts continuously (exp(−(z+oas)·t)), and sums. compute_oas inverts this — brentq on [−500, 2000] bps for the spread that matches a target market price. Because optionality here is captured only through the deterministic prepay speed (not a stochastic-rate lattice), this is an OAS in the spread-to-curve sense rather than a Monte-Carlo / lattice option-adjusted spread.

On floating-rate coupons. Beyond the fixed-coupon path described above, the engine also supports floating-rate cash flows: BondCashflows.generate_schedule accepts float_spread (decimal, e.g. 0.035 for L+350) plus a sofr_forwards array, and StructuredBondPricer.__init__ forwards both through. When a float_spread is supplied, each period's all-in coupon is computed as fwd[i] + float_spread (the period forward rate plus the spread) rather than a static coupon — the implemented capability for floating-rate ABS/loan cash flows. The deterministic Output Snapshot below exercises the fixed-coupon path only.

4. Average life and risk. compute_wal returns the principal-weighted average life Σ tᵢ·Pᵢ / Σ Pᵢ. compute_duration returns Macaulay, modified, effective (yield ±10bp finite difference on the cash-flow-yield repricing), and convexity. compute_spread_duration bumps OAS ±1bp — the key risk metric for spread product. compute_key_rate_dv01 produces triangle-weighted key-rate DV01s across standard tenors. YTM is the cash-flow yield solved by compute_ytm.

On "lattice." The model family is named "prepay / lattice." FinView implements the prepay half rigorously. It does not contain a lattice / binomial / trinomial interest-rate tree for embedded options; there is no Tree/lattice class in the repo. Embedded optionality (the homeowner's call) enters through the prepay speed and the OAS, not through backward induction on a rate tree. This is stated plainly in Limitations and is the principal honesty caveat of this card.


Key Assumptions

Key Assumptions and Their Justification

Assumption Canonical value Derivation Justification
Prepayment speed (CPR) 6% annual constant scenario / market-implied Constant-CPR is the simplest defensible prepay convention; 6% is a representative seasoned agency-conventional speed. Round-trips exactly via compute_implied_cpr (recovered 0.060000). Production uses the empirically-fitted CPR surface, not a flat number.
Prepay→SMM conversion SMM = 1−(1−CPR)^(1/freq) closed form Industry-standard CPR/SMM identity; preserves the annualized prepay fraction under monthly compounding. SMM ≈ 0.5143%/mo at 6% CPR.
Discount curve flat 5% continuously-compounded zero, 10 tenors scenario A flat curve isolates prepay/structure effects from curve shape and makes the snapshot fully reproducible with no live data. Production consumes the calibrated Treasury curve.
OAS 0 bps (snapshot) round-trip self-consistency Snapshot prices at OAS=0 then re-solves compute_oas back to that price, recovering ≈0 bps — a solver self-consistency check, not a market spread. Production overlays live OAS by rating/tier.
Amortization passthrough (level-payment) product term Pass-through is the canonical agency-MBS structure; BondCashflows also supports bullet (CMBS) and level (ABS).
Default overlay off (hazard = 0) product term Agency MBS carries a credit guarantee; default/recovery is exercised for non-agency ABS/CMBS via the hazard-rate overlay but is zero here.

Operational notes. Government/agency ratings (AA+, AAA) are assigned near-zero OAS by the fitting service; only non-agency tranches carry a rating-derived spread. The cash-flow generator assumes uniform payment intervals with no day-count adjustment and treats recovery at period end.


Output Snapshot

Output Snapshot

Deterministic run of StructuredBondPricer on the canonical 30-year agency MBS pass-through — reproducible, requires no live firm data (BV-032 immune; no FRED/iShares/live-OAS touched). Reproduce with python scripts/model_snapshots.py M-134 (from InsModel Models/, which inserts the FinView repo on sys.path); the engine behavior is asserted by tests/test_structured_pricer.py in FinView (16 tests, all passing).

Input: face $100,000 · 5.00% coupon · 30y · monthly pay · AA+ agency · passthrough amortization · 6% CPR · flat 5.00% zero curve · OAS 0 bps.

output value meaning
cpr_annual 0.0600 supplied prepay speed (annual CPR)
smm_monthly 0.005143 per-month prepay fraction = 1−(1−CPR)^(1/12)
clean_price 99,932.60 PV of waterfall (99.93% of par)
ytm 0.050104 cash-flow yield (≈ curve + price discount)
oas_bps_recovered −0.0000 round-trip OAS back to the OAS=0 price (solver self-consistency)
implied_cpr_recovered 0.060000 CPR recovered from the model price (round-trip)
wal_years 8.59 weighted-average life — well below the 30y legal final
effective_duration 6.46 yield ±10bp finite-difference duration
modified_duration 6.46 analytic modified duration
convexity 67.34 second-order price sensitivity
spread_duration 6.49 OAS ±1bp price sensitivity (key spread-risk metric)

The economics are correct and self-consistent. A 5% pass-through priced off a 5% flat curve sits just below par (99.93%) because the slight discount reflects the finite prepay-shortened cash-flow profile; YTM ≈ 5.01% accordingly. The 6% CPR collapses a 30-year legal final to an 8.59-year WAL, and the effective duration (6.46) is materially shorter than a comparable 30-year bullet — exactly the prepayment-driven shortening that the test suite asserts (test_mbs_wal_less_than_maturity, test_mbs_duration_less_than_bullet, test_higher_cpr_shorter_wal). The implied-CPR and OAS round-trips both recover their inputs to solver tolerance, validating the inverse solvers.

Captured 2026-06-04 · deterministic, no live data.


Limitations

Limitations and Known Gaps

  1. No lattice / no stochastic-rate option model. Despite the family name ("prepay / lattice"), FinView contains no binomial/trinomial interest-rate tree and no backward-induction engine for embedded options. There is no Tree/lattice class in the repo. Embedded optionality (the prepayment call) is captured only through a deterministic CPR and the OAS shift — the OAS is a spread-to-curve, not a Monte-Carlo / lattice option-adjusted spread. This is the single largest gap versus a production MBS desk; a true OAS would require a path-simulated short rate driving a rate-dependent prepay function.

  2. Constant CPR, not a prepay function of rates/incentive. The model takes a single CPR (or a bucketed surface), not a refinancing-incentive curve, burnout, or seasoning ramp (PSA-style). PSA is not implemented — there is no PSA multiplier or 100/PSA ramp class; only flat CPR/SMM. Prepay does not respond to the rate path within a valuation, so negative convexity is understated.

  3. Single flat pool — no tranching. BondCashflows projects one balance. There is no sequential-pay, PAC/support, or senior/sub waterfall; CMBS/ABS capital-structure cash-flow allocation across tranches is out of scope. The bond is modeled as a whole pool.

  4. OAS is a parallel zero-curve shift. The spread is added uniformly to all interpolated zero rates; there is no term-structure-of-spreads or tranche-specific spread vector.

  5. No day-count / calendar conventions. BondCashflows assumes uniform payment intervals (1/freq year) with no 30/360 or Act/Act adjustment and no settlement/accrued-interest handling (dirty_price == clean_price, accrued_interest = 0).

  6. Cross-repo residency (BV-032 context). The engine is FinView-resident and consumed by the insurance stack only via sys.path insertion. There is no InsModel-native MBS engine; any InsModel consumer inherits FinView's release cadence and venv. Both venvs (FinView/.venv and InsModel/Models/.venv) reproduce the snapshot identically (numpy/scipy present in both).

  7. No 10-K / firm-disclosure grounding. This card is grounded in code and the deterministic snapshot only. It makes no claim about any issuer's actual disclosed prepayment experience, pool composition, or holdings.

Tracked for ratification (not applied in this documentation pass). The following are output-changing / modeling-code items left for ratification, noted here for transparency and tracked in the 2L register: a true lattice / Monte-Carlo OAS engine (path-simulated short rate → rate-dependent prepay), a PSA / refinancing-incentive prepay function (burnout, seasoning ramp, negative convexity), and tranche / waterfall support (sequential-pay, PAC/support, senior/sub) — all under INV-007 (capability gap, Limitations 1–3); the missing Tier-2 validation evidence (sensitivity suite, agency-MBS frozen golden cross-check CI gate, recorded challenger run vs goldcopy.L1.mortgage_pricer, annual backtest needing DS-059 pool data — see Validation Packet); and a packaged / versioned cross-repo interface plus CI drift gate for the FinView-resident pricing library consumed by InsModel via sys.path under INV-032 (cross-repo drift gate, Limitation 6, shared with M-040 / M-130 / M-131).


Validation Evidence

Validation Packet

Check Where What it proves
Engine test suite (16 tests, all pass) FinView/tests/test_structured_pricer.py Price > 0; WAL < maturity (8–12y band); higher CPR → shorter WAL; premium MBS faster prepay → lower price; MBS duration < bullet; CMBS bullet @CPR=0 matches BondPricer within $1; YTM round-trip < $0.01.
Implied-CPR round-trips test_implied_cpr_roundtrip_*, _with_oas, _monotonicity, _boundary_low, _impossible_price compute_implied_cpr recovers the input CPR to <0.001–0.002; monotone in price; raises on unachievable prices. Snapshot recovers 0.060000.
OAS solver self-consistency snapshot oas_bps_recovered = −0.0000 compute_oas inverts price_from_curve back to ≈0 bps against its own curve.
Cross-repo reproducibility snapshot run under both venvs Identical output under FinView/.venv and ecosystem/InsModel/Models/.venv.
Independent challenger (gold_copy) goldcopy.L1.mortgage_pricerecosystem/gold_copy/instruments/mortgage_pricer.py (CommercialMortgagePricer, tier-2-gate, owner engine-owner-validation, validates: finview.L1.structured_pricer) A separate-implementation challenger pricer (amortization schedule, OAS, effective duration, LTV-driven default probability) exists in the gold-copy repo as the cross-check leg.
Registry binding insightalm/modelling/engine_registry.yaml finview.L1.structured_pricer (tier-2-gate, model_membership: [M-134]), finview.L0.bond_cashflows (tier-3-infra), finview.L2.structured_analytics (portfolio analytics).

Tier-2 evidence gaps (MRM §10.2). The checks above prove engine mechanics, round-trip solver self-consistency, and that an independent challenger implementation exists; per the MRM §10.2 Tier-2 validation table they do not yet discharge the full Tier-2 expectation. The following are honest pending items (no result is asserted or fabricated):

  • Sensitivity suite on the top-3 assumptions — DELIVERED (COND-002). A runnable directional shock suite on CPR (prepay speed), the discount-curve level, and OAS lives at modelling/validation_evidence/M-134/v1.0.0/sensitivity_suite.py (exit 0, run 2026-06-06): higher CPR → shorter WAL (monotone) and lower premium-MBS price; higher curve level → lower price (monotone); wider OAS → lower price; spread_duration > 0; OAS solver round-trips 75bp. Re-confirms test_higher_cpr_shorter_wal / test_higher_cpr_lower_price_premium / test_implied_cpr_monotonicity.
  • Challenger-comparison run — DELIVERED (COND-002). A recorded side-by-side run vs goldcopy.L1.mortgage_pricer lives at modelling/validation_evidence/M-134/v1.0.0/reconcile_finview_vs_goldcopy.py (exit 0, run 2026-06-06): 5 prepay-free overlap cases (CPR=0 level-pay + bullet/IO-balloon), max relative price diff 1.98e-14, within the 0.5% registry tolerance. Honest residual: the agreement is exact only on the prepay-free overlap — the challenger is a commercial-mortgage pricer with no CPR, so the CPR-prepay leg has no independent prepay challenger (validated by the unit suite + CPR sensitivity instead). 2L peer sign-off pending re-review.
  • Annual backtest vs realized MBS price / prepay — PENDING (honest). No comparison of modeled price/WAL/CPR against realized pool experience exists. Needs DS-059 pool-level prepay/price data (status planned, registration-gated, not on disk); the backtest cannot be run or scored — and A-280 cannot be calibrated off default-PSA-100 — until that source lands. No surrogate number is fabricated; the model is kept scoped with the default-PSA-placeholder caveat (RAT-134 COND-002 option (b)).

Other open: the challenger validates a commercial-mortgage pricer against the structured pricer; a frozen golden cross-check tolerance band for the agency-MBS case is not yet a CI gate, and no lattice-OAS reference exists to validate Limitation 1.


References

References

Engine / code (source of truth): - FinView/finlib/structured_pricer.pyStructuredBondPricer (finview.L1.structured_pricer): pricing, OAS, WAL, durations, implied CPR. - FinView/finlib/bond_cashflows.pyBondCashflows (finview.L0.bond_cashflows): waterfall, CPR→SMM, default overlay, WAL. - FinView/data_layer/services/cpr_fitting.py — market-implied CPR fitting and CPR-surface aggregation. - FinView/backend/services/structured_analytics_service.py — portfolio-level WAL / spread-duration / prepay-stress analytics. - FinView/tests/test_structured_pricer.py — 16-test engine suite (referenced by Output Snapshot). - ecosystem/gold_copy/instruments/mortgage_pricer.pyCommercialMortgagePricer (goldcopy.L1.mortgage_pricer), independent challenger.

Methodology literature: - Fabozzi, F. J. — Handbook of Mortgage-Backed Securities (CPR/SMM, WAL, PSA convention, OAS framework). - Hayre, L. (ed.) — Salomon Smith Barney Guide to Mortgage-Backed and Asset-Backed Securities (prepayment modeling, refinancing incentive, burnout). - Davidson & Levin — Mortgage Valuation Models: Embedded Options, Risk, and Uncertainty (option-adjusted spread, lattice/Monte-Carlo OAS — the methodology M-134 does not yet implement, per Limitation 1). - SIFMA / PSA Standard Prepayment Model — the 100% PSA ramp benchmark (referenced for context; PSA is not implemented, per Limitation 2).

Internal: - Engine registry: insightalm/modelling/engine_registry.yaml (entries finview.L1.structured_pricer, finview.L0.bond_cashflows, finview.L2.structured_analytics, goldcopy.L1.mortgage_pricer). - Snapshot harness: ecosystem/InsModel/Models/scripts/model_snapshots.py (snap_M_134). - BV-032 (firm-data divergence) — snapshot is deterministic and firm-data-free, hence immune. - 2L register findings (tracked backlog): INV-007 — capability gap (lattice / MC-OAS, PSA prepay function, tranche/waterfall support; see Limitations 1–3 and the Tier-2 evidence gaps); INV-032 — cross-repo drift gate (packaged / versioned FinView pricing-library interface + CI drift gate for the sys.path consumption path; see Limitation 6, shared with M-040 / M-130 / M-131).


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 the FinView engine source (structured_pricer.py / bond_cashflows.py / cpr_fitting.py) + the deterministic snapshot (scripts/model_snapshots.py M-134, captured 2026-06-04). Stub marker advanced from to. No model outputs fabricated; OAS / implied-CPR round-trips and snapshot values are reproduced from the engine.
  • 2026-06-06 — code-grounded documentation-accuracy pass against the FinView engine source. Added the floating-rate-coupon capability note to Methodology (float_spread + sofr_forwards in BondCashflows.generate_schedule / StructuredBondPricer.__init__); framed the Validation Packet's open items against the MRM §10.2 Tier-2 table as explicit honest pending lines (sensitivity suite on CPR/curve/OAS; annual backtest needing DS-059 pool data; recorded challenger run + peer sign-off vs goldcopy.L1.mortgage_pricer); cross-referenced 2L register findings INV-007 (capability gap) and INV-032 (cross-repo drift gate) under Limitations + References; consolidated gated modeling-code items (lattice/MC-OAS, PSA prepay, tranching, Tier-2 evidence, cross-repo interface) into a transparency note. No model outputs, validation results, or back-test numbers were fabricated or changed.
  • 2026-06-06 (RAT-134-v1.0.0 remediation, 1L)COND-001: corrected the title, Intended Use, and Description (and the registry name/intended_use/description) from the lattice/Monte-Carlo/PSA overclaim to the implemented constant-CPR prepay + spread-to-curve methodology, with the lattice/MC/PSA absence stated explicitly and tracked under INV-007 (no engine change — documentation-accuracy correction, RAT option (a)). COND-002: assembled the Tier-2 validation-evidence pack at modelling/validation_evidence/M-134/v1.0.0/ — a runnable CPR/curve/OAS sensitivity suite (sensitivity_suite.py, exit 0) and a recorded finview-vs-goldcopy challenger reconciliation (reconcile_finview_vs_goldcopy.py, max rel price diff 1.98e-14 within the 0.5% registry tolerance, exit 0); the annual realized-prepay/price back-test + A-280 PSA calibration remain honest pending behind DS-059 (planned/registration-gated/not-on-disk), with the default-PSA-placeholder caveat retained. Reconciled the registry documentation_pack (model_card / validation_evidence / change_log → present, LG-134-01) and stamped lifecycle.last_validated_on = 2026-06-06 (LG-134-02). No model outputs, validation results, or back-test numbers were fabricated.

2L Inventory Review

Open findings (3)

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.

MEDIUM INV-007 · P1 · capability-gap

'Lattice/prepay' structured pricer has no lattice, no PSA; 'OAS' is a Z-spread

Despite the "prepay / lattice" family name, there is no binomial/trinomial tree or backward-induction; embedded optionality enters only through a deterministic flat CPR and a parallel spread. The "OAS" is a parallel spread-to-zero-curve (a Z-spread), not a Monte-Carlo/lattice option-adjusted spread. No PSA ramp; prepay does not respond to the rate path, so negative convexity is understated. Single flat pool — no tranching.

Recommendation: Add (1) a PSA/refi-incentive prepay function so CPR responds to rates, (2) a Monte-Carlo or lattice OAS engine for true option-adjusted spread, (3) tranche/waterfall support for structured deals. Relabel the current "OAS" as Z-spread until MC-OAS exists.

LOW INV-032 · cross-repo · cross-repo

FinView-resident pricing/CECL engines reached cross-repo with no drift gate

M-040 (CECL) and M-130-137 (asset pricing) are FinView-resident, consumed by InsModel only via sys.path insertion. No packaged interface or drift gate — a finlib signature change silently breaks the InsModel snapshot harness / consumers.

Recommendation: Define a packaged interface (or a versioned contract) for the FinView pricing library and a CI drift gate, so cross-repo consumers fail loudly on signature change. Tie to D041 ownership.


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 sr_26_2 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 attested

Ratification

Ratified — RAT-134-v1.0.1

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