Assets
The assets concept owns market asset identity. Asset is the user-facing API
model for registering and querying canonical market assets. It connects external
identifiers, asset categories, provider metadata, and asset-indexed data
products into a consistent Main Sequence representation.
Most application code should work with msm.api.assets.Asset. That Pydantic row
model is backed by msm.models.assets.AssetTable, the SQLAlchemy MetaTable
schema declaration used for platform registration and compiled SQL operations.
Use AssetTable when authoring schema, repository, or registration code; use
Asset when application code needs to create, upsert, filter, update, or delete
asset rows.
Asset Model
AssetTable is intentionally small. It is the asset registry, not the place to
store every instrument-specific field. The stable identity fields are:
uid: internal row identity used by relational detail tables.unique_identifier: the canonical public handle for the asset.asset_type: a short classification string, such ascrypto,equity, orfuture.
AssetType is the type registry. Register an asset_type before using it in
new asset workflows so the meaning of the string is discoverable. In the current
schema, Asset.asset_type is a string classification field whose values should
match rows in AssetType; it is not a database foreign key in this release.
Use msm.constants for built-in asset type keys instead of repeating literals
across projects:
from msm.constants import (
ASSET_TYPE_BOND,
ASSET_TYPE_CRYPTO,
ASSET_TYPE_CURRENCY,
ASSET_TYPE_CURRENCY_SPOT,
ASSET_TYPE_EQUITY,
ASSET_TYPE_FUTURE,
)
assert ASSET_TYPE_BOND == "bond"
assert ASSET_TYPE_CRYPTO == "crypto"
assert ASSET_TYPE_CURRENCY == "currency"
assert ASSET_TYPE_CURRENCY_SPOT == "currency_spot"
assert ASSET_TYPE_EQUITY == "equity"
assert ASSET_TYPE_FUTURE == "future"
The constants module also exposes BUILT_IN_ASSET_TYPE_DEFINITIONS, whose
entries can produce payloads for AssetType.upsert(...):
from msm.api.assets import AssetType
from msm.constants import BUILT_IN_ASSET_TYPE_DEFINITIONS
for definition in BUILT_IN_ASSET_TYPE_DEFINITIONS:
AssetType.upsert(**definition.as_payload())
+-----------------------------+ logical value +-----------------------------+
| AssetType |<----------------------------| Asset |
|-----------------------------| |-----------------------------|
| uid | | uid |
| asset_type unique | | unique_identifier unique |
| display_name | | asset_type |
| description | +-----------------------------+
| metadata_json |
+-----------------------------+
The intended extension model is relational composition. Do not extend
AssetTable by adding columns such as maturity, strike, expiry, issuer,
venue-specific payloads, or serialized pricing instruments. Instead, add a
detail table keyed by the asset row and keep the core AssetTable stable.
Futures use the same extension pattern:
+-----------------------------+ one-to-one detail +-----------------------------+
| AssetTable |-------------------------------->| FutureAssetDetailsTable |
|-----------------------------| asset_uid PK/FK |-----------------------------|
| uid PK | | asset_uid PK/FK |
| unique_identifier unique | | kind |
| asset_type | | underlying_index_uid FK |
+-----------------------------+ | settlement_asset FK |
| margin_asset FK |
| expires_at |
| contract_size |
| metadata_json |
+-----------------------------+
FutureAssetDetailsTable is the built-in futures detail table. Its public API
mirrors the current pattern: a SQLAlchemy FutureAssetDetailsTable for schema
work and a Pydantic Future row model for application code. For one-to-one
instrument details, the detail table's asset_uid should be both the primary
key and the foreign key to AssetTable.uid; a separate detail-row uid is
unnecessary.
Currency Assets
Single currency code/name metadata is user or provider-owned data. Keep it in
the workflow using it, then register single currencies as normal Asset rows
with asset_type="currency".
CurrencySpot is the built-in extension for tradable spot pairs such as
EUR/USD. The pair is stored as a normal Asset with
asset_type="currency_spot", while CurrencySpotAssetDetailsTable stores the
base and quote currency references:
from msm.api.assets import Asset, CurrencySpot
from msm.constants import ASSET_TYPE_CURRENCY
USD = {"code": "USD", "currency_name": "US Dollar"}
EUR = {"code": "EUR", "currency_name": "Euro"}
usd = Asset.upsert(unique_identifier=USD["code"], asset_type=ASSET_TYPE_CURRENCY)
eur = Asset.upsert(unique_identifier=EUR["code"], asset_type=ASSET_TYPE_CURRENCY)
eur_usd = CurrencySpot.upsert(
unique_identifier="BBG0013HGRV5",
base_currency_uid=eur.uid,
quote_currency_uid=usd.uid,
)
The typed asset API normalizes asset type strings before writing them:
"Currency" becomes currency, "Currency Spot" becomes currency_spot, and
"Future" becomes future. Friendly display names belong in
AssetType.display_name.
See Currency Assets for the schema, registration dependency
order, and the exact CurrencySpot.upsert(...) workflow.
Bond Assets
Bonds are normal Asset rows with asset_type="bond" plus a one-to-one
BondAssetDetailsTable row. Issuers are separate reference rows in IssuerTable,
not assets and not loose strings.
import datetime as dt
from msm.api.assets import Asset, Bond
from msm.api.issuers import Issuer
from msm.constants import ASSET_TYPE_CURRENCY
issuer = Issuer.upsert(
unique_identifier="example-issuer",
display_name="Example Issuer",
)
usd = Asset.upsert(unique_identifier="USD", asset_type=ASSET_TYPE_CURRENCY)
bond = Bond.upsert(
unique_identifier="example-usd-bond-2031",
issuer_uid=issuer.uid,
currency_asset_uid=usd.uid,
issue_date=dt.date(2026, 5, 27),
maturity_date=dt.date(2031, 5, 27),
status="ACTIVE",
)
See Bond Assets for the issuer table, bond detail schema, lifecycle status values, and registration dependency order.
See examples/msm/assets/us_treasury_bond_workflow.py for a Treasury note example
that uses CUSIP 91282CQQ7 as the canonical asset identifier, stores FIGI
BBG0221YLR31 in OpenFigiDetails, and keeps coupon/tenor terms out of the
minimal bond detail schema.
OpenFIGI As Asset Properties
OpenFIGI metadata is the built-in example of extending the asset model with
provider-specific properties. OpenFigiDetails is not part of AssetTable; it
is a one-to-one detail table keyed by the asset row. This keeps canonical asset
identity small while still allowing provider facts such as FIGI, ISIN, ticker,
security type, exchange, and raw provider payload to be stored relationally.
+-----------------------------+ one-to-one provider +-----------------------------+
| AssetTable |-------------------------------->| OpenFigiAssetDetailsTable |
|-----------------------------| asset_uid PK/FK |-----------------------------|
| uid PK | | asset_uid PK/FK |
| unique_identifier unique | | figi |
| asset_type | | composite |
+-----------------------------+ | share_class |
| isin |
| ticker |
| security_type |
| metadata |
| raw_payload |
+-----------------------------+
Application code should still start with the Asset API. Raw ticker symbols
are not good Asset.unique_identifier values: they can collide across venues,
change over time, and require exchange/security context to interpret. When a
ticker is the only identifier available for a listed provider-backed asset,
first resolve it through OpenFIGI with the required market, exchange, and
security context. Then register the asset with the resolved FIGI as the stable
Asset.unique_identifier, and store the ticker in OpenFigiDetails and
display snapshots.
If you already have a FIGI, resolve by FIGI:
from msm.api.assets import Asset, AssetType, OpenFigiDetails
from msm.constants import ASSET_TYPE_EQUITY
from msm.services.assets.openfigi import query_by_figi, query_figi
AssetType.upsert(asset_type=ASSET_TYPE_EQUITY, display_name="Equity")
normalized = query_by_figi("BBG00FNFPQH4")
If you only have a ticker, map the ticker to OpenFIGI first:
normalized = query_figi(
["AAPL"],
market_sector="Equity",
exch_code="US",
)[0]
Then upsert the canonical asset and its OpenFIGI detail row using
asset_uid=asset.uid:
asset = Asset.upsert(
unique_identifier=normalized["unique_identifier"],
asset_type=ASSET_TYPE_EQUITY,
)
details = OpenFigiDetails.upsert(
asset_uid=asset.uid,
figi=normalized["figi"],
composite=normalized["composite"],
share_class=normalized["share_class"],
isin=normalized["isin"],
ticker=normalized["ticker"],
name=normalized["name"],
exchange_code=normalized["exchange_code"],
security_type=normalized["security_type"],
security_type_2=normalized["security_type_2"],
security_market_sector=normalized["security_market_sector"],
security_description=normalized["security_description"],
unique_id=normalized["unique_id"],
unique_id_fut_opt=normalized["unique_id_fut_opt"],
metadata_text=normalized["metadata"],
raw_payload=normalized["raw_payload"],
)
OpenFigiAssetDetailsTable.asset_uid is both the primary key and the foreign key to
AssetTable.uid. There is no separate uid column on the detail table.
OpenFigiDetails.uid in the Pydantic row API resolves to the same value as
asset_uid so generic row identity helpers still have a stable row identifier.
Scope
Assets answer these questions:
- What is the canonical
unique_identifierfor an asset? - Which registered asset type classifies the asset?
- Which categories or category memberships describe the asset?
- Which provider details, such as OpenFIGI metadata, were used to resolve it?
Primary Modules
msm.models.assets: asset-related SQLAlchemy/MetaTable declarations, includingAssetTable,AssetTypeTable, asset categories, memberships, and provider detail tables.msm.models.assets.core: core asset registry model.msm.models.assets.types: asset type registry model.msm.models.assets.currency_spot: currency spot relationship detail model.msm.models.assets.bonds: bond relationship and lifecycle detail model.msm.models.issuers: issuer reference data used by bond assets.msm.api.assets: user-facing Pydantic rows and typed class operations forAsset,AssetType,AssetCategory,AssetCategoryMembership,Bond,CurrencySpot, andOpenFigiDetails.msm.api.issuers: user-facing Pydantic rows and typed class operations for issuer reference data.msm.constants: static built-in asset type keys and built-inAssetTypedefinitions for application and project code.msm.data_nodes.assets: asset-indexed DataNodes such asAssetSnapshot.msm.services.assets: application-facing asset service helpers over repositories.msm.services.assets.openfigi: OpenFIGI query, normalization, and row-building helpers.msm.models.assets.categories: category and membership models.msm.models.assets.provider_details: provider metadata such as OpenFIGI details.msm.repositories.assets,msm.repositories.asset_categories, andmsm.repositories.provider_details: MetaTable operation builders for asset control-plane records.
Key Contracts
The asset unique_identifier is the stable handle used by portfolios, holdings,
pricing details, and market data. Category membership should describe asset
classification without changing identity.
AssetType records a unique asset_type, optional display_name, optional
description, and optional metadata_json. Use it as a lightweight registry of
what each type string means in a namespace.
Pricing details are not the same thing as core asset details or market prices.
Serialized priceable instrument terms belong to msm_pricing, not
msm.models.assets: current terms are stored in
msm_pricing.models.AssetCurrentPricingDetailsTable, historical pricing-detail
observations live in msm_pricing.data_nodes.pricing_details, and users attach
or load them through Instrument.attach_to_asset(asset) and
Instrument.load_from_asset(asset). The current table and the timestamped
DataNode are independent storage paths, not views or automatic mirrors of one
another, so AssetCurrentPricingDetailsTable may contain rows while the
AssetPricingDetail DataNode is empty. Price histories and display snapshots
remain market-data workflows.
Creating, Querying, And Deleting Assets
Use Pydantic row models in msm.api.assets for normal application workflows.
They expose class methods over the active markets runtime and return typed
objects.
from msm.api.assets import Asset, AssetType
from msm.constants import ASSET_TYPE_CRYPTO, ASSET_TYPE_CRYPTO_DEFINITION
AssetType.upsert(**ASSET_TYPE_CRYPTO_DEFINITION.as_payload())
asset = Asset.upsert(
unique_identifier="example-asset-btc",
asset_type=ASSET_TYPE_CRYPTO,
)
asset_by_identifier = Asset.get_by_unique_identifier(
unique_identifier="example-asset-btc",
)
asset_by_uid = Asset.get_by_uid(asset.uid)
crypto_assets = Asset.filter(
unique_identifier_contains="example-asset-",
asset_type=ASSET_TYPE_CRYPTO,
)
# Optional cleanup for temporary custom assets only:
# Asset.delete(asset.uid)
When a workflow owns startup preflight, register the required MetaTables before row operations run:
import msm
runtime = msm.start_engine(models=["AssetType", "Asset"])
asset_table_handle = runtime.table("Asset")
Production code normally assumes the Asset MetaTable is already registered.
The process must initialize or attach the runtime during startup. If no runtime
exists, or if the active runtime does not include the required asset tables,
the row API raises with instructions to run explicit startup preflight.
Example and test workflows that need isolated MetaTables can opt into
the example namespace by setting MSM_AUTO_REGISTER_NAMESPACE before importing
msm.api row classes, then running explicit bootstrap:
import os
from examples.platform.bootstrap import (
EXAMPLE_NAMESPACE_ENV,
EXAMPLE_METATABLE_NAMESPACE,
)
os.environ.setdefault(EXAMPLE_NAMESPACE_ENV, EXAMPLE_METATABLE_NAMESPACE)
import msm
from msm.api.assets import Asset
from msm.constants import ASSET_TYPE_CRYPTO
msm.start_engine(models=["AssetType", "Asset"])
asset = Asset.upsert(
unique_identifier="example-asset-btc",
asset_type=ASSET_TYPE_CRYPTO,
)
The namespace is part of runtime/table registration, not an asset payload field.
Use msm.start_engine(models=[...]) for controlled startup preflight with a
selected table set.
Use runtime.context or runtime.table(...) for lower-level multi-table
repository operations that need to compile statements across registered models.
Prefer Asset.upsert(...) in setup scripts and repeatable examples because it
is safe to rerun for the same unique_identifier. Use lower-level
create_asset(...) only when a duplicate asset should fail the workflow.
Use AssetCategory and AssetCategoryMembership when the universe itself is a
named reusable object:
from msm.api.assets import Asset, AssetCategory
from msm.constants import ASSET_TYPE_CRYPTO
btc = Asset.upsert(unique_identifier="BTC", asset_type=ASSET_TYPE_CRYPTO)
eth = Asset.upsert(unique_identifier="ETH", asset_type=ASSET_TYPE_CRYPTO)
category = AssetCategory.upsert(
unique_identifier="crypto-majors",
display_name="Crypto Majors",
)
memberships = AssetCategory.replace_memberships(
category_uid=category.uid,
asset_uids=[btc.uid, eth.uid],
)
See examples/msm/assets/asset_category_workflow.py for a lifecycle example that
creates a category, adds assets, removes assets, and prints category membership
after each change. The normal run leaves assets in the category; only the
explicit cleanup flag clears and deletes the temporary category. The asset
examples share identifiers, asset type payloads, currency payloads, and FIGI
constants through
examples/msm/assets/utils/reference_data.py.
Use OpenFigiDetails.upsert(...) for typed provider metadata rows when the
asset row already exists. The OpenFIGI section above shows the provider-detail
extension pattern. Provider services may still build AssetTable or
OpenFigiAssetDetailsTable instances internally when authoring SQLAlchemy schema
rows.
Do not delete assets as part of the normal setup path. Only use cleanup for temporary test assets or custom organization-owned records. Public or shared mastered assets should be treated as reference data; remove category memberships or downstream references instead of deleting the canonical identity row.
See examples/msm/assets/asset_crud_workflow.py for a focused example that creates
temporary custom assets, registers asset types, resolves BBG00FNFPQH4 through
OpenFIGI, registers the returned provider details, writes an example
AssetSnapshot frame, searches by type, and lists the created assets. FIGI
resolution requires the Main Sequence secret OPEN_FIGI_API_KEY; create it in
www.main-sequence.app/app/main_sequence_workbench/secrets before running the
example. Cleanup is opt-in through --delete-temporary-assets.
See examples/msm/assets/currency_spot_workflow.py for a focused currency example
that creates EUR and USD assets, resolves BBG0013HGRV5 through OpenFIGI,
uses CurrencySpot.upsert(...) to create the EUR/USD pair, and writes
matching AssetSnapshot rows.
Asset-Indexed DataNodes
Timestamped asset facts, such as snapshots and pricing details, belong in
asset-indexed DataNodes instead of columns on AssetTable. See
Asset-Indexed DataNodes for the detailed
AssetIndexedDataNode contract, the difference from a generic Main Sequence
DataNode, and AssetSnapshot as the concrete implementation.
Extension Notes
Add new DataNode schemas under msm.data_nodes.assets when the output is
time-indexed market data. Add provider-specific lookup or normalization under a
service submodule such as msm.services.assets.openfigi. Add new persistent
fields in msm.models only when the platform schema needs to own the field.
There is intentionally no msm.assets package boundary. Asset identity belongs
to MetaTable models and repositories, timestamped asset facts belong to
DataNodes, and external provider integration belongs to services.