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).
Inputs, processing, outputs
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 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
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 and Known Gaps
-
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/latticeclass 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. -
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.
-
Single flat pool — no tranching.
BondCashflowsprojects 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. -
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.
-
No day-count / calendar conventions.
BondCashflowsassumes uniform payment intervals (1/freqyear) with no 30/360 or Act/Act adjustment and no settlement/accrued-interest handling (dirty_price == clean_price,accrued_interest = 0). -
Cross-repo residency (BV-032 context). The engine is FinView-resident and consumed by the insurance stack only via
sys.pathinsertion. There is no InsModel-native MBS engine; any InsModel consumer inherits FinView's release cadence and venv. Both venvs (FinView/.venvandInsModel/Models/.venv) reproduce the snapshot identically (numpy/scipy present in both). -
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 viasys.pathunder INV-032 (cross-repo drift gate, Limitation 6, shared with M-040 / M-130 / M-131).
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_pricer — ecosystem/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-confirmstest_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_pricerlives atmodelling/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
Engine / code (source of truth):
- FinView/finlib/structured_pricer.py — StructuredBondPricer (finview.L1.structured_pricer): pricing, OAS, WAL, durations, implied CPR.
- FinView/finlib/bond_cashflows.py — BondCashflows (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.py — CommercialMortgagePricer (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
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 fromto. 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_forwardsinBondCashflows.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 vsgoldcopy.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 registrydocumentation_pack(model_card / validation_evidence / change_log → present, LG-134-01) and stampedlifecycle.last_validated_on = 2026-06-06(LG-134-02). No model outputs, validation results, or back-test numbers were fabricated.
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.
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.
'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.
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.
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-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.