Bond & Fixed Income Pricing — OAS / Duration / Convexity Family Price corporate, treasury, and structured bond positions with OAS, duration, and convexity for ALM, CECL, and counterparty exposure modeling.
Family-level model covering bond pricing methodology: discounting cash flows along the appropriate curve, computing option-adjusted spread (OAS), modified duration, and convexity. Two implementations: a primary production pricer (FinView L1) and an independent challenger (Gold Copy L1) used for Track A/B/C validation.
Inputs, processing, outputs
Methodology
M-130 is a first-principles fixed-rate bond pricer implemented in
finlib/bond_pricer.py and bound to the engine identifier
finview.L1.bond_pricer via the @engine(...) governance decorator. It is a
FinView-resident engine: the code and its tests live in the FinView repo
([redacted]), not in InsModel. It is consumed cross-repo by the
insurance asset/capital stack (fixed-income holdings valuation, duration/DV01
for ALM, OAS for credit-spread risk), but it is owned and versioned by FinView
— see Limitation 5.
The engine takes four scalar contract parameters — face_value, coupon_rate,
maturity_years, payment_frequency (default 2 = semi-annual), plus a rating
tag carried only for OAS context — and exposes five computational paths:
-
price_from_yield(ytm)— discrete present-value of the coupon stream and principal under a single flat yield. Coupon dates are generated by_payment_times()(anarangefromdtto maturity in steps of1/frequency, with the last node snapped exactly ontomaturity_years). Each cash flow is discounted by1 / (1 + y/freq)**nwheren = round(t*freq)is the integer period count. Returnsclean_price,dirty_price, andaccrued_interest. Because the engine assumes pricing on a coupon date, accrued interest is 0 and clean == dirty. -
price_from_curve(dfs, tenor_years, oas_bps=0.0)— the curve-based path. It converts the input discount factors to continuously-compounded zero rates (z = -ln(DF)/t), linearly interpolates those zeros onto the bond's coupon dates viafinlib.curve_builder.interpolate_curve, adds the OAS (in decimal) to every interpolated zero, rebuilds discount factors (exp(-(z+oas)*t)), and present-values the cash flows. It then back-solves aytmfrom the resulting clean price. (Per a#75guard the engine toleratesoas_bps=Nonefor proxy bonds, treating it as zero.) -
compute_ytm(market_price)— invertsprice_from_yieldwith a Brent root finder (scipy.optimize.brentq) over[-0.10, 1.0], widening to[-0.50, 2.0]on failure. Tolerancextol=1e-12. -
compute_duration(ytm)— Macaulay duration as the PV-weighted average cash-flow time, modified duration asmacaulay / (1 + ytm/freq), plus a finite-difference effective duration and convexity using a ±10bp yield bump:effective = (P_- − P_+)/(2·P0·dy)andconvexity = (P_- + P_+ − 2·P0)/(P0·dy²).
dv01(ytm) — total DV01 as a first-class engine output (added 2026-06-04
resolving INV-015). It parallel-bumps the yield by +1bp and returns the
resulting price decline (p0 − p_up, where both are clean_price from
price_from_yield), reported as a positive number — consistent with the
bump-based sensitivities in compute_duration. This is the engine's own
bump, not the card's derived clean · mod_dur · 1e-4 identity.
compute_key_rate_dv01(...)andcompute_oas(market_price, ...)— key-rate DV01 bumps each standard tenor (1,2,3,5,7,10,20,30y) by 1bp with a triangular weight that decays linearly to the adjacent key tenors and reprices; OAS is the Brent root over[-500, 2000]bps that equates the curve price to a target market price. The test suite asserts the OAS solver recovers a 130bp spread to within 1bp.
There is no settlement-date or business-day calendar in the finlib engine: the
day-count is implicit in the n = round(t·freq) period counting (effectively
30/360-like on whole coupon periods), and the curve uses continuous compounding
on actual/actual-style year fractions. This is the principal divergence from a
production calendar-aware pricer (the QuantLib challenger uses
Actual365Fixed — see Validation Packet).
Key Assumptions and Their Justification
| ID | Assumption | Value / Choice | Justification |
|---|---|---|---|
| A1 | Discount curve | Flat 5% par-yield curve, bootstrapped to SOFR discount factors by bootstrap_sofr_curve |
Reproduces the flat_curve_5pct test fixture exactly; a flat curve makes the par result analytically checkable (coupon = yield ⇒ price = par). |
| A2 | Compounding (yield path) | Discrete, (1 + y/freq)**n, semi-annual |
Market convention for US coupon bonds; matches the compute_ytm inverse so price→yield→price round-trips to <1e-10 (asserted by test_ytm_roundtrip). |
| A3 | Compounding (curve path) | Continuous, exp(-z·t), on interpolated zero rates |
Standard for curve construction; introduces a small (~0.4bp of price) discrete-vs-continuous gap the test bounds at <2% (test_price_from_curve). |
| A4 | Day-count / settlement | Implicit whole-period n = round(t·freq); pricing assumed on a coupon date (accrued = 0) |
Keeps the engine calendar-free and deterministic; the cost is no mid-period accrued interest and no holiday/roll convention (Limitation 1). |
| A5 | Payment frequency | 2 (semi-annual), overridable | US Treasury/corporate convention; short-tenor T-bills use higher freq so round(maturity·freq) ≥ 1 (guarded by TestUSTreasury). |
| A6 | OAS application | Parallel additive shift in bps on every interpolated zero rate | Simple, invertible spread definition; lets compute_oas recover a planted spread to <1bp (test_oas_solver). |
Prose. The canonical snapshot input is a $1,000-face, 5% semi-annual, 10-year BBB bond priced off the flat-5% bootstrapped curve. This input is chosen (not firm-sourced) precisely because it has a known closed-form answer: a bond whose coupon equals the flat discount yield must price at par. That makes the snapshot self-validating — any drift off $1,000 immediately flags a discounting or interpolation regression. The discrete (yield) vs continuous (curve) compounding choice is deliberate and is the one place the two price paths disagree; the engine's own test tolerates that gap at 2%, and the observed gap is ~0.4bp of price.
Output Snapshot
Deterministic single-bond run of BondPricer (finview.L1.bond_pricer) —
reproducible, requires no live market or firm data. Generated by
python scripts/model_snapshots.py M-130; the same numerics are asserted by
tests/test_bond_pricer.py (par-at-par, YTM round-trip, duration monotonicity,
convexity-positive, OAS recovery).
Input: $1,000 face · 5.00% coupon · 10y · semi-annual · BBB · flat-5% bootstrapped SOFR discount curve.
| output | value | meaning |
|---|---|---|
| clean_price | 1,000.00 | curve price; coupon = curve yield ⇒ par (raw 1000.0037, the ~0.4bp discrete-vs-continuous gap) |
| dirty_price | 1,000.00 | equals clean — priced on a coupon date, zero accrued |
| ytm (solved) | 5.0000 % | Brent inversion of the clean price (raw 4.99995%) |
| macaulay_duration | 7.9895 y | PV-weighted average cash-flow time |
| modified_duration | 7.7946 y | macaulay / (1 + ytm/2) |
| convexity | 73.6295 | finite-difference, ±10bp bump; positive as required for a vanilla bond |
| dv01 ($/1bp) | 0.7791 | engine-returned dv01(ytm) — price decline from a +1bp parallel yield bump (p0 − p_up, raw 0.77909362). Refreshed 2026-06-06 from scripts/model_snapshots.py M-130. The prior 0.7795 was the card's derived clean · mod_dur · 1e-4 figure; the engine bump is −0.00037 lower (convexity makes the actual +1bp decline slightly smaller than the linear duration estimate). |
| recovered OAS | 130.0000 bps | compute_oas recovers a planted 130bp spread to <1bp |
The par result is the load-bearing check: a 5% coupon discounted on a flat 5%
curve returns essentially face value, confirming the coupon-schedule generation,
zero-rate conversion, interpolation, and discounting all compose correctly. The
modified duration of ~7.79y is consistent with a 10y par bond, and convexity is
positive as a vanilla fixed-rate bond requires. DV01 is now an engine output
(dv01(ytm), a +1bp parallel bump); on a ~7.79y-duration $1,000 par bond it is
$0.7791/bp (raw 0.77909362), refreshed 2026-06-06 from the engine's bump. This is
−$0.00037 below the prior card-derived $0.7795 (price × mod-dur × 1bp identity):
convexity makes the actual +1bp price decline marginally smaller than the linear
duration estimate, so the engine bump and the duration identity agree to ~5 bps of
each other — a sanity check that both paths are consistent.
Risk measures captured 2026-06-04; dv01 refreshed 2026-06-06 from
scripts/model_snapshots.py M-130 (INV-015 engine method) · deterministic, no live data.
Limitations and Known Gaps
-
No accrued interest / settlement calendar. The engine assumes pricing on a coupon date, so
accrued_interest = 0andclean_price == dirty_pricealways. There is no business-day calendar, holiday roll, or actual/actual-vs-30/360 day-count switch — the day-count is implicit in integer period counting. Mid-period (between-coupon) pricing is therefore not supported; dirty price for a settled trade would be understated by the accrued amount. -
DV01 output — fully resolved (engine method + snapshot refreshed). Originally a limitation: the contract
outputslist nameddv01butBondPricerreturned nodv01key, so the card derived it fromclean · modified_duration · 1e-4. Resolved 2026-06-04: INV-015 added a first-classdv01(ytm)method (parallel +1bp bump returning the price declinep0 − p_up); the declared contract output is emitted. Resolved 2026-06-06 (COND-002 remediation): the Output Snapshot dv01 row was refreshed frompython scripts/model_snapshots.py M-130— the snapshot script now callspricer.dv01(ytm)(previously it recomputed the linear identity), and the card shows the engine value 0.7791 (raw 0.77909362), −0.00037 below the old derived 0.7795 (convexity gap). No remaining documentation-currency item on dv01. Key-rate DV01 was already implemented (compute_key_rate_dv01); the prior "total DV01 must be assembled by the caller" gap is closed by thedv01()method. -
Discrete/continuous compounding split. Yield-path pricing is discrete
(1+y/f)^n; curve-path pricing is continuousexp(-z·t). They do not agree exactly (≈0.4bp of price on the par snapshot). Code comments andtest_price_from_curveexplicitly accept this within a 2% band — fine for risk/ALM use, but a caller expecting tick-level price parity between the two paths will be surprised. -
OAS is a flat parallel zero-rate shift.
compute_oasand theoas_bpsargument add a single constant to every interpolated zero. There is no term-structure of spread, no option-exercise model, and no negative-convexity adjustment — so for callable/putable or MBS-style bonds the "OAS" here is really a Z-spread, not a true option-adjusted spread. -
Cross-repo ownership (FinView-resident). This engine lives in
[redacted]with its test in[redacted]. It is consumed by the insurance asset/capital stack but is not owned or versioned in InsModel. The model-card governance home (InsModel/insightalm) and the code home (FinView) diverge — consistent with BV-032 firm-data-path divergence. Any contract or registry change must be coordinated across both repos. -
No firm / 10-K grounding. The snapshot uses a chosen synthetic bond and a synthetic flat curve. No issuer, no real market quote, no 10-K holding feeds this card; the input is deterministic-by-construction so the par invariant holds. Real-portfolio pricing (issuer ratings, observed spreads, actual settlement dates) is out of scope for the snapshot.
Tracked for ratification. The following are output-changing / modeling-code items, noted here for transparency: (a) ~~refreshing the Output Snapshot
dv01value to the numberBondPricer.dv01()actually returns~~ — DONE 2026-06-06 (COND-002): the snapshot script now callspricer.dv01(ytm)and the card shows 0.7791; (b) the still- open part of INV-015 — a real accrual / day-count + settlement-date path soaccrued_interestis non-zero and mid-period dirty pricing is supported (Limitation 1 remains accurate; the source still hardcodesaccrued_interest = 0.0); (c) INV-032 — a packaged/versioned cross-repo interface + CI drift gate for the FinView-resident pricing library consumed by InsModel viasys.path(Limitation 5).
Validation Packet
| Check | Where | What it asserts |
|---|---|---|
| Gold-copy challenger | ecosystem/gold_copy/instruments/bond_pricer.py (goldcopy.L1.bond_pricer) |
An independent re-implementation (does not import FinView/InsModel; uses gold_copy.market_data.curve_builder). Method-for-method identical algorithm — same price_from_yield, price_from_curve, compute_ytm, compute_duration, compute_key_rate_dv01, compute_oas. Serves as the cross-engine challenger for the bond pricer. |
| QuantLib cross-validation | FinView/tests/test_ql_validation.py::TestBondValidation |
BondPricer.price_from_curve vs QuantLib FixedRateBond (DiscountingBondEngine, Actual365Fixed) for {4% 5y, 5% 10y, 3% 30y}, asserted within BOND_PRICE_TOL. QuantLib is a test-only dependency (@pytest.mark.quantlib, skipped if not installed). |
| Par-at-par | test_bond_pricer.py::test_par_bond_prices_at_par |
coupon = YTM ⇒ price = face (<$0.01). |
| YTM round-trip | test_ytm_roundtrip (+ T-bill variant) |
price→ytm→price closes to <1e-10. |
| Duration monotonicity | test_duration_monotonicity |
5y < 10y < 30y modified duration. |
| Higher yield ⇒ lower price | test_higher_yield_lower_price |
monotone price/yield. |
| Curve vs yield agreement | test_price_from_curve |
flat-5% curve price within 2% of yield price. |
| Convexity positive | test_convexity_positive |
vanilla-bond convexity > 0. |
| Key-rate DV01 sums to total | test_key_rate_dv01_sums_to_duration |
Σ KR-DV01 ≈ price·mod_dur·1bp within 15%. |
| OAS solver | test_oas_solver |
recovers a planted 130bp spread to <1bp. |
| Engine benchmarks | test_engine_benchmarks.py::TestBondPricerBenchmarks |
frozen golden prices (par, above-par at 4%, below-par at 6%, zero-coupon Macaulay = maturity). |
| T-bill / short-end guard | test_bond_pricer.py::TestUSTreasury |
sub-1y maturities never price at $0 and have positive small duration (empty-payment_times bug guard). |
Venv portability: the snapshot runs identically under both the FinView venv
([redacted]) and the InsModel venv
([redacted]) — same numbers to all printed
digits — given FinView is on sys.path. This confirms the cross-repo consumer
(InsModel asset stack) can execute the engine with no FinView-specific runtime
beyond numpy/scipy.
References
Fixed-income literature: - Fabozzi, F. J. Bond Markets, Analysis, and Strategies — clean/dirty price, Macaulay vs modified duration, convexity, DV01 definitions. - Tuckman, B. & Serrat, A. Fixed Income Securities — discount-curve pricing, Z-spread / OAS, key-rate durations. - Hull, J. Options, Futures, and Other Derivatives — continuous vs discrete compounding and zero-rate bootstrapping.
Engine and supporting code:
- [redacted] — BondPricer
(finview.L1.bond_pricer), the engine documented here.
- [redacted] — bootstrap_sofr_curve,
interpolate_curve (finview.L0.curve_builder), the discount-curve source.
- [redacted] — BondCashflows coupon
schedule (used by the T-bill cash-flow tests).
Challenger and tests:
- [redacted] — independent
Gold-Copy challenger (goldcopy.L1.bond_pricer).
- [redacted] — primary test suite.
- [redacted] — frozen golden cases.
- [redacted] — QuantLib cross-validation
(test-only dependency).
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 sections from
BondPricer(finview.L1.bond_pricer) + the bond_pricer test suites + bound A-NNN / DS-NNN registry entries. Stub marker advanced fromto. - 2026-06-06 — code-grounded documentation-accuracy pass against
finlib/bond_pricer.py. Corrected the DV01 description to matchBondPricer.dv01()per the INV-015 resolution (a first-classdv01(ytm)method added 2026-06-04 that parallel-bumps the yield +1bp and returns the price declinep0 − p_up): updated Methodology §4, the Output Snapshot dv01 row (value marked pending re-snapshot; not refreshed — output-changing, gated for ratification), and Limitation 2 (split the now-resolved DV01-method gap from the remaining snapshot-currency item). Added the Standards Coverage section (asop_56, sr_26_2, internal) mirroring M-001, grounded only in the existing framework tags. Confirmed Limitation 1 (no accrued interest / no settlement calendar / no day-count) remains accurate — source still hardcodesaccrued_interest = 0.0; INV-015 added onlydv01(), not the accrual path. Noted gated items (snapshot refresh, INV-015 accrual path, INV-032 cross-repo packaging) under Limitations. No model outputs, back-test numbers, or validation results were fabricated or changed. - 2026-06-06 — RAT-130-v1.0.0 remediation (1L, Decision 053). COND-002:
refreshed the Output Snapshot dv01 row from
python scripts/model_snapshots.py M-130to the engine's bump-based value 0.7791 (raw 0.77909362) — the snapshot script (InsModel/Models/scripts/model_snapshots.py::snap_M_130) was changed to callpricer.dv01(ytm)instead of recomputing theclean · mod_dur · 1e-4identity; recorded the −0.00037 diff vs the prior derived 0.7795 (convexity). Updated Output Snapshot, Methodology prose, Limitation 2, and the Tracked-for-ratification block. COND-001: the registry documentation_pack flags + lifecycle were reconciled to disk (model_card/change_log present, last_validated_on stamped) and the Tier-2 validation-evidence pack assembled atmodelling/validation_evidence/M-130/v1.0.0/README.mdpointing at the live 61-test suite + QuantLib + gold-copy challengers. No numbers fabricated.
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.
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.
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-130-v1.0.1
Latest ratification on file: RAT-130-v1.0.1. Authored by 2L (mrm-peer-reviewer) per Decision 028 charter §5 Pattern A.