Tier 2 · Internal Risk Management

Curve Construction — No-Arbitrage Bootstrap / Multi-Curve Family

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

Intended Use

Curve Construction — No-Arbitrage Bootstrap / Multi-Curve Family Construct discount, projection, and credit-spread curves via no-arbitrage bootstrapping and multi-curve framework for downstream pricers.

Family-level model covering deterministic no-arbitrage bootstrapping of discount factors and zero rates from par yields (Treasury, OIS- SOFR, credit spreads), and the multi-curve framework that supplies separate curves for discounting vs projection. Output curves consumed by all L1 pricer families (M-130..M-135). Implementations: 2 FinView L0 engines.


Components

Inputs, processing, outputs

data sources
DS-001 · DS-002 · DS-003 · DS-005 · DS-009 · DS-010 · DS-013 · DS-031 · DS-061
assumptions
A-053
engines
finview.L0.curve_builder
finview.L0.curve_set
contracts
yield_curve_v1
dimensions
D1 · D2

Methodology & Mechanics

Methodology

M-137 is the curve-construction foundation of the FinView pricing stack. It takes a set of market instruments quoted as par rates — money-market deposits / Treasury bills at the short end (≤ 1Y) and par swaps / par bonds at the long end (> 1Y) — and produces a self-consistent term structure of discount factors, continuously-compounded zero rates, and forward rates that every downstream pricer consumes. The engine family is FinView-resident (finlib/curve_builder.py); the InsightALM model registry binds it as M-137 via three engine entries: finview.L0.curve_builder (the bootstrap), finview.L0.curve_set (the multi-curve container), and finview.L0.interpolation (the log-linear / PCHIP interpolators).

Sequential bootstrap. bootstrap_sofr_curve strips discount factors one tenor at a time via an initial forward pass (each node sees only earlier nodes), exploiting the par condition that a par instrument prices to 1.0:

  • Short end (T ≤ 1Y) — treated as a single-payment money-market instrument: DF(T) = 1 / (1 + c·T), where c is the par (simple) rate. No coupon stripping is required because there is one cash flow.
  • Long end (T > 1Y) — treated as a semi-annual par coupon bond. The par condition 1 = Σ (c/2)·DF(t_j) + DF(T) is solved for the unknown terminal DF(T). The PV of the prior coupons (t_j < T) is evaluated against the discount factors already bootstrapped at earlier tenors, with intermediate coupon-date factors filled in by log-linear interpolation of discount factors (_interpolate_df). Rearranging gives DF(T) = (1 − PV_prior_coupons) / (1 + c/2).

Self-consistency refinement and self-check (post-INV-028, #92). The engine now contains two consistency facilities, both implemented but defaulted OFF:

  • refine_passes (default 0) — after the forward pass, runs N additional full-curve sweeps that re-solve every coupon node against the complete current curve (rather than only earlier nodes), driving the bootstrap to reprice its own calibrating instruments to par. With refinement on the long-end residual collapses toward ~0 (see Output Snapshot).
  • self_check_tol_bps (default None) — when set, the engine calls reprice_residuals() after building and emits a logger.warning if the max reprice residual exceeds the tolerance. The self-check is therefore implemented but not yet enabled-by-default; it warns, it does not raise.

Both knobs default OFF deliberately, to preserve the frozen gold curves downstream pricers calibrate against — so default/gold output (refine_passes=0, self_check_tol_bps=None) still carries the long-end residual documented below. Turning either on by default changes every downstream gold curve and is held for ratification (INV-028 follow-up; see Limitations note).

Zero and forward rates. Once the discount-factor curve is built, zero rates are recovered by inverting the continuous-compounding relation z(T) = −ln DF(T) / T, and instantaneous-ish forward rates between adjacent grid tenors are recovered from the discount-factor ratio f(T_{i-1}→T_i) = (DF_{i-1}/DF_i − 1) / Δt. The first forward is pinned to the first zero.

Interpolation scheme. Discount factors are interpolated log-linearly (linear in ln DF, equivalent to piecewise-constant forward rates between knots) — the workhorse used inside the bootstrap itself. For off-grid zero-rate queries, interpolate_curve offers either plain linear interpolation (default, backward-compatible) or PCHIP (monotone cubic Hermite, the Hagan–West "monotone-convex"-family choice that suppresses the spurious forward oscillation linear schemes can produce). PCHIP is also available for the discount-factor interpolation path.

Multi-curve framework. Post-2008 / post-LIBOR pricing requires separating the discounting curve (OIS / SOFR) from the projection curve (the forward-rate curve for the floating leg) and from government (Treasury) benchmarks. build_sofr_curve bootstraps an OIS/SOFR discount curve from a SOFR overnight rate plus a dictionary of par swap rates (2Y…30Y), with optional short-end calibration from 90-day / 180-day SOFR averages. The CurveSet dataclass is the container that holds Treasury, discount (OIS/SOFR), and projection curves side by side, plus a rating-keyed dictionary of credit spreads. Its get_discount_dfs / get_projection_fwds accessors fall back to the Treasury curve when an OIS/projection curve is absent, so single-curve and multi-curve callers share one interface.

Where it sits in the stack. M-137 is the Tier-2 gate that the FinView derivative pricers stand on: M-130 (bond pricer), M-132 (interest-rate swap pricer — its swap_terms input is paired with a curve_set), M-133 (swaption pricer), and M-135 (SOFR-futures pricer) all consume the curves M-137 produces. If the curve is wrong, every price above it is wrong — which is why this card is the foundation the others reference. The full dependent set (6 models: M-130–M-135) is enumerated in the Dependencies section above.


Key Assumptions

Key Assumptions and Their Justification

Assumption Value / Choice Justification
Calibrating instruments Deposits/bills (≤ 1Y) + semi-annual par swaps or par bonds (> 1Y), each input as a par rate Par instruments are the liquid, market-quoted points; the par condition (price = 1.0) is what makes the sequential bootstrap closed-form and exact at each knot.
Coupon frequency (> 1Y) Semi-annual (c/2 every 0.5y) Hard-coded convention matching USD swap / Treasury market practice; documented in the CurveBuilderGovernance docstring.
Compounding (zero rates) Continuous (z = −ln DF / T) Standard for analytic curve representation; makes zero/forward algebra additive and is the form the downstream pricers expect.
Discount-factor interpolation Log-linear (linear in ln DF) by default; PCHIP optional Log-linear ⇒ piecewise-constant forwards (no negative forwards on a well-behaved curve); PCHIP (Hagan–West family) is the smooth, non-oscillatory upgrade when forward smoothness matters.
Day-count convention Uniform actual/actual assumed — tenors are passed as decimal years; no explicit business-day / day-count calendar Acknowledged simplification (engine docstring: "uniform day-count convention (actual/actual assumed)"). Adequate for the synthetic / benchmark curves this card exercises; see Limitation 2.
Futures convexity None The bootstrap consumes par swap/deposit rates, not futures prices; no convexity adjustment is applied. Documented limitation in the engine docstring; see Limitation 3.
SOFR–Treasury basis Additive parallel spread in bps (sofr_treasury_spread_bps), default 0 A first-order basis adjustment; lowers all discount factors uniformly (verified by test_spread_adjustment). A true OIS/SOFR curve is built separately via build_sofr_curve rather than spreading Treasury.

Prose. The model is deliberately a par-rate bootstrapper, not a global least-squares / spline-fit calibrator. The advantage is that every grid knot reprices its calibrating instrument exactly (residual ≈ 0 at the knot — see Output Snapshot), the method is deterministic and order-stable, and there are no optimizer convergence concerns. The cost is that accuracy between widely-spaced knots depends entirely on the interpolation scheme: coupon cash flows that fall between sparse long-end tenors are priced off log-linearly interpolated discount factors, which is where the only material reprice error appears (Limitation 1).


Output Snapshot

Output Snapshot

Deterministic curve construction from a synthetic par-instrument set — reproducible, requires no live market data (BV-032 immune: the input is a hand-specified upward-sloping par curve, not a market pull). Reproduce with python scripts/model_snapshots.py M-137; the underlying engine behaviour is asserted by FinView tests/test_curve_builder.py (12 tests).

Input: Treasury par curve, tenors [0.25, 0.5, 1, 2, 3, 5, 7, 10, 20, 30]y, par yields 4.30% → 5.30% (upward-sloping; mirrors the upward_curve test fixture). OIS/SOFR overlay: SOFR overnight 4.30% + par swaps 2Y 4.10% / 5Y 4.20% / 10Y 4.30% / 30Y 4.50%.

output value meaning
DF(1y) 0.9569 1-year discount factor (Treasury)
DF(5y) 0.7882 5-year discount factor
DF(10y) 0.6077 10-year discount factor
DF(30y) 0.2005 30-year discount factor
zero(10y) 4.9801% continuously-compounded 10-year zero rate
zero(30y) 5.3564% 30-year zero rate (curve is upward-sloping, as expected)
fwd(1y→2y) 4.8174% forward rate over the 1→2y window
fwd(7y→10y) 5.6812% forward rate over the 7→10y window
has_multi_curve True CurveSet holds a distinct OIS/SOFR discount curve
SOFR_DF(10y) 0.6523 OIS/SOFR 10-year discount factor (> Treasury DF — lower SOFR curve)
TSY−SOFR DF spread @10y −445.37 bps OIS/SOFR discounts less steeply here (SOFR ~4.3% flat vs Treasury 5.0% @10y)
max reprice residual (default forward pass) 66.10 price-bps worst calibrating-instrument reprice error (par = 1.0), at the 20y instrument; reproduced 2026-06-06 via python scripts/model_snapshots.py M-137 (refine_passes=0 default)
max reprice residual (refinement on — available but OFF) ~0.14 price-bps same fixture with refine_passes=6; the available-but-off improvement, held for ratification (INV-028 follow-up) — not the default/gold output
instruments calibrated 10 all 10 par instruments restruck off the bootstrapped curve

Reprice residual is the validation that matters. A correct bootstrap must reprice its own calibrating instruments to par. Per-tenor residuals are effectively zero at the short end and small through 10y (0.00 bps at ≤1y, 0.33 bps at 2y, 1.55 bps at 5y, 4.62 bps at 10y) and then balloon at the sparse long end (66.10 bps at 20y, 12.69 bps at 30y). This is not a bootstrap bug — the terminal DF(T) knot itself is solved exactly; the residual comes from re-pricing the coupon cash flows that fall on the 10y→20y→30y gaps, where log-linear discount-factor interpolation over a 10-year window is a coarse approximation. Tightening the long-end tenor grid (adding 15y/25y knots), switching the coupon-date fill to PCHIP, or running the engine's refine_passes self-consistency loop collapses this residual — with refine_passes=6 on the same fixture the max residual drops to ~0.14 bps. Refinement is off by default to preserve frozen gold curves, so the held/default figure remains 66.10 bps; default-on enablement + gold re-freeze is the INV-028 follow-up held for ratification (see Limitation 1).

The off-grid zero-rate interpolation is well-behaved: at 4y/8y/15y the log-linear and PCHIP zeros agree to within ~3 bps (e.g. 15y: 5.1342% log-linear vs 5.1636% PCHIP), confirming the curve is smooth enough that interpolation choice is second-order for rate queries.

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


Limitations

Limitations and Known Gaps

  1. Long-end reprice error under a sparse tenor grid. With knots at 10y/20y/30y, coupon cash flows on the 20y and 30y par bonds are discounted off log-linearly interpolated DFs across 10-year gaps, producing a worst-case ~66 bps reprice residual at 20y (vs < 5 bps through 10y) on the default forward pass. The terminal knot is exact; the residual is an interpolation-gap artifact. Mitigation: denser long-end knots, PCHIP coupon-date interpolation, or the engine's own refine_passes self-consistency loop (post-INV-028, #92) which collapses the residual toward ~0 when enabled. A calibration self-check (reprice_residuals + self_check_tol_bps warning path) is implemented but not yet enabled-by-default — it warns rather than fails, and refinement is off by default to preserve frozen gold curves (see Methodology and Validation Packet). Enablement-by-default + gold re-freeze is tracked as the INV-028 follow-up (held for ratification, see note below).
  2. Uniform day-count / no calendar. Tenors are decimal years; there is no business-day adjustment, holiday calendar, or per-leg day-count basis (actual/360 for SOFR floating, 30/360 for fixed, etc.). The engine docstring states "uniform day-count convention (actual/actual assumed)." This biases accrual-sensitive prices for real-dated instruments and is acceptable only for benchmark / synthetic curves, not for trade-date production marking.
  3. No futures convexity adjustment. The bootstrap calibrates to par deposit/swap rates, not futures prices. A futures-based short end would need a convexity correction (the engine docstring flags this explicitly); it is absent. M-135 (SOFR-futures pricer) consumes the curve but the curve is not itself futures-calibrated.
  4. Additive parallel SOFR–Treasury basis is first-order. The sofr_treasury_spread_bps path applies a flat parallel spread to all par rates; a real SOFR–Treasury basis is tenor-dependent. The richer path (build_sofr_curve) bootstraps OIS/SOFR independently, which is preferred — the flat-spread option remains a convenience approximation.
  5. No global no-arbitrage / monotone-convex guarantee. Log-linear DF interpolation gives piecewise-constant (non-smooth) forwards; PCHIP is monotone but not the full Hagan–West "monotone convex" forward interpolator. Neither path enforces a hard non-negative-forward constraint at calibration time — a steeply inverted input curve could in principle yield a negative interpolated forward without raising an error.
  6. Cross-repo ownership / change-control (FinView-resident). The engine lives in [redacted], outside the InsightALM governance repos. The snapshot inserts the FinView source tree on sys.path to run. Change-control binding between the InsightALM model registry (M-137) and the FinView source is by registry path reference only; there is no automated drift gate that fails if the FinView signature changes — should be tracked as a governance issue, same class as the M-040 cross-repo finding.
  7. Aspirational docstring line — no wired Bloomberg validation. The CurveBuilderGovernance engine docstring states "Validation: cross-check bootstrapped zeros against Bloomberg SOFR curve." This is an aspirational docstring line, not a performed validation: the engine and the snapshot are numpy-only and have no Bloomberg dependency. The actual implemented self-consistency check is reprice_residuals() against the model's own calibrating instruments (Validation Packet). Noted here so the docstring is not read as implying a market-data cross-check that does not exist.

Tracked for ratification (not applied in this documentation pass). The following are output-changing / modeling-code items left for ratification: (a) INV-028 follow-up — enabling refine_passes and/or self_check_tol_bps by default in bootstrap_sofr_curve / build_sofr_curve and re-freezing the gold curves (refinement-on changes every downstream gold curve and the M-137 snapshot); (b) INV-032 — a packaged/versioned interface + CI drift gate for the FinView-resident finlib library consumed cross-repo by InsModel via sys.path (Limitation 6); (c) a hard non-negative-forward / monotone-convex calibration-time constraint (Limitation 5); (d) day-count/calendar support (Limitation 2) and futures convexity (Limitation 3). None applied here.


Validation Evidence

Validation Packet

Check Where Evidence
DFs in (0,1] and monotonically decreasing FinView tests/test_curve_builder.py::test_flat_curve_discount_factors flat 5% curve → every 0 < DF ≤ 1 and DF_i < DF_{i-1}
Zero rates positive test_zero_rates_positive positive par yields → positive zeros ✓
Flat curve recovers par test_flat_curve_zero_rates_near_yield flat 5% → all zeros within 50 bps of 5% ✓
Upward slope preserved test_upward_curve upward par yields → zero(30y) > zero(0.25y) ✓ (snapshot: 5.3564% > ~4.28%)
Interpolation exact at knots test_interpolate_curve exact at 1y/5y/10y, monotone between ✓
Spread lowers DFs test_spread_adjustment +50 bps SOFR–TSY spread → every DF_spread < DF_base
Multi-curve DFs valid TestMultiCurve::test_build_sofr_curve_valid_dfs build_sofr_curve → 10 DFs, all in (0,1] ✓
Custom tenor grid honored test_build_sofr_curve_custom_tenors 3-tenor grid → 3 DFs ✓
SOFR ≠ Treasury test_sofr_curve_differs_from_treasury curves differ when rates differ ✓ (snapshot: multi-curve True, −445 bps DF spread @10y)
CurveSet fallback test_curveset_fallback_to_treasury no OIS → get_discount_dfs() returns Treasury ✓
CurveSet OIS routing test_curveset_with_ois OIS present → get_discount_dfs() returns OIS ✓
Credit-spread lookup test_credit_spread_retrieval rating-keyed spread dict; unknown rating → None ✓
Self-reprice (calibration consistency) scripts/model_snapshots.py M-137 par instruments restruck off bootstrapped curve; residual < 5 bps ≤ 10y, 66 bps @20y (Limitation 1) ✓/⚠
Calibration self-check function (implemented) FinView finlib/curve_builder.py::reprice_residuals (INV-028 fix #92) reprices each calibrating par instrument off the built curve, returns max residual in price-bps; wired into bootstrap_sofr_curve/build_sofr_curve via self_check_tol_bps (logger.warning above tolerance). Implemented; defaulted off (warns, does not raise) ✓
Full curve-builder suite pytest tests/test_curve_builder.py 12 passed

Self-reprice invariant: at each grid knot the bootstrapped terminal DF(T) reprices its calibrating par instrument to 1.0 by construction; the only non-trivial residual is interpolation-driven at the sparse long end (Limitation 1), which is a known, bounded artifact rather than a correctness failure. This invariant is now machine-checkable via reprice_residuals() (implemented, defaulted off — Validation Packet row above).

Cross-venv reproducibility: the snapshot is numpy-only (no InsModel dependency) and produces byte-identical output under both the FinView venv ([redacted]) and the InsModel venv ([redacted], importing finlib off the FinView source on sys.path).

Open MRM items: (a) calibration self-check implemented (reprice_residuals, INV-028 fix #92) — enablement-by-default (self_check_tol_bps / refine_passes) + gold re-freeze pending (INV-028 follow-up, Limitation 1); (b) cross-repo FinView change-control / drift gate (Limitation 6 / INV-032). Both should be filed as tracked governance issues.


References

References

Curve construction / interpolation literature: - P. S. Hagan and G. West, "Interpolation Methods for Curve Construction," Applied Mathematical Finance 13(2), 2006 — the reference for log-linear vs monotone-convex discount-factor interpolation and the forward-oscillation pathologies the PCHIP path avoids. - P. S. Hagan and G. West, "Methods for Constructing a Yield Curve," Wilmott Magazine, 2008 — practitioner treatment of bootstrap + interpolation trade-offs. - F. M. Ametrano and M. Bianchetti, "Everything You Always Wanted to Know About Multiple Interest Rate Curve Bootstrapping But Were Afraid to Ask" (2013) — the multi-curve (OIS-discounting / projection-curve) framework that build_sofr_curve + CurveSet implement. - L. B. G. Andersen and V. V. Piterbarg, Interest Rate Modeling, Vol. I, Ch. 6 — multi-curve discounting and forward-curve construction post-LIBOR.

Engine source: - FinView:finlib/curve_builder.pybootstrap_sofr_curve, build_sofr_curve, interpolate_curve, _interpolate_df, and the CurveSet dataclass; governance sentinels CurveBuilderGovernance (finview.L0.curve_builder), CurveSetGovernance (finview.L0.curve_set), InterpolationGovernance (finview.L0.interpolation). - FinView:tests/test_curve_builder.py — 12-test validation suite (TestCurveBuilder, TestMultiCurve); fixtures flat_curve_5pct / upward_curve in tests/conftest.py.

Internal: - insightalm/modelling/engine_registry.yaml — engine entries finview.L0.curve_builder, finview.L0.curve_set, finview.L0.interpolation (all tier-2-gate / tier-3-infra, model_membership: [M-137] for the first two), owner engine-owner-foundation. - Downstream consumers: M-130 (bond pricer), M-132 (IRS pricer, input curve_set), M-133 (swaption pricer), M-135 (SOFR-futures pricer). - BV-032 (firm-data divergence) — this card's snapshot is BV-032-immune by construction (synthetic par inputs, no live market pull).


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 finlib/curve_builder.py + engine_registry + bound DS/A entries. Stub marker advanced to ``.
  • 2026-06-06 — code-grounded documentation-accuracy pass against the post-INV-028 engine state (FinView #92). Added Standards Coverage (ASOP-56 reliance + SR 26-2 aggregate/concentration) and Dependencies (no upstream models; 6 downstream consumers M-130/M-132/M-133/M-135 + family) sections. Updated Methodology + Limitation 1 to reflect that reprice_residuals() self-check and the refine_passes self-consistency loop exist but default OFF (refine_passes=0, self_check_tol_bps=None) to preserve frozen gold curves — so default/gold output still carries the long-end residual; replaced "not yet wired into a calibration self-check" with "self-check implemented but not yet enabled-by-default". Reconciled the Output Snapshot residual by re-running scripts/model_snapshots.py M-137 (default forward pass reproduces 66.10 bps; noted the refinement-on improvement ~0.14 bps as available-but- off). Added Validation-Packet row for reprice_residuals and reworded open MRM item (a). Noted the engine docstring's "cross-check against Bloomberg SOFR curve" as an aspirational line, not a wired validation (numpy-only, no Bloomberg dependency — Limitation 7). Noted gated/ratification items (INV-028 default-on + gold re-freeze, INV-032 drift gate, non-negative-forward guard, day-count / futures convexity) under Limitations. No model outputs, back-test numbers, or validation results were fabricated or changed. Code-side history stays in FinView git log.

2L Inventory Review

Open findings (2)

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.

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-137-v1.0.1

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