Skip to content

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:

  1. Persist or resolve the asset through msm.api.assets.Asset.
  2. Store the asset-to-pricing relationship in pricing-owned persistence, keyed by the asset UID.
  3. Keep InstrumentModel payloads limited to priceable terms. Do not include main_sequence_asset_id, uid, or asset_uid in 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:

  1. Register the canonical index type through msm.api.indices.IndexType. Fixed-income examples use the built-in interest_rate type.
  2. Persist the canonical index through msm.api.indices.Index.
  3. Upsert msm_pricing.api.IndexConventionDetails with the index UID and the serializable convention payload needed to rebuild the pricing index.
  4. Upsert msm_pricing.api.Curve with a stable curve unique_identifier.
  5. Upsert msm_pricing.api.CurveBuildingDetails for that curve.
  6. Upsert msm_pricing.api.PricingMarketDataSetCurveBinding through upsert_index_curve_selection(...) to bind the selected market-data set, valuation role, index UID, and quote side to the curve UID.
  7. Publish curve observations through DiscountCurvesNode with curve_identifier set to the same curve unique_identifier. Each emitted row must include a non-empty curve mapping for the constructed pricing nodes and key_nodes for the dated input quotes used to build that row. key_nodes is 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 optional CurveKeyNode helper 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 override DiscountCurvesNode.normalize_key_nodes(...) or attach set_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:

  1. Register or resolve the bond asset type, issuer, currency asset, and bond asset through msm.api.assets and msm.api.issuers.
  2. Register the interest_rate index type through msm.api.indices.IndexType, then register the canonical index through msm.api.indices.Index.
  3. Upsert IndexConventionDetails, separate projection and discount Curve rows, matching CurveBuildingDetails, and PricingMarketDataSetCurveBinding.upsert_index_curve_selection(...) rows under msm_pricing.api. Floating instruments need both role_key="projection" and role_key="discount" bindings for the floating index UID. The helper persists the generic selector fields internally, so index-based workflows should pass index_uid, not selector_type and selector_key.
  4. Publish one month of mock fixings through a FixingRatesNode subclass and a sampled flat-forward projection curve plus a sampled flat-forward discount curve through DiscountCurvesNode subclasses. Each curve storage row stores both the compressed pricing curve and compressed source-owned key_nodes provenance. The publisher emits key_nodes as JSON, and read/API helpers return decompressed JSON. The mock example uses the recommended yield-aware CurveKeyNode shape and intentionally omits source_reference because its flat-forward inputs are synthetic. Real publishers use an asset or index source reference as shown above. CurveBuildingDetails remains the source for final curve construction rules. The pricing storage classes declare their EOD cadence as __cadence__ = "1d".
  5. Attach pricing storage tables, then upsert the default market-data set and its concept bindings with PricingMarketDataSet and PricingMarketDataSetBinding.
  6. Create a FloatingRateBond with floating_rate_index_uid=index.uid.
  7. Add pricing details with instrument.attach_to_asset(asset, ...); without an explicit timestamp this creates the current loadable instrument definition.
  8. Reload it generically with Instrument.load_from_asset(asset), set the valuation date, then call price(market_data_set="default"), analytics(), get_cashflows(), and carry_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.
  9. 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 with instrument, units, optional asset_uid, and optional metadata_json. Rows must not carry their own market-data set because ValuationPosition has 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:

  1. Define the SQLAlchemy model under msm.models with MarketsMetaTableMixin and MarketsBase.
  2. Set __metatable_identifier__ to the stable table identity.
  3. Put schema, table info, indexes, and constraints in __table_args__.
  4. Do not set __tablename__; the markets mixin assigns the physical table name from the storage app segment and logical identity. Built-in tables use ms_markets, producing ms_markets__<lowercase-identity>. Project-local extension tables may set __markets_storage_app__, for example binance_spot, to produce project-owned names such as binance_spot__binancespotaccountdetails. The MSM_AUTO_REGISTER_NAMESPACE suffix still applies when configured before model import.
  5. Add the model to markets_sqlalchemy_models() in foreign-key dependency order.
  6. Generate or update a normal Alembic revision under the active namespace directory in src/migrations/versions/.
  7. 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:

  1. Define an abstract project-local mixin with the project's default __metatable_namespace__ and, when needed, a project-owned __markets_storage_app__.
  2. Define the SQLAlchemy model with that mixin and MarketsBase.
  3. Give it one stable __markets_base_identifier__; ms-markets combines this base identifier with the mixin namespace to produce the globally unique MetaTable identifier.
  4. Declare relationships with normal SQLAlchemy ForeignKey(...) targets.
  5. Add or sync the package/project migration that creates or refreshes the table and finalizes the schema.
  6. Attach at runtime with msm.start_engine(models=[MyModelTable]).
  7. Put row operations in an optional MarketsMetaTableRow wrapper.

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