Benchmark Curve Resolution Implementation Plan
Status
Implemented. This document records the verified pricing gap, the target contract, and the implementation tasks that closed it.
Success Condition
Benchmark-driven curve resolution is correct when a bond with
benchmark_rate_index_uid can resolve the intended valuation curve through
PricingMarketDataSetCurveBindingTable for the requested market-data set, role,
selector, and quote side; missing rows fail with actionable errors; z-spread and
other curve-dependent APIs do not silently fall back to unrelated curves; and
curve/fixing caches remain effective for repeated calls with the same resolved
curve identity.
Verified Original State
benchmark_rate_index_uid is an instrument payload field. It points to
IndexTable.uid. It is not a curve UID and it is not enough to select a curve
by itself.
The complete curve selection key is:
market_data_set
+ role_key
+ selector_type = index
+ selector_key = <benchmark_rate_index_uid>
+ quote_side
-> PricingMarketDataSetCurveBinding.curve_uid
-> Curve.unique_identifier
-> DiscountCurvesStorage.curve_identifier
-> CurveBuildingDetails.curve_uid
-> QuantLib YieldTermStructureHandle
The pre-implementation code did not fully enforce that graph:
Bond.get_benchmark_index_curve()builds a QuantLib index throughresolve_quantlib_index(...)and returns its forwarding term structure. It does not let the caller provide a benchmark-specific role or quote side.resolve_quantlib_index(...)defaults an index curve request to theprojectionrole whencurve_type="discount"is passed. That behavior is acceptable for floating-rate projection by convention, but it is not a z-spread base curve rule.Bond.z_spread(...)tries an explicit curve, then a floater index curve, thenget_benchmark_index_curve(), then a default curve. It catches exceptions from index and benchmark resolution and converts them toNone, so a missing benchmark binding can be hidden until a later generic "No discount curve" error.PricingMarketDataSetCurveBindingsupportsquote_side, andcurve_binding_key(...)usesdefaultwhenquote_sideis omitted. The floating bond example previously wrote aquote_side="mid"binding, while pricing calls did not pass quote side. That meant the stored binding key wasprojection:index:<uid>:mid, but the resolver asked forprojection:index:<uid>:default.- The resolver API partly conflates role and curve type.
z_spread_baseis a valuation role, not necessarily the physicalCurve.curve_type.
Tests that mock the resolver or only assert that a method was called do not prove the end-to-end binding graph is correct. The example must be smoke-tested against the same binding key that the runtime requests.
Target Contract
Persisted instrument payloads may store an index UID, such as
benchmark_rate_index_uid, floating_rate_index_uid, or float_leg_index_uid.
Runtime curve selection must not assume that an index UID implies a unique curve.
Every code path that resolves a curve from persisted market-data bindings must accept or carry the full selection context:
market_data_set
role_key
selector_type
selector_key
quote_side
optional explicit curve_uid or curve_unique_identifier override
optional expected curve_type validation
For benchmark z-spread, the default binding role should be:
role_key = "z_spread_base"
selector_type = "index"
selector_key = str(benchmark_rate_index_uid)
quote_side has no implicit mid fallback. If a caller wants the mid curve,
the caller must pass quote_side="mid" or the example must create a
quote_side=None default binding. The implementation should choose one policy
per example and test it end to end.
role_key and curve type validation must be decoupled. A role such as
z_spread_base may select a curve whose Curve.curve_type is discount,
projection, government, or another supported build type. The binding role
describes why the curve is selected; the curve row describes what the curve is.
Implementation Tasks
- [x] Introduce a shared curve selection context.
Add a small pricing-owned helper, for example under
msm_pricing.pricing_engine, that carries the binding selection fields and can
produce stable cache keys.
Required behavior:
- [x] normalize
market_data_setthroughPricingMarketDataSet.resolve_uid(...)when a persistent selection is needed; - [x] normalize
quote_sidewith the same rules ascurve_binding_key(...); - [x] expose constructors for index-selected roles, especially projection, forwarding, discount, and z-spread base;
- [x] include role, selector, quote side, explicit override, and market-data set in cache keys;
-
[x] avoid storing mutable QuantLib handles inside the selection object.
-
[x] Add a direct curve-for-index resolver helper.
Add a helper that resolves a curve handle directly from an index selector:
resolve_curve_for_index_binding(
index_uid=...,
valuation_date=...,
market_data_set=...,
role_key=...,
quote_side=...,
curve_uid=None,
curve_unique_identifier=None,
expected_curve_type=None,
)
This helper should call the semantic index curve selection path, which resolves
through PricingMarketDataSetCurveBinding.resolve_index_curve_uid(...). It
should not build a QuantLib index just to get a curve handle, and callers should
not need to pass raw selector fields for index-scoped resolution.
Resolver validation must change so role_key selects the binding and
expected_curve_type validates the selected curve only when the caller asks for
that validation.
- [x] Fix benchmark z-spread resolution.
Update Bond.z_spread(...) so benchmark resolution follows the explicit
binding graph:
- [x] use caller-provided
discount_curveif supplied; - [x] for floating-rate instruments, resolve the floating projection curve through its own explicit projection selection context;
- [x] if
benchmark_rate_index_uidexists, resolverole_key="z_spread_base"throughPricingMarketDataSetCurveBinding; - [x] only use
_get_default_discount_curve()when no benchmark selector is configured.
Do not catch missing benchmark binding errors as a clean fallback. If
benchmark_rate_index_uid is configured and the selected market-data set has no
matching z_spread_base:index:<uid>:<quote_side> binding, raise an error that
names:
benchmark_rate_index_uid;- market-data set;
- role key;
- selector type/key;
- quote side;
-
operation (
z_spread). -
[x] Add binding arguments to public curve-dependent APIs.
Any public method that may resolve a curve from the market-data binding layer must expose or carry the selection inputs it needs.
Minimum bond scope:
- [x]
price(...): keep currentwith_yieldand explicit/default-curve behavior, but accept projection/discount quote-side arguments when the instrument family resolves a curve from bindings. - [x]
analytics(...),duration(...), andget_cashflows(...): pass through the same curve-selection context used byprice(...). - [x]
z_spread(...): acceptbenchmark_curve_role_key,benchmark_curve_quote_side, optional benchmark curve overrides, and optional expected curve-type validation. - [x]
carry_roll_down(...): continue using the already-built curve, but the cache and diagnostics must identify the selection context used by the priorprice(...)oranalytics(...)call.
Minimum swap scope:
-
[x]
InterestRateSwap.price(...)and cashflow methods must accept the floating projection quote side or a shared curve-selection context before resolvingfloat_leg_index_uid. -
[x] FastAPI operation metadata must expose the new parameters where applicable so frontend calls can request the same binding key that setup created.
-
[x] Preserve and verify caching.
MSDataInterface.get_historical_discount_curve(...) is cached at the interface
method boundary. Its effective cache key includes the curve identity argument,
target date, and market_data_set. That cache should remain in place.
Bond-level caches must include the normalized curve selection context. A change
from quote_side="mid" to quote_side="offer" must invalidate any cached
QuantLib index, pricer, price, analytics, duration, and z-spread result that
depends on the resolved curve.
Tests must verify:
- [x] repeated calls with the same benchmark selection do not reread the same curve observations unnecessarily;
- [x] changing quote side or role changes the binding lookup and invalidates the high-level pricing cache;
- [x]
MSDataInterface.cache_info()or a monkeypatched read counter proves curve reads are still cached for identical(curve_identifier, valuation_date, market_data_set)requests.
Required Tests
Add focused tests before claiming the implementation is correct:
- [x]
z_spreadwithbenchmark_rate_index_uidresolves throughPricingMarketDataSetCurveBinding.resolve_index_curve_uid(...)withrole_key="z_spread_base", the benchmark index UID, and the caller's quote side. - [x] Missing benchmark
z_spread_basebinding raises an actionable error and is not swallowed. - [x] A binding created with
quote_side="mid"is not found by an omitted quote-side request; the test should force the implementation or example to be explicit. - [x]
resolve_curve_for_index_binding(...)does not require a curve-owned index relationship. - [x]
role_keyandexpected_curve_typeare tested independently. - [x] Fixed-rate and zero-coupon bonds with
benchmark_rate_index_uidcan compute z-spread through the benchmark binding without requiringreset_curve(...). - [x] Floating-rate bonds still resolve their projection curve correctly after the shared selection context is introduced.
- [x]
InterestRateSwapprojection resolution accepts the same quote-side policy. - [x] FastAPI asset pricing operations accept and forward the new curve-selection parameters for z-spread and curve-preview.
- [x] The bond pricing example uses the same quote-side policy it writes.
Documentation And Example Tasks
Update all user-facing material in the same implementation:
- [x]
docs/knowledge/msm_pricing/runtime_resolution.md: add the benchmark z-spread path and make clear thatbenchmark_rate_index_uidis only an index selector. - [x]
docs/knowledge/msm_pricing/market_data_sets.md: documentz_spread_basebindings with quote-side policy. - [x]
docs/knowledge/msm_pricing/curves.md: show benchmark z-spread curve binding setup. - [x]
docs/tutorial/05-pricing.md: replace any curve-index shortcut language withCurve,CurveBuildingDetails, andPricingMarketDataSetCurveBinding. - [x]
docs/fast_api/v1/fixed_income_pricer.mdand the related ADR: document new operation parameters. - [x]
.agents/skills/ms_markets/pricing/fixed_income_curve_building/SKILL.md: keep agent guidance aligned so future changes do not claim benchmark resolution without a binding. - [x]
examples/msm_pricing/bond_pricing_example/main.py: either use a default quote-side binding or pass the same quote side to runtime calls. - [x] Add or extend an example for a fixed-rate bond with
benchmark_rate_index_uid, az_spread_basecurve binding, andz_spread(...)using the explicit selection context.
Non-Goals
This plan does not make CurveTable own an index foreign key.
This plan does not make a bare benchmark_rate_index_uid enough to resolve a
curve.
This plan does not silently default omitted quote side to mid.
This plan does not require fixed-rate price() to automatically use the
benchmark curve as its discount curve. That would be a separate pricing policy
decision. The immediate fix is that any function that claims to resolve a
benchmark curve must accept and use the binding selection context required to
resolve the correct curve.