Instruments
This page covers how priceable instrument payloads attach to assets through the
AssetCurrentPricingDetailsTable projection, and how account and portfolio
holdings become transient valuation baskets with ValuationLine and
ValuationPosition.
Asset To Instrument
AssetCurrentPricingDetailsTable is the fast one-row-per-asset table used by
Instrument.load_from_asset(asset). It stores the current serialized
instrument payload for the asset.
+-----------------------------+ one-to-one extension +-----------------------------+
| AssetTable |-------------------------------->| AssetCurrentPricingDetails |
|-----------------------------| asset_uid PK/FK |-----------------------------|
| uid PK | | asset_uid PK/FK |
| unique_identifier unique | | instrument_type |
| asset_type = bond | | instrument_dump |
+-----------------------------+ | pricing_details_date |
| serialization_format |
| pricing_package_version |
| source |
| metadata |
+-----------------------------+
Important rules:
asset_uidis the only primary key and a foreign key toAssetTable.uidwith cascade delete.- The serialized instrument payload is identity-free. It does not carry
asset_uid,uid, or the removedmain_sequence_asset_id. - Asset linkage is owned by
AssetCurrentPricingDetailsTable.asset_uid, not byInstrumentModel. - The public write path is timestamped: use
Instrument.attach_to_asset(asset)ormsm_pricing.api.add_pricing_details(...)for one asset. For large universes, usemsm_pricing.api.add_many_pricing_details(...); it serializes many asset/instrument pairs and writes them through chunked bulk upserts instead of one MetaTable operation per asset. Both paths writeAssetPricingDetailsStorage. AssetCurrentPricingDetailsTableis an internal current projection used for fast runtime loading. When the caller does not providepricing_details_date, the API usesnow()as the snapshot timestamp and updates this current projection. When the caller providespricing_details_date, the API upserts that exact timestamped snapshot and updates current when no current row exists or when the new date is newer than current.- Batch persistence chunks by caller
batch_sizeand sets each compiled MetaTable operation's SDKmax_rowslimit to cover the submitted chunk. Returned row counts are validated per bulk operation so partial write-returning responses fail instead of being treated as complete. - Application code should not update
AssetCurrentPricingDetailsTabledirectly as the normal UX. Direct row APIs are low-level infrastructure for the no-date current-snapshot path.
The related DataNode stays separate:
AssetCurrentPricingDetailsTable
grain: one current row per asset_uid
purpose: fast load/rebuild of the current priceable instrument
AssetPricingDetail DataNode
grain: (time_index, asset_identifier)
purpose: timestamped pricing metadata and historical pricing-detail records
Use the instrument API, not manual row assembly:
from msm.api.assets import Asset
from msm_pricing import Instrument, FloatingRateBond
from msm_pricing.api import add_many_pricing_details, load_instruments_from_assets
asset = Asset.get_by_unique_identifier("example-floating-bond")
bond = FloatingRateBond(
face_value=100,
floating_rate_index_uid=index.uid,
# remaining contract terms omitted
)
bond.attach_to_asset(asset)
loaded = Instrument.load_from_asset(asset)
add_many_pricing_details(
[
{"asset": asset, "instrument": instrument}
for asset, instrument in asset_instrument_pairs
],
batch_size=1000,
)
Instrument.load_from_asset(asset) reads the current pricing details row,
rebuilds the concrete instrument class stored in instrument_type, attaches
private runtime context such as _asset_uid, and returns the concrete object.
Typed loaders such as FloatingRateBond.load_from_asset(asset) use the same
path but reject mismatched stored instrument types.
For many assets, use the batch loader:
instruments_by_asset_uid = load_instruments_from_assets(assets, batch_size=1000)
It reads AssetCurrentPricingDetailsTable with chunked asset_uid IN (...)
searches, rebuilds concrete instrument classes, validates each instrument
against the corresponding asset, and returns a mapping keyed by Asset.uid.
Provider-Neutral Bond Terms
Use BondInstrumentTerms when an adapter already normalized source rows into
generic bond terms and only needs to construct an ms-markets instrument model.
This helper is not a source parser and is not a persistence API. It builds the
existing ZeroCouponBond, FixedRateBond, or FloatingRateBond classes, then
callers persist those instruments through the normal attach_to_asset(...),
add_pricing_details(...), or add_many_pricing_details(...) paths.
import datetime as dt
import uuid
import QuantLib as ql
from msm_pricing.instruments import (
BondInstrumentTerms,
build_bond_instrument_from_terms,
)
terms = BondInstrumentTerms(
instrument_type="fixed_rate_bond",
valuation_date=dt.date(2026, 1, 2),
issue_date=dt.date(2025, 1, 2),
maturity_date=dt.date(2027, 1, 2),
face_value=100.0,
day_count=ql.Actual360(),
calendar=ql.TARGET(),
business_day_convention=ql.Following,
settlement_days=2,
benchmark_rate_index_uid=uuid.uuid4(),
coupon_rate=0.05,
coupon_frequency=ql.Period(6, ql.Months),
)
instrument = build_bond_instrument_from_terms(terms)
Rules:
benchmark_rate_index_uidis optional for zero-coupon and fixed-rate bonds; it is benchmark analytics metadata, not a cashflow requirement.floating_rate_index_uidis required for floating-rate bonds.- When a floating-rate bond omits
benchmark_rate_index_uid, the builder usesfloating_rate_index_uidas the benchmark UID by default. - The helper does not resolve indexes, bootstrap reference rows, infer source classifications, repair schedules, or read assets.
quantlib_evaluation_settings(...)temporarily sets QuantLib global evaluation settings during construction and restores the previous settings afterward.
Run examples/msm_pricing/bond_terms.py for a fully offline construction
smoke test.
Account And Portfolio Valuation Inputs
msm_pricing does not query account holdings, target positions, or portfolio
weights directly. Those packages own snapshot selection and exposure
normalization. Pricing starts after the source rows have been reduced to asset
rows plus signed units. This boundary is defined in
ADR 0033.
Account holdings use asset_identifier, quantity, and direction. The
account workflow should select the holdings snapshot, resolve each
asset_identifier to an Asset row, then normalize units as
quantity * direction:
from msm_pricing.api import load_instruments_from_assets
from msm_pricing.valuation import build_valuation_position
assets = list(assets_by_identifier.values())
instruments_by_asset_uid = load_instruments_from_assets(assets)
valuation_rows = [
{
"instrument": instruments_by_asset_uid[asset.uid],
"units": row["quantity"] * row["direction"],
"asset_uid": asset.uid,
"metadata_json": {"source": "account_holding"},
}
for row in account_holding_rows
for asset in [assets_by_identifier[row["asset_identifier"]]]
]
position = build_valuation_position(
valuation_rows,
valuation_date=valuation_date,
market_data_set="eod",
)
Portfolio workflows follow the same boundary after they choose the portfolio composition source and convert weights, notionals, or quantities into concrete asset-level units:
assets = list(assets_by_uid.values())
instruments_by_asset_uid = load_instruments_from_assets(assets)
valuation_rows = [
{
"instrument": instruments_by_asset_uid[row["asset_uid"]],
"units": row["units"],
"asset_uid": row["asset_uid"],
"metadata_json": {"source": "portfolio_weight"},
}
for row in normalized_portfolio_rows
]
position = build_valuation_position(
valuation_rows,
valuation_date=valuation_date,
market_data_set="eod",
)
ValuationPosition has one market_data_set for the whole basket. Build
separate baskets when two groups of lines must be valued against different
market-data sets. See Market Data Sets for how the
selected set resolves to DataNode reads.
build_valuation_position(...) accepts normalized mappings or a pandas
DataFrame with required instrument and units fields, plus optional
asset_uid and metadata_json. The valuation date is required and the
market-data set is passed only at the basket level. Rows must not carry their
own market_data_set; source-specific selection and asset/instrument loading
belong before this pricing boundary.
Use PricingValuationContext for portfolio-style pricing instead of calling
private valuation-position helpers. The context prepares the known instrument
universe once, resolves supported fixed-income market-data rows with set-based
row API operations, stores the immutable input contract in
PricingValuationContextSpec, and returns copied/wrapped prepared instruments
so the caller-owned instrument terms are not mutated:
from msm_pricing.valuation import PricingValuationContext
context = PricingValuationContext.prepare_for_position(
position,
curve_quote_side="mid",
)
total_market_value = position.price(context=context)
per_line = position.price_breakdown(context=context)
prepared = context.prepare_instrument(position.lines[0].instrument)
assert prepared.instrument is not position.lines[0].instrument
unit_price = prepared.price()
observed_z_spread = prepared.z_spread(target_dirty_ccy)
PreparedInstrument.z_spread(...) uses the same prepared context injection path
as price(): the context supplies the selected market-data set and curve quote
side when the underlying instrument supports those arguments. Callers must pass
target_dirty_ccy as a normalized currency dirty price; source-specific quote
parsing, such as dirty price per 100 notional, remains outside the core
valuation context.
For curve scenario runs, use
msm_pricing.scenarios.curves.price_curve_scenario(...) when shocks can be
expressed as basis-point bumps to source curve key nodes. The workflow is:
- Build a
ValuationPositionfrom already normalized instrument/unit rows. - Prepare a
PricingValuationContextonce, or let the scenario helper prepare one from the position. - Create a
CurveScenariokeyed byCurve.unique_identifier. - Call
price_curve_scenario(...). - Read
CurveScenarioResult.base_market_value,scenario_market_value,market_value_delta,line_impacts,curve_shocks, and optional diagnostics.
from msm_pricing.scenarios.curves import (
CurveBumpSpec,
CurveScenario,
price_curve_scenario,
)
from msm_pricing.valuation import PricingValuationContext
context = PricingValuationContext.prepare_for_position(
position,
curve_quote_side="mid",
)
result = price_curve_scenario(
position,
CurveScenario(
name="parallel-up-25bp",
shocks_by_curve_identifier={
"USD-SOFR-3M-PROJECTION": CurveBumpSpec(parallel_bp=25.0),
},
),
context=context,
)
The helper delegates line pricing to msm_pricing.price_scenario(...) after it
has constructed line-scoped base and scenario curve handles. Use
price_scenario(...) directly only when the caller already owns the exact
line-scoped handles. In both paths, mutable scenario curve state is applied to
prepared copies instead of caller-owned instruments or cached base prepared
instruments.
Use msm_pricing.scenarios.valuation.run_valuation_scenario_workflow(...) when
the caller needs the broader orchestration layer around the same prepared
context: base valuation, one or more scenario runs, line-level partial-success
diagnostics, typed analytics and cashflows, line impacts, carry impacts, and
explicit observed dirty-price z-spread overlays. That workflow still delegates
curve shock mechanics to msm_pricing.scenarios.curves; it returns typed
records for downstream table adapters instead of producing pandas or dashboard
payloads inside core msm_pricing. See
Pricing Scenarios for the scenario package layout and
Valuation Scenario Workflow for the workflow
contract.
The prepared context is intentionally not expandable. Its spec records the
valuation date, market-data set, quote side, valuation-role requirements, and
submitted instrument universe. Calling prepare_instrument(...) with an
instrument that was not included in prepare(...) or
prepare_for_position(...) is rejected; build a new context for a different
universe.
The prepared context caches market-data concept bindings, index rows, index
convention rows, curve bindings, curve rows, curve-building details, curve
observations, fixing observations, QuantLib curve handles, and base QuantLib
indexes for index-referencing instruments. Prepared floating-rate bond pricing
uses that context state rather than re-entering backend row or observation
resolution in the line-pricing hot loop. See
examples/msm_pricing/pricing_valuation_context.py for a runnable offline
example that uses the repository's mock flat-forward curve and mock fixing
builders, prepares a mock index-referencing instrument through
PricingValuationContext, and demonstrates the copy boundary without requiring
live platform market data.