Pricing Instruments
Connect priceable instruments to the canonical assets you registered in Assets and Categories. This chapter covers pricing instrument identity, market-data set bindings, bond pricing, and a short note on extending the markets schema.
For the runtime model behind these row APIs, see Core Concepts.
Pricing instrument identity
Use this workflow when connecting pricing terms to a canonical asset:
- Persist or resolve the asset through
msm.api.assets.Asset. - Store the asset-to-pricing relationship in pricing-owned persistence, keyed by the asset UID.
- Keep
InstrumentModelpayloads limited to priceable terms. Do not includemain_sequence_asset_id,uid, orasset_uidin serialized instrument terms.
This keeps instrument reconstruction independent from Main Sequence persistence
identity. instrument.attach_to_asset(asset, ...) writes a timestamped
pricing-details observation. Without pricing_details_date, it uses now()
and updates the current projection for fast Instrument.load_from_asset(...)
reads. With an explicit date, it upserts that timestamped snapshot and updates
current when no current row exists or when the date is newer than current. See
examples/msm_pricing/instrument_identity_boundary.py for a minimal payload
boundary example.
For large universes, use msm_pricing.api.add_many_pricing_details(...) instead
of calling attach_to_asset(...) or add_pricing_details(...) in a loop. It
serializes many asset/instrument pairs and persists pricing rows through
chunked bulk upserts:
from msm_pricing.api import add_many_pricing_details, load_instruments_from_assets
add_many_pricing_details(
[
{"asset": asset, "instrument": instrument}
for asset, instrument in asset_instrument_pairs
],
batch_size=1000,
)
When an account, portfolio, or custom workflow already has asset rows and
signed units, use load_instruments_from_assets(...) to batch-load the current
instrument definitions before constructing ValuationLine rows:
instruments_by_asset_uid = load_instruments_from_assets(assets, batch_size=1000)
Account and portfolio packages still own snapshot selection and unit normalization. Pricing receives only the normalized valuation lines.
When a vendor or application has already normalized source data into generic bond terms, build the ms-markets instrument first and then use the same persistence path:
from msm_pricing.instruments import (
BondInstrumentTerms,
build_bond_instrument_from_terms,
)
instrument = build_bond_instrument_from_terms(terms)
instrument.attach_to_asset(asset)
BondInstrumentTerms is provider-neutral. It constructs zero-coupon,
fixed-rate, or floating-rate bond instrument models without resolving assets,
mapping source identifiers, or bootstrapping reference indexes. Run
examples/msm_pricing/bond_terms.py for an offline construction example.
When the pricing persistence tables are needed, attach them through
msm_pricing.bootstrap.attach_pricing_schemas(...). That startup flow includes
the core asset and index tables first, then pricing extension tables, and uses
the same direct backend attachment contract as msm.start_engine(...).
Pricing market-data sets
Pricing bootstrap also seeds default market-data bindings for the built-in pricing market-data set:
PricingMarketDataSet(set_key="default")
-> PricingMarketDataSetBinding(concept_key="discount_curves")
data_node_uid = DiscountCurvesStorage.get_meta_table_uid()
-> PricingMarketDataSetBinding(concept_key="interest_rate_index_fixings")
data_node_uid = IndexFixingsStorage.get_meta_table_uid()
The binding row maps (market_data_set_uid, concept_key) to a backend DataNode
storage table UID. Use msm_pricing.api.PricingMarketDataSet and
PricingMarketDataSetBinding when an application needs an eod, live, or
risk_manager source set:
from msm_pricing.api import PricingMarketDataSet, PricingMarketDataSetBinding
from msm_pricing.data_nodes.curves.storage import DiscountCurvesStorage
from msm_pricing.settings import (
PRICING_CONCEPT_DISCOUNT_CURVES,
PRICING_MARKET_DATA_SET_EOD,
)
market_data_set = PricingMarketDataSet.upsert(
set_key=PRICING_MARKET_DATA_SET_EOD,
display_name="EOD pricing market data",
)
PricingMarketDataSetBinding.upsert(
market_data_set_uid=market_data_set.uid,
concept_key=PRICING_CONCEPT_DISCOUNT_CURVES,
data_node_uid=DiscountCurvesStorage.get_meta_table_uid(),
storage_table_identifier=DiscountCurvesStorage.get_identifier(),
)
Pricing resolution looks up the active market-data set and concept, then reads
the resulting DataNode with APIDataNode.build_from_table_uid(...). Public
workflows should bind storage table UIDs; identifiers are diagnostic only.
Callers select a non-default source set at valuation time:
bond.price(market_data_set="eod")
bond.price(market_data_set="live")
Instrument write and read path
The user-facing write and read path belongs to instrument classes:
from msm_pricing import Instrument, ZeroCouponBond
bond = ZeroCouponBond(...)
bond.attach_to_asset(asset)
loaded = Instrument.load_from_asset(asset)
attach_to_asset(...) is the normal user-facing write path. It records the
timestamped pricing details first. Calls without pricing_details_date create a
current snapshot at now() and update the active instrument definition. Calls
with pricing_details_date reconcile that dated snapshot against the active
instrument definition and update current when the dated snapshot should win.
Use Instrument.load_from_asset(asset) when the asset is known but the concrete
pricing instrument is not. Use a typed loader such as
ZeroCouponBond.load_from_asset(asset) when the caller expects a specific
instrument family and wants a type check.
Index-referencing instruments
When an instrument references a market index, register the pricing registry rows before publishing curve observations:
- Register the canonical index type through
msm.api.indices.IndexType. Fixed-income examples use the built-ininterest_ratetype. - Persist the canonical index through
msm.api.indices.Index. - Upsert
msm_pricing.api.IndexConventionDetailswith the index UID and the serializable convention payload needed to rebuild the pricing index. - Upsert
msm_pricing.api.Curvewith a stable curveunique_identifier. - Upsert
msm_pricing.api.CurveBuildingDetailsfor that curve. - Upsert
msm_pricing.api.PricingMarketDataSetCurveBindingthroughupsert_index_curve_selection(...)to bind the selected market-data set, valuation role, index UID, and quote side to the curve UID. - Publish curve observations through
DiscountCurvesNodewithcurve_identifierset to the same curveunique_identifier. Each emitted row must include a non-emptycurvemapping for the constructed pricing nodes andkey_nodesfor the dated input quotes used to build that row.key_nodesis source-owned JSON at the publisher/API boundary and is compressed by storage. The base contract is JSON object/list provenance with JSON-serializable nested values. Prefer the optionalCurveKeyNodehelper or the standard fields shown below when they fit the source, including yield-native inputs for discount curves. Source-specific producers can add source-owned extensions and can overrideDiscountCurvesNode.normalize_key_nodes(...)or attachset_key_nodes_validator(...)when they need stricter validation.
return pd.DataFrame(
[
{
"time_index": valuation_timestamp,
"curve_identifier": curve.unique_identifier,
"curve": compressed_curve_nodes,
"key_nodes": [
{
"maturity_date": "2026-06-26",
"instrument_type": "direct_zero_rate",
"quote": 0.0500,
"quote_type": "zero_rate",
"quote_unit": "decimal",
"yield": 0.0500,
},
{
"maturity_date": "2026-08-25",
"source_reference": {
"type": "index",
"identifier": "USD_SOFR_SWAP_3M",
},
"instrument_type": "ois_swap",
"quote": 0.0508,
"quote_type": "par_rate",
"quote_unit": "decimal",
},
],
"metadata_json": {"source_snapshot": "example-2026-05-27"},
}
]
)
source_reference identifies the market input behind a key node. Use
type="asset" for a registered bond or other asset and type="index" for
a swap, futures, deposit, or other quote series registered in IndexTable.
The source reference is independent of helper_type; futures do not inherit
OIS-only construction fields. Do not publish the old top-level
asset_identifier or index_identifier key-node fields.
See examples/msm_pricing/pricing_registry_rows.py for the row API workflow.
CurveBuildingDetails.interpolation_method is enforced at runtime. Use one of
log_linear_discount, log_cubic_discount, linear_zero,
cubic_zero, natural_cubic_zero, monotone_cubic_zero, or linear_forward with
quote_convention="forward_rate". Deprecated QuantLib methods such as
log_linear_zero and MonotonicLogCubicDiscountCurve are rejected.
When the source curve is made from rate-helper instruments instead of direct
zero or forward nodes, use the generic helper reconstruction path instead of a
connector-local curve builder. Persist
CurveBuildingDetails.builder_type="rate_helper_curve", publish generic helper
key nodes with helper_type values such as overnight_deposit_helper and
ois_rate_helper, and provide the required runtime overnight index to the
resolver or scenario builder. Futures price helpers use explicit price quotes
such as helper_type="sofr_future_rate_helper" with
quote_type="futures_price" and quote_unit="price". Bond-helper curves use
helper_type="zero_coupon_bond_helper" or
helper_type="fixed_rate_bond_helper" with clean or dirty price quotes and
generic price units such as price_per_100. Cross-currency helper curves use
the same helper_schema="rate_helpers@v1" while carrying FX spot as
context/provenance rather than a QuantLib helper; FX swap and constant-notional
cross-currency basis helpers resolve collateral curves and base/quote indexes
through RateHelperRuntimeResolver. The primitive API also works fully in
memory:
import QuantLib as ql
from msm_pricing.pricing_engine.curves import (
CurveObservationExportConfig,
FixedRateBondHelperSpec,
InterestRateFutureHelperSpec,
OISRateHelperSpec,
OvernightDepositHelperSpec,
ZeroCouponBondHelperSpec,
export_curve_observation_nodes,
reconstruct_curve_handle_from_helper_specs,
reconstruct_curve_term_structure_from_helper_specs,
)
handle = reconstruct_curve_handle_from_helper_specs(
(
OvernightDepositHelperSpec(quote=0.0475, tenor="1D"),
InterestRateFutureHelperSpec(
quote=95.25,
reference_month="JUN",
reference_year=2026,
reference_frequency="Monthly",
),
OISRateHelperSpec(
quote=0.0480,
tenor="1Y",
settlement_days=2,
overnight_index=ql.Sofr(),
payment_convention="ModifiedFollowing",
payment_frequency="Annual",
payment_calendar=ql.TARGET(),
fixed_payment_frequency="Annual",
fixed_calendar=ql.TARGET(),
averaging_method="Compound",
),
),
valuation_date=valuation_date,
day_counter=ql.Actual360(),
)
nodes = export_curve_observation_nodes(
handle,
valuation_date=valuation_date,
node_days=[7, 30, 90, 180, 365],
)
bond_handle = reconstruct_curve_handle_from_helper_specs(
(
ZeroCouponBondHelperSpec(
quote=97.5,
quote_type="clean_price",
quote_unit="price_per_100",
settlement_days=0,
face_value=100.0,
maturity_date="2026-07-02",
issue_date="2026-01-02",
),
FixedRateBondHelperSpec(
quote=99.0,
quote_type="clean_price",
quote_unit="price_per_100",
coupon_rate=0.05,
issue_date="2026-01-02",
maturity_date="2027-01-02",
tenor="6M",
settlement_days=0,
face_value=100.0,
day_counter=ql.Actual360(),
),
),
valuation_date=valuation_date,
day_counter=ql.Actual360(),
)
Run examples/msm_pricing/curve_reconstruction.py for the offline helper
reconstruction and observation-export smoke test, including the neutral
cross-currency helper-key-node workflow.
For curves whose OIS helpers require non-default payment schedules, pass the
generic OIS schedule fields explicitly in OISRateHelperSpec or in helper key
nodes; do not keep a connector-local constructor only because the payment
frequency, payment calendar, or averaging method is non-default.
For bond-helper scenario curves, core ms-markets currently supports no-op
reconstruction and strict diagnostics for non-empty yield shocks. A future
yield-to-price conversion layer must explicitly price the bond from the bumped
yield before clean or dirty price helper quotes can be shocked generically.
For helper-built curves that need a persisted observation output such as
compounded annual zero rates, derive the export config from build details and
combine explicit front nodes with pillar export. Use a term structure, not only
a handle, when exporting QuantLib pillar dates:
term_structure = reconstruct_curve_term_structure_from_helper_specs(
helper_specs,
valuation_date=valuation_date,
day_counter=ql.Actual360(),
)
export_config = CurveObservationExportConfig.from_curve_building_details(
curve_building_details
)
nodes = export_curve_observation_nodes(
term_structure,
valuation_date=valuation_date,
node_days=[1],
include_pillar_dates=True,
config=export_config,
)
Serialized pricing instruments should reference these rows by UUID, not by
mutable names. Use floating_rate_index_uid on floating-rate bonds and
float_leg_index_uid on swaps. The runtime resolver turns those UUIDs into the
correct convention row, curve row, QuantLib index, curve, and fixing series.
Fixed-rate and zero-coupon bonds can also store benchmark_rate_index_uid for
benchmark analytics. That field is only the index selector; the benchmark curve
for z-spread must be bound explicitly:
from msm_pricing import FixedRateBond
from msm_pricing.api import PricingMarketDataSetCurveBinding
PricingMarketDataSetCurveBinding.upsert_index_curve_selection(
market_data_set_uid=market_data_set.uid,
role_key="z_spread_base",
index_uid=benchmark_index.uid,
quote_side="mid",
curve_uid=benchmark_curve.uid,
)
bond = FixedRateBond(
face_value=100.0,
issue_date=issue_date,
maturity_date=maturity_date,
day_count=day_count,
coupon_frequency=coupon_frequency,
coupon_rate=0.05,
benchmark_rate_index_uid=benchmark_index.uid,
)
bond.set_valuation_date(valuation_date)
spread = bond.z_spread(
target_dirty_ccy=101.25,
market_data_set=market_data_set.set_key,
benchmark_curve_quote_side="mid",
)
If the binding is written with quote_side="mid", the runtime call must also
request "mid". Omitted quote side means the default binding key, not an
implicit mid quote.
When the computed spread must be applied to a resolved curve handle, keep that as a runtime overlay:
from msm_pricing.pricing_engine import apply_z_spread_to_curve
spreaded_curve = apply_z_spread_to_curve(benchmark_curve_handle, spread)
The helper expects the decimal continuous spread returned by z_spread(...).
It does not modify the published curve observation, key_nodes, or curve build
metadata.
Bond pricing workflow
For a full floating-rate bond workflow, use
examples/msm_pricing/bond_pricing_example/. It follows this order:
- Register or resolve the bond asset type, issuer, currency asset, and bond
asset through
msm.api.assetsandmsm.api.issuers. - Register the
interest_rateindex type throughmsm.api.indices.IndexType, then register the canonical index throughmsm.api.indices.Index. - Upsert
IndexConventionDetails, separate projection and discountCurverows, matchingCurveBuildingDetails, andPricingMarketDataSetCurveBinding.upsert_index_curve_selection(...)rows undermsm_pricing.api. Floating instruments need bothrole_key="projection"androle_key="discount"bindings for the floating index UID. The helper persists the generic selector fields internally, so index-based workflows should passindex_uid, notselector_typeandselector_key. - Publish one month of mock fixings through a
FixingRatesNodesubclass and a sampled flat-forward projection curve plus a sampled flat-forward discount curve throughDiscountCurvesNodesubclasses. Each curve storage row stores both the compressed pricingcurveand compressed source-ownedkey_nodesprovenance. The publisher emitskey_nodesas JSON, and read/API helpers return decompressed JSON. The mock example uses the recommended yield-awareCurveKeyNodeshape and intentionally omitssource_referencebecause its flat-forward inputs are synthetic. Real publishers use an asset or index source reference as shown above.CurveBuildingDetailsremains the source for final curve construction rules. The pricing storage classes declare their EOD cadence as__cadence__ = "1d". - Attach pricing storage tables, then upsert the
defaultmarket-data set and its concept bindings withPricingMarketDataSetandPricingMarketDataSetBinding. - Create a
FloatingRateBondwithfloating_rate_index_uid=index.uid. - Add pricing details with
instrument.attach_to_asset(asset, ...); without an explicit timestamp this creates the current loadable instrument definition. - Reload it generically with
Instrument.load_from_asset(asset), set the valuation date, then callprice(market_data_set="default"),analytics(),get_cashflows(), andcarry_roll_down(...). Pricing uses the projection binding for coupon forecasting and the discount binding for discounting. Binding one physical curve to both roles is valid only when both role rows explicitly point to that curve. - Use
build_valuation_position(...)when the valuation input is already normalized to instrument plus unit rows. For account or portfolio sources, the owning package still selects the source snapshot and resolves assets; pricing receives rows withinstrument,units, optionalasset_uid, and optionalmetadata_json. Rows must not carry their own market-data set becauseValuationPositionhas one basket-level source selection. The bond pricing example now values the loaded bond both as a single instrument and as a one-line valuation basket. For portfolio-style runs, prepare a public valuation context once and pass it to the basket methods:
from msm_pricing.valuation import PricingValuationContext, build_valuation_position
position = build_valuation_position(
normalized_rows,
valuation_date=valuation_date,
market_data_set="eod",
)
context = PricingValuationContext.prepare_for_position(
position,
curve_quote_side="mid",
)
total_market_value = position.price(context=context)
observed_z_spread = context.prepare_instrument(bond).z_spread(target_dirty_ccy)
PricingValuationContext returns copied or wrapped prepared instruments; it
does not mutate the caller-owned instruments in the submitted valuation
lines. The prepared context is fixed to the instrument universe submitted to
prepare(...) or prepare_for_position(...); build a new context when the
valuation date, market-data set, quote side, or instrument universe changes.
Prepared z_spread(...) calls expect target_dirty_ccy to already be a
currency dirty price; convert source quotes such as dirty price per 100
before calling the prepared instrument.
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. Shock maps are
keyed by Curve.unique_identifier, and the helper delegates line pricing to
msm_pricing.price_scenario(...) after building runtime scenario handles
from copied key-node provenance:
from msm_pricing.scenarios.curves import (
CurveBumpSpec,
CurveScenario,
price_curve_scenario,
)
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,
)
When the shocked curve is helper-reconstructed from OIS key nodes, pass
overnight_index or overnight_index_resolver to
price_curve_scenario(...); the high-level loop forwards it to scenario
handle construction. When the shocked curve is helper-reconstructed from
cross-currency key nodes, pass helper_runtime_resolver so collateral
curves and base/quote indexes are resolved explicitly.
Use msm_pricing.scenarios.curves.price_resolved_curve_scenario(...) when
the caller already has exact line-scoped base and scenario curve handles.
That resolved workflow accepts typed LineCurveResolution records and still
delegates line pricing to msm_pricing.price_scenario(...). The returned
CurveScenarioResult includes selected base and scenario handle maps by
line so application-owned analytics can reuse the same curve selection. In
both cases, scenario state is applied to prepared copies instead of
caller-owned instruments.
Use msm_pricing.scenarios.valuation.run_valuation_scenario_workflow(...)
when the caller needs the broader valuation workflow rather than only the
strict curve-scenario price delta. The valuation workflow prepares or reuses
the context once, delegates curve runtime override construction to
msm_pricing.scenarios.curves, prices base and scenario runs with
partial-success diagnostics, returns typed line prices, analytics,
cashflows, line impacts, carry impacts, and exposes observed dirty-price
z-spread overlays without mutating line metadata. Dashboard and API code
should convert those typed records to the required table shape outside core
msm_pricing.
For a fast local smoke test of that workflow, run
examples/msm_pricing/valuation_inputs.py for normalized valuation rows or
examples/msm_pricing/pricing_valuation_context.py for prepared
fixed-income context behavior. Run examples/msm_pricing/curve_scenario.py
for the context-resolved offline curve-scenario example, or
examples/msm_pricing/resolved_curve_scenario.py for the explicit-handle
workflow. Run examples/msm_pricing/valuation_scenario_workflow.py for the
generic base/scenario valuation workflow with typed impacts, carry, and
observed z-spread overlay output. These examples avoid live platform
market-data setup. See
Pricing Scenarios for the
canonical scenario documentation.
Optional spread analytics
Use msm_pricing.analytics.spreads when a workflow already has leg marks,
spread histories, or leg-level analytics in memory and needs reusable
relative-value statistics. This namespace is pure-data; it does not read
assets, portfolios, curves, pricing details, or platform storage.
Cross-asset helpers live in msm_pricing.analytics.spreads.base and are
re-exported from msm_pricing.analytics.spreads:
from msm_pricing.analytics.spreads import build_spread_series, spread_zscore_matrix
spread = build_spread_series(asset_marks, hedge_marks, hedge_ratio=1.25)
matrix = spread_zscore_matrix({"asset_vs_hedge": spread.values})
Fixed-income interpretation lives in
msm_pricing.analytics.spreads.fixed_income:
from msm_pricing.analytics.spreads import fixed_income_spread_metrics
metrics = fixed_income_spread_metrics(
base_values=asset_bond_marks,
hedge_values=benchmark_bond_marks,
base_dv01=100_000.0,
hedge_dv01=80_000.0,
)
The default fixed-income hedge ratio is base_dv01 / hedge_dv01, matching the
spread formula base - hedge_ratio * hedge. Future equity, index, commodity,
and option spread helpers should be added as sibling modules under
msm_pricing.analytics.spreads; fixed-income concepts should not be pushed
into the cross-asset base module.
Run examples/msm_pricing/fixed_income_spread_analytics.py for an offline
example that computes a DV01-neutral spread, z-score matrix, and forecast cone.
See Pricing Analytics for the package
boundary and optional dependency policy.
The reusable mock market-data components live in examples/msm_pricing/utils/ so
the same curve and fixing DataNode extension pattern can be reused by swap
pricing examples.
Extending the schema
The advanced workflows below cover adding project-owned relational tables to the markets schema. They follow the same migration-then-attach lifecycle as the built-in tables.
Markets MetaTable models
Use this workflow when adding or reviewing a market-domain relational table:
- Define the SQLAlchemy model under
msm.modelswithMarketsMetaTableMixinandMarketsBase. - Set
__metatable_identifier__to the stable table identity. - Put schema, table info, indexes, and constraints in
__table_args__. - Do not set
__tablename__; the markets mixin assigns the physical table name from the storage app segment and logical identity. Built-in tables usems_markets, producingms_markets__<lowercase-identity>. Project-local extension tables may set__markets_storage_app__, for examplebinance_spot, to produce project-owned names such asbinance_spot__binancespotaccountdetails. TheMSM_AUTO_REGISTER_NAMESPACEsuffix still applies when configured before model import. - Add the model to
markets_sqlalchemy_models()in foreign-key dependency order. - Generate or update a normal Alembic revision under the active namespace
directory in
src/migrations/versions/. - Use the SDK migration upgrade flow for schema mutation, then
msm.start_engine(...)for runtime attachment. Do not call model.register()methods or local registration helpers from application code.
msm.start_engine(...) resolves selected tables by backend identifier using
model.__table__.name; it does not import, register, or migrate missing
tables.
Examples that use example-scoped platform-managed MetaTables must set
MSM_AUTO_REGISTER_NAMESPACE=mainsequence.examples before importing
MetaTable-backed msm.api or msm.models modules, then run explicit startup
attachment. Row operations never register or attach MetaTables on first use. A
different namespace or registration configuration is rejected for the
already-initialized process.
Pass models=[...] to explicit preflight when a workflow only needs a subset of
tables, for example msm.start_engine(models=["Asset"]). Normal examples and
application code should use typed row classes such as
msm.api.assets.Asset.upsert(...). Use runtime.table(...) and
runtime.context only for lower-level repository or service internals.
See examples/msm/platform/inspect_markets_metatable_models.py for a small offline
inspection example that prints the SDK-derived table names. See
Markets Models for the model reference.
Project-local MetaTable extensions
Use this workflow when an application owns a custom markets table that should be migrated and registered with the same path as built-in tables:
- Define an abstract project-local mixin with the project's default
__metatable_namespace__and, when needed, a project-owned__markets_storage_app__. - Define the SQLAlchemy model with that mixin and
MarketsBase. - Give it one stable
__markets_base_identifier__; ms-markets combines this base identifier with the mixin namespace to produce the globally unique MetaTable identifier. - Declare relationships with normal SQLAlchemy
ForeignKey(...)targets. - Add or sync the package/project migration that creates or refreshes the table and finalizes the schema.
- Attach at runtime with
msm.start_engine(models=[MyModelTable]). - Put row operations in an optional
MarketsMetaTableRowwrapper.
MSM_AUTO_REGISTER_NAMESPACE still overrides the project mixin namespace when
set before model import. Use that for isolated tests and example runs; do not
make env-only namespace setup the main project extension contract.
The models=[...] selector is the public runtime attachment boundary. It
expands foreign-key dependencies, verifies and attaches the selected SQLAlchemy
model through direct backend lookup, and binds the resolved backend
MetaTable object back to that model. Do not build a project-local UID map or call
row create_schemas() helpers as the extension mechanism.
For asset detail tables keyed by AssetTable.uid, expose uid as an alias of
asset_uid in the row wrapper while keeping the SQLAlchemy primary key on
asset_uid.
See examples/msm/platform/custom_asset_details_extension.py for a minimal
project-local asset detail table, row wrapper, and startup function. See
Platform Extensions for the platform
extension reference.
Next → Pricing knowledge base